アイデアのために独自の言語プラグインを書く方法(その1 )


導入


私はバックエンド開発者として働いています、そして、時々、私は仕事ルーチンに疲れています.いくつかのオープンソースに深まって、私は本当にJetbrainsの製品やアイデアに熱心に役立ちます.アイデアを自分で書くことができるプラグインでカスタマイズすることができます.
アイデアのコードの大部分はオープンソースです.Jetbrainsから開発者は、常にプルの要求を作成し、メインツールを改善するのに役立ちます.
私は既に1つの検査で書く方法に関する記事を持っています.しかし、あなたがアイデアがどのように働くかを理解したいならば、あなたは言語プラグインを書くようにしなければなりません.
これをしましょう.
「モルモット」としては簡単な言葉を使うMonkey , インタプリタとコンパイラはbooks on Golang . 私の目標はすべてをカバーすることではなかったので、プラグインは、この言語のいくつかの限られたサブセットをカバーしています.インタプリタが見つかるhere .
猿のFibonacci数の計算例
let fibonacci = fn(x){
                    if (x == 0){
                        0;
                    }
                    else{
                        if (x == 1){
                            return 1;
                        }
                        else{
                            fibonacci(x - 1) + fibonacci(x - 2);
                        };
                    };
};
このプラグインをjavaとkotlin(jvm)で書きます.

必要条件


新しいプラグインを作成する最も簡単な方法はthe template . これは積極的に開発されており、すでに最も必要な機能が含まれていた.
また、Devkitプラグインが必要です

Grammar-Kit プラグインは、辞書と構文解析器を生成するために使用されます
他のプラグインの例

  • Java (考えのコードで)

  • go-plugin (このプラグインはGolandとして知られています).
  • Haskell
  • Erlang
  • Frege

  • Monkey plugin この記事では
  • 言語プラグインの基礎を作成する


    言語プラグインを作成する最初の段階はthe documentation .
  • 新しい言語を宣言する必要がありますgo-plugin , frege , monkey )
  • 猿の例
    import com.intellij.lang.Language
    
    class MonkeyLanguage : Language("Monkey") {
        companion object {
            @JvmStatic
            val INSTANCE = MonkeyLanguage()
        }
    }
    
    
  • アイコンを宣言するgo-plugin , frege , monkey )
  • 新しいファイルタイプを宣言し、一緒にすべてを組み合わせるgo-plugin , frege , monkey )
  • 猿の例
    import com.intellij.openapi.fileTypes.LanguageFileType
    import javax.swing.Icon
    
    class MonkeyFileType : LanguageFileType(MonkeyLanguage.INSTANCE) {
        override fun getName(): String {
            return "Monkey File"
        }
    
        override fun getDescription(): String {
            return "Monkey language file"
        }
    
        override fun getDefaultExtension(): String {
            return "monkey"
        }
    
        override fun getIcon(): Icon {
            return MonkeyIcons.FILE
        }
    
        companion object {
            @JvmStatic
            val INSTANCE = MonkeyFileType()
        }
    }
    
    その後、拡張ポイントを介して新しいファイルタイプを有効にする必要があります.プラグインが提供するすべての機能は、1つ以上の拡張ポイントを介して有効になります.ファイルプラグインで宣言しなければなりません.XML (例)go-plugin , frege ). エクステンションポイントを使用する他の例を以下に示しますdocumentation .
    サル( resource/meta - inf/plugin . xml )の例
    <extensions defaultExtensionNs="com.intellij">
      <fileType name="Monkey File"
                implementationClass="com.github.pyltsin.monkeyplugin.MonkeyFileType"
                fieldName="INSTANCE"
                language="Monkey"
                extensions="monkey"/>
    </extensions>
    

    クリエーションツリー


    残念ながら、1つの記事のコードを解析するのに必要な全体の理論を記述することは不可能です.このトピックの基本的な知識はDragon Book "

    この本によると、コードで作業するコンパイラのプロセスは次の手順から成ります.
  • 語彙分析器
  • 構文解析
  • 意味解析
  • 中間符号発生器
  • コード独立オプティマイザー
  • コードジェネレータ
  • マシン依存コードオプティマイザ
  • IDEが正常に動作するには、最初の3つの分析器を実装する必要があります.
  • 字句解析器(文字の流れを読み、トークンにグループ化する)
  • パーサ(トークンのストリームを受け取り、それらからシンタックスツリーを構築する)AST )
  • 意味解析器(いくつかの規則に従ってソースコードを検証するためにツリーを使用する).
  • アイデアでは、ASTツリーの代わりに、アナログが使用されます- PSI木(プログラム構造インタフェース).
    PSI木を作成するプロセスは、図からの図によく示されますdocumentation :

    PSIツリーを見るにはPSI Viewer ( Tools -> view psiの構造)

    考えでは、抽象的なTreeElementクラスは主にPSI木を実装するのに用いられます
    public abstract class TreeElement extends ElementBase implements ASTNode, Cloneable {
      private TreeElement myNextSibling;
      private TreeElement myPrevSibling;
      private CompositeElement myParent;
    ...
    }
    
    アイデアでは、あなたはGrammarKit Lexerとパーサーを作成するプラグイン.

    レクサー


    おそらく、アイデアのためのレクチャーを作成する最も簡単な方法はJFlex . GrammarkItプラグインには既に実装が含まれており、lexerを生成することができます.BNFファイルまたはから.フレックスファイル(より多くのカスタマイズオプションで).猿の言語の例を見ることができますhere , フレージュのためのより複雑なものhere .
    Lexerを生成するには、Gradleプラグインを設定するか、コンテキストメニューを使用する必要があります.フレックスファイル-“JFlexジェネレータを実行する”.
    その後、実装するクラスを宣言する必要がありますcom.intellij.lexer.Lexer . 既に生成されたjflex lexerのアダプタがあります.com.intellij.lexer.FlexAdapter

    パーサー


    アイデアでは、Grammarkitプラグインによるコード生成は、主にパーサーを作成するために使用されます.残念ながら、それについて多くのドキュメンテーションがありません、そして、それは2011年に発表されるだけですTutorial and HOWTO .
    言語の文法についてはBNF . 使用される唯一の違いは::= として.
    式の文法例here
    ご覧のように、BNFファイルは2つの部分から成ります.
  • 最初の部分は、メタ情報(そして、Flexファイルが使われないならば、トークンの説明)について説明します
  • 第二部は文法そのものを記述する.
  • メタ情報の一部を考えましょう.parserClass - 生成されたパーサクラスの名前と場所parserUtilClass - パーサのための追加メソッドのセットを含むクラスへの参照com.intellij.lang.parser.GeneratedParserUtilBase クラスまたはその継承子)extends = <some class> - すべてのPSI要素(ツリーノード)が継承される基底クラスへのリファレンス.通常com.intellij.extapi.psi.ASTWrapperPsiElement またはその継承子.extends(<regexp for tree nodes>) = <psi-element> (例:extends(".*expr")=expr ) - すべてのpsi要素は指定されたpsi要素から継承されます.psiClassPrefix , psiImplClassSuffix - クラスとインタフェースのための接頭辞(通常、言語の名前による)とインターフェイスを実装するための接尾辞(通常)Impl )psiPackage and psiImplPackage インタフェース用のパッケージとその実装です.implements - 同様extends , インターフェイスelementTypeHolderClass - 要素のすべての型の生成されたストレージelementTypeClass - Elemets型のクラス(生成されない、継承するクラス)com.intellij.psi.tree.IElementType )elementTypeFactory - 要素の型を生成するファクトリを作成するpsiImplUtilClass - psi要素の必須メソッドの実装として使用される静的メソッドのセットを持つクラス.
    このような行があるとしましょう
    ImportSpec ::= [ '.' | identifier ] ImportString {
      stubClass="com.goide.stubs.GoImportSpecStub"
      methods=[getAlias getLocalPackageName shouldGoDeeper isForSideEffects isDot getPath getName isCImport]
    }
    
    そのためには、psiImplUtilClass :
    public static String getAlias(@NotNull GoImportSpec importSpec)
    
    その後getAlias は以下のように生成されます:
      public String getAlias() {
        return GoPsiImplUtil.getAlias(this);
      }
    
    
    それでは、BNFのルール自体に移りましょう.修飾子は、各ルールに対して使用できます.private , fake , など).その説明はhere . 例えばprivate イン
    private boolean_group ::= xor_expr | between_expr | is_not_expr
    
    PSI要素がboolean_group は生成されません.
    BNFファイルで文法を正しく記述することができないなら、それは外部規則を使用してコードでそれを記述するのに役に立つでしょう.
    文法の重要な部分の1つは、エラーを扱うための規則です.つのキーワードが使われます:pin and recoverWhile .pin - トークン番号を示します.例えば、ゴングで構造を宣言する
    Struct Type ::= struct '{' Fields? '}' {pin=1}
    
    recoverWhile - すべてのルールが一致した後にトークンを消費できるかどうかを指定します.この属性の使用に関する推奨事項についてはhere .
    また、あなたは注意を払うべきですrecommendations 優先度に基づく構文解析用.
    将来の仕事のための正確で便利な文法規則を作成することは、言語のためにプラグインを実装する最も難しい部分のうちの1つであると私に思われます.始めるには、以下のようにします.go-plugin , Frege , Monkey (猿に対しては、この言語のサブセットのみを簡略化するために実装しています).
    BNFを作成してからパーサーを生成した後、File 相続人com.intellij.extapi.psi.PsiFileBase ) (例からgo-plugin , Frege , Monkey ) とパーサの定義クラスcom.intellij.lang.ParserDefinition ) 例go-plugin , Frege , Monkey ), して拡張ポイントを介して有効にします.
    <lang.parserDefinition language="Monkey"
    implementationClass="com.github.pyltsin.monkeyplugin.parser.MonkeyParserDefinition"/>