目录

我的学习分享

记录精彩的程序人生

X

NetBeans Lexer API

词法分析器模块定义LexerAPI,以提供对各种输入源的token序列的访问。
API入口点是TokenHierarchy类,其静态方法为给定的输入源提供其实例。

输入源

TokenHierarchy可以被创建为不可变的输入源(CharSequencejava.io.Reader),也可以被创建为可变的输入源(典型的如javax.swing.text.Document
对于可变输入源,词法分析器框架会自动对token层次结构中的token进行更新,并对底层文本输入进行后续更改。层次结构的token始终反映给定时间的输入文本。

TokenSequence和Token

TokenHierarchy.tokenSequence()允许迭代Token实例列表。
Token带有Token标识TokenId(由Token.id()返回)和表示为CharSequence(由Token.text()返回)的文本(也称为Token主体)。
TokenUtilities包含许多与Token文本操作有关的有用方法,例如TokenUtilities.equals(CharSequence text,Object o),TokenUtilities.startsWith(CharSequence text,CharSequence prefix)等。
也可以通过TokenUtilities.equals(CharSequence text)调试Token的文本(用转义符替换特殊字符)。
典型Token也包含在输入文本中出现的偏移量信息。

轻量级Token

由于存在很多Token实例,其中Token文本对于所有或许多实例都相同(例如,java关键字,运算符或单个空格的空白),因此可以通过允许创建Token实例(即仅一个令牌)来显着减少内存消耗,实例用于所有输入中所有Token的出现。
轻量级Token不带有有效偏移量(其内部偏移量为-1)。
因此,TokenSequence用于遍历Token(而不是常规迭代器),并且提供TokenSequence.offset(),即使将其放置在轻量级Token上也可以返回适当的偏移量。
当持有对Token实例的引用时,其偏移量也可以由Token.offset(TokenHierarchy tokenHierarchy)确定。 tokenHierarchy参数应始终为null,它将在将来的发行版中用于令牌层次结构快照支持。
对于轻量级Token,Token.offset(TokenHierarchy tokenHierarchy)返回-1,对于常规Token,其给出的值与TokenSequence.offset()相同。
在某些应用中,轻量级Token的使用可能会出现问题。 例如,如果解析器想在解析树节点中使用Token实例来确定节点的边界,则轻量级Token将始终返回offset -1,因此通常不能仅从Token中确定解析树节点的位置。
因此,可以通过使用TokenSequence.offsetToken()来检查当前Token,如果它是轻量级的Token,则将其替换为具有有效偏移量且具有与原始属性相同属性的非轻量级Token实例。

TokenId和Language

Token由TokenId接口表示的ID标识。 一种语言的Token ID通常实现为Java枚举(Enum的扩展名),但这不是强制性的。
给定语言的所有Token ID由Language描述。
每个Token ID可以属于一个或多个Token类别,这些类别允许更好地操作相同类型的令牌(例如,关键字或运算符)。
每个Token ID可以定义其主要类别TokenId.primaryCategory(),而LanguageHierarchy.createTokenCategories()可以为给定语言的Token ID提供其他类别。
每个语言描述都有一个强制的mime类型规范Language.mimeType()
尽管它是一点无关的信息,但它带来了很多好处,因为使用mime类型的语言可以附带任意种类的设置(例如语法着色信息等)。

LanguageHierarchy, Lexer, LexerInput and TokenFactory

希望提供一种语言的SPI提供者首先需要定义其SPI对应语言LanguageHierarchy。 它主要需要在LanguageHierarchy.createTokenIds()中定义Token ID,并在LanguageHierarchy.createLexer(LexerInput lexerInput,TokenFactory tokenFactory,Object state,LanguagePath languagePath,InputAttributes inputAttributes)中定义lexer。
Lexer从LexerInput读取字符,并将文本拆分为Token。
Token是使用TokenFactory的方法生成的。
由于每个Token的内存消耗至关重要,因此Token在SPI中没有任何对应对象。 但是,该框架阻止实例化除词法分析器模块的实现中包含的那些Token类之外的任何其他Token类。

Language Embedding

通过语言嵌入,令牌的平面列表实际上变成了由TokenHierarchy类表示的树状层次。 每个Token可能会分解为一系列嵌入式Token。
可以调用TokenSequence.embedded()方法来获取嵌入的Token(位于分支Token上时)。
有两种指定令牌中嵌入哪种语言的方法。 可以在LanguageHierarchy.embedding()方法中明确指定(硬编码)语言,也可以在默认的Lookup中注册一个LanguageProvider,这将为嵌入式语言创建一种Language。
语言层次的深度没有限制,可以根据需要添加任意数量的嵌入式语言。
在SPI中,语言嵌入由LanguageEmbedding表示。

API用例

获取各种输入的Token层次结构。

TokenHierarchy是Lexer API的入口点,它以Token的形式表示给定的输入。

    String text = "public void m() { }";
    TokenHierarchy hi = TokenHierarchy.create(text, JavaLanguage.description());

Swing文档的Token层次结构必须在读/写文档的锁定下进行。

    document.readLock();
    try {
        TokenHierarchy hi = TokenHierarchy.get(document);
        ... // explore tokens etc.
    } finally {
        document.readUnlock();
    }

从给定的偏移量获取和迭代特定Swing文档上的令牌序列。

Token覆盖了整个文档,可以向前或向后迭代。
每个Token都可以包含语言嵌入,也可以通过Token序列进行探索。 语言嵌入覆盖了Token的整个文本(在分支Token的结尾处可以跳过几个字符)。

    document.readLock();
    try {
        TokenHierarchy hi = TokenHierarchy.get(document);
        TokenSequence ts = hi.tokenSequence();
        // If necessary move ts to the requested offset
        ts.move(offset);
        while (ts.moveNext()) {
            Token t = ts.token();
            if (t.id() == ...) { ... }
            if (TokenUtilities.equals(t.text(), "mytext")) { ... }
            if (ts.offset() == ...) { ... }

            // Possibly retrieve embedded token sequence
            TokenSequence embedded = ts.embedded();
            if (embedded != null) { // Token has a valid language embedding
                ...
            }
        }
    } finally {
        document.readUnlock();
    }

典型客户:

  • 编辑器的绘制代码对lexer/editorbridge模块中的org.netbeans.modules.lexer.editorbridge.LexerLayer进行语法着色。
  • 支撑匹配代码在向前/向后方向搜索匹配的支撑。
  • 代码完成的快速检查插入符号是否位于注释Token中。
  • 解析器构造一个解析树,以正向遍历Token。

使用Token序列的语言路径

对于给定的Token序列,客户端可以检查它是否是Token层次结构中的顶级Token序列,或者它是否嵌入在哪个级别,以及它是什么父语言。
每个Token都可以包含语言嵌入,也可以通过Token序列进行探索。 语言嵌入覆盖了Token的整个文本(在分支Token的结尾处可以跳过几个字符)。

    TokenSequence ts = ...
    LanguagePath lp = ts.languagePath();
    if (lp.size() > 1) { ... } // This is embedded token sequence
    if (lp.topLanguage() == JavaLanguage.description()) { ... } // top-level language of the token hierarchy
    String mimePath = lp.mimePath();
    Object setting-value = some-settings.getSetting(mimePath, setting-name);

有关输入的其他信息

InputAttributes类可能包含有关在其上创建Token层次结构的文本输入的额外信息。 例如,可能存在有关输入所代表的语言版本的信息,并且可以编写词法分析器以识别该语言的多个版本。 通过一个简单的整数进行版本控制就足够了:

public class MyLexer implements Lexer<MyTokenId> {
    
    private final int version;
    
    ...
    
    public MyLexer(LexerInput input, TokenFactory<MyTokenId> tokenFactory, Object state,
    LanguagePath languagePath, InputAttributes inputAttributes) {
        ...
        
        Integer ver = (inputAttributes != null)
                ? (Integer)inputAttributes.getValue(languagePath, "version")
                : null;
        this.version = (ver != null) ? ver.intValue() : 1; // Use version 1 if not specified explicitly
    }
    
    public Token<MyTokenId> nextToken() {
        ...
        if (recognized-assert-keyword) {
            return (version >= 4) { // "assert" recognized as keyword since version 4
                ? keyword(MyTokenId.ASSERT)
                : identifier();
        }
        ...
    }
    ...
}

然后,客户端将使用以下代码:

    InputAttributes attrs = new InputAttributes();
    // The "true" means global value i.e. for any occurrence of the MyLanguage including embeddings
    attrs.setValue(MyLanguage.description(), "version", Integer.valueOf(3), true);
    TokenHierarchy hi = TokenHierarchy.create(text, false, SimpleLanguage.description(), null, attrs);
    ...

过滤掉不必要的Token

过滤仅适用于不可变的输入(例如String或Reader)。

    Set<MyTokenId> skipIds = EnumSet.of(MyTokenId.COMMENT, MyTokenId.WHITESPACE);
    TokenHierarchy tokenHierarchy = TokenHierarchy.create(inputText, false,
        MyLanguage.description(), skipIds, null);
    ...

典型客户:

  • 解析器构造一个解析树。 它对注释和空白标记不感兴趣,因此根本不需要构造这些标记。

SPI用例

提供语言描述和词法分析器。

Token ID应定义为枚举。 例如,可以从org.netbeans.modules.lexer.editorbridge.calc.lang.CalcTokenId复制org.netbeans.lib.lexer.test.simple.SimpleTokenId或以下示例。
静态language()方法返回描述Token ID的语言。

public enum CalcTokenId implements TokenId {

    WHITESPACE(null, "whitespace"),
    SL_COMMENT(null, "comment"),
    ML_COMMENT(null, "comment"),
    E("e", "keyword"),
    PI("pi", "keyword"),
    IDENTIFIER(null, null),
    INT_LITERAL(null, "number"),
    FLOAT_LITERAL(null, "number"),
    PLUS("+", "operator"),
    MINUS("-", "operator"),
    STAR("*", "operator"),
    SLASH("/", "operator"),
    LPAREN("(", "separator"),
    RPAREN(")", "separator"),
    ERROR(null, "error"),
    ML_COMMENT_INCOMPLETE(null, "comment");


    private final String fixedText;

    private final String primaryCategory;

    private CalcTokenId(String fixedText, String primaryCategory) {
        this.fixedText = fixedText;
        this.primaryCategory = primaryCategory;
    }
    
    public String fixedText() {
        return fixedText;
    }

    public String primaryCategory() {
        return primaryCategory;
    }

    private static final Language<CalcTokenId> language = new LanguageHierarchy<CalcTokenId>() {
        @Override
        protected Collection<CalcTokenId> createTokenIds() {
            return EnumSet.allOf(CalcTokenId.class);
        }
        
        @Override
        protected Map<String,Collection<CalcTokenId>> createTokenCategories() {
            Map<String,Collection<CalcTokenId>> cats = new HashMap<String,Collection<CalcTokenId>>();

            // Incomplete literals 
            cats.put("incomplete", EnumSet.of(CalcTokenId.ML_COMMENT_INCOMPLETE));
            // Additional literals being a lexical error
            cats.put("error", EnumSet.of(CalcTokenId.ML_COMMENT_INCOMPLETE));
            
            return cats;
        }

        @Override
        protected Lexer<CalcTokenId> createLexer(LexerRestartInfo<CalcTokenId> info) {
            return new CalcLexer(info);
        }

        @Override
        protected String mimeType() {
            return "text/x-calc";
        }
        
    }.language();

    public static final Language<CalcTokenId> language() {
        return language;
    }

}

请注意,不需要发布底层LanguageHierarchy扩展。
Lexer示例:

public final class CalcLexer implements Lexer<CalcTokenId> {

    private static final int EOF = LexerInput.EOF;

    private static final Map<String,CalcTokenId> keywords = new HashMap<String,CalcTokenId>();
    static {
        keywords.put(CalcTokenId.E.fixedText(), CalcTokenId.E);
        keywords.put(CalcTokenId.PI.fixedText(), CalcTokenId.PI);
    }
    
    private LexerInput input;
    
    private TokenFactory<CalcTokenId> tokenFactory;

    CalcLexer(LexerRestartInfo<CalcTokenId> info) {
        this.input = info.input();
        this.tokenFactory = info.tokenFactory();
        assert (info.state() == null); // passed argument always null
    }
    
    public Token<CalcTokenId> nextToken() {
        while (true) {
            int ch = input.read();
            switch (ch) {
                case '+':
                    return token(CalcTokenId.PLUS);

                case '-':
                    return token(CalcTokenId.MINUS);

                case '*':
                    return token(CalcTokenId.STAR);

                case '/':
                    switch (input.read()) {
                        case '/': // in single-line comment
                            while (true)
                                switch (input.read()) {
                                    case '\r': input.consumeNewline();
                                    case '\n':
                                    case EOF:
                                        return token(CalcTokenId.SL_COMMENT);
                                }
                        case '*': // in multi-line comment
                            while (true) {
                                ch = input.read();
                                while (ch == '*') {
                                    ch = input.read();
                                    if (ch == '/')
                                        return token(CalcTokenId.ML_COMMENT);
                                    else if (ch == EOF)
                                        return token(CalcTokenId.ML_COMMENT_INCOMPLETE);
                                }
                                if (ch == EOF)
                                    return token(CalcTokenId.ML_COMMENT_INCOMPLETE);
                            }
                    }
                    input.backup(1);
                    return token(CalcTokenId.SLASH);

                case '(':
                    return token(CalcTokenId.LPAREN);

                case ')':
                    return token(CalcTokenId.RPAREN);

                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                case '.':
                    return finishIntOrFloatLiteral(ch);

                case EOF:
                    return null;

                default:
                    if (Character.isWhitespace((char)ch)) {
                        ch = input.read();
                        while (ch != EOF && Character.isWhitespace((char)ch)) {
                            ch = input.read();
                        }
                        input.backup(1);
                        return token(CalcTokenId.WHITESPACE);
                    }

                    if (Character.isLetter((char)ch)) { // identifier or keyword
                        while (true) {
                            if (ch == EOF || !Character.isLetter((char)ch)) {
                                input.backup(1); // backup the extra char (or EOF)
                                // Check for keywords
                                CalcTokenId id = keywords.get(input.readText());
                                if (id == null) {
                                    id = CalcTokenId.IDENTIFIER;
                                }
                                return token(id);
                            }
                            ch = input.read(); // read next char
                        }
                    }

                    return token(CalcTokenId.ERROR);
            }
        }
    }

    public Object state() {
        return null;
    }

    private Token<CalcTokenId> finishIntOrFloatLiteral(int ch) {
        boolean floatLiteral = false;
        boolean inExponent = false;
        while (true) {
            switch (ch) {
                case '.':
                    if (floatLiteral) {
                        return token(CalcTokenId.FLOAT_LITERAL);
                    } else {
                        floatLiteral = true;
                    }
                    break;
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    break;
                case 'e': case 'E': // exponent part
                    if (inExponent) {
                        return token(CalcTokenId.FLOAT_LITERAL);
                    } else {
                        floatLiteral = true;
                        inExponent = true;
                    }
                    break;
                default:
                    input.backup(1);
                    return token(floatLiteral ? CalcTokenId.FLOAT_LITERAL
                            : CalcTokenId.INT_LITERAL);
            }
            ch = input.read();
        }
    }
    
    private Token<CalcTokenId> token(CalcTokenId id) {
        return (id.fixedText() != null)
            ? tokenFactory.getFlyweightToken(id, id.fixedText())
            : tokenFactory.createToken(id);
    }

}

包含Token ID和语言描述的类应该是API的一部分。 词法分析器应仅是实现的一部分。

提供语言嵌入。

可以在LanguageHierarchy.embedding()中静态提供嵌入,例如,参见 org.netbeans.lib.lexer.test.simple.SimpleLanguage。

或者可以通过使用“ Editors / language-mime-type / languagesEmbeddingMap”文件夹中的文件通过xml层动态提供,该文件由Token ID的名称命名,其中包含目标mime-type以及初始和结束跳跃长度:

    <folder name="Editors">
        <folder name="text">
            <folder name="x-outer-language">
                <folder name="languagesEmbeddingMap">
                    <file name="WORD"><![CDATA[text/x-inner-language,1,2]]>
                    </file>
                </folder>
            </folder>
        </folder>
    </folder>

http://bits.netbeans.org/dev/javadoc/org-netbeans-modules-lexer/overview-summary.html