浅析TypeScript Compiler 原理

发表于 2年以前  | 总阅读数:426 次

前言

众所周知,JavaScript是一款弱类型语言,变量的类型是在运行时动态决定的,2012年微软推出的typescript 给 javascript 扩展了类型的语法和语义,为JavaScript提供了静态检查的能力,这样能够提前发现类型不匹配的错误,还能够在开发时提示可用的属性方法,更利于大型项目的开发和维护。

那么TypeScript它究竟是如何工作的呢,这就要涉及TypeScript 编译器的相关原理了!

关键部分

  • Scanner扫描器:词法分析,生成token流

  • Parser解析器:生成AST

  • Binder绑定器:创建Symbol关联AST,形成语义

  • Checker检查器:类型检查

  • Emitter发射器:输出编译后的文件

处理流程

1 . 对于源代码,TS首先对它进行词法分析,通过scanner进行逐词扫描,生成token流

2 . 对于scanner生成的token,parser会对其进行组装并生成一棵AST

3 . binder会生成symbol(符号),并为AST上的每一个节点绑上相应的symbol

4 . checker检查处理后的AST,利用其进行语法检查

5 . emitter根据最终的AST生成JS代码和声明文件(d.ts)

scanner扫描器

什么是token

这里的token和平时前端使用的token并不是一个东西,此处的token实际上是一个标记。scanner会对源代码进行词法分析,根据每个“词”生成不同类别的标记(token),实质上是对“词”的一个分类过程。

例如const a = 1;这行代码,里面有const关键字,有变量a,有数字1,有结束标志;。这些每个都可以生成一个token,只是类别不同罢了。

TS compiler内部是将token的所有类型枚举出来了,在type.ts中可以找到,使用 SyntaxKind 存储标记类型(SyntaxKind 本质是numebr,比较起来性能更高)。

实际上,SyntaxKind不仅存储着token的类型,还存储了AST节点的类型,这个将于parser中用到。

export const enum SyntaxKind {
        Unknown,
        EndOfFileToken,
        SingleLineCommentTrivia,
        MultiLineCommentTrivia,
        NewLineTrivia,
        WhitespaceTrivia,
        ShebangTrivia, more pleasant manner.
        ConflictMarkerTrivia,
        NumericLiteral,
        BigIntLiteral,
        StringLiteral,
        JsxText,
        JsxTextAllWhiteSpaces,
        //...(more)
}

字符处理

在介绍scanner的工作流程之前,我想先跟大家介绍几个scanner中关于字符处理的函数。scanner所做的工作实质上就是词法分析的工作,因此免不了对源代码进行字符的合法性处理、位置判断等等。

CharacterCodes

export const enum CharacterCodes {
        _ = 0x5F,
        $ = 0x24,

        _0 = 0x30,
        _1 = 0x31,
        _2 = 0x32,
        _3 = 0x33,
        _4 = 0x34,
        _5 = 0x35,
        _6 = 0x36,
        _7 = 0x37,
        _8 = 0x38,
        _9 = 0x39,

        a = 0x61,
        b = 0x62,
        c = 0x63,
        d = 0x64,
        e = 0x65,
        f = 0x66,
        g = 0x67,
        h = 0x68,
        //...(more)
}

TS compiler的编译也是参照unicode编码表的。在type.ts中,通过枚举的方式将unicode中的所有编码列举出来,为什么要这么做呢?如果我们直接使用0x5F这样一个16进制数字,我们不能直观的理解这是什么字符。通过枚举的方式,我们可以直接通过类如CharacterCodes.a的方式获取相应的unicode值。

字符判断

scanner中大部分的字符判断都是基于CharacterCodes的。

判断是否是空格

    export function isWhiteSpaceLike(ch: number): boolean {
        return isWhiteSpaceSingleLine(ch) || isLineBreak(ch);
    }

判断是否是换行符

   export function isLineBreak(ch: number): boolean {
        // ES5 7.3:
        // The ECMAScript line terminator characters are listed in Table 3.
        //     Table 3: Line Terminator Characters
        //     Code Unit Value     Name                    Formal Name
        //     \u000A              Line Feed               <LF>
        //     \u000D              Carriage Return         <CR>
        //     \u2028              Line separator          <LS>
        //     \u2029              Paragraph separator     <PS>
        // Only the characters in Table 3 are treated as line terminators. Other new line or line
        // breaking characters are treated as white space but not as line terminators.

        return ch === CharacterCodes.lineFeed ||
            ch === CharacterCodes.carriageReturn ||
            ch === CharacterCodes.lineSeparator ||
            ch === CharacterCodes.paragraphSeparator;
    }

判断是否是数字

    function isDigit(ch: number): boolean {
        return ch >= CharacterCodes._0 && ch <= CharacterCodes._9;
    }

除了这些,sanner中还有不少对于字符判断的函数,这里就不一一列举,有兴趣的同学可以自行查看源码。

标识符(Identifier)判断

标识符的判断比字符判断稍微复杂一些,TS compiler 分别用 isUnicodeIdentifierStartisUnicodeIdentifierPart 两个函数分别判断字符是否可以可以作为标识符开头、字符是否可以作为标识符。

/* @internal */ export function isUnicodeIdentifierStart(code: number, languageVersion: ScriptTarget | undefined) {
    return languageVersion! >= ScriptTarget.ES2015 ?
        lookupInUnicodeMap(code, unicodeESNextIdentifierStart) :
        languageVersion === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) :
            lookupInUnicodeMap(code, unicodeES3IdentifierStart);
}

function isUnicodeIdentifierPart(code: number, languageVersion: ScriptTarget | undefined) {
    return languageVersion! >= ScriptTarget.ES2015 ?
        lookupInUnicodeMap(code, unicodeESNextIdentifierPart) :
        languageVersion === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) :
            lookupInUnicodeMap(code, unicodeES3IdentifierPart);
}

可以作为标识符的字符并没有比较通用的规律,ES规范中也是一个个手动指定的,判断是否字符是否能做标识符的基本实现是:记录允许做标识符的字符,然后查表。

而不难发现,其实除了一些特定的符号,大部分可以作为标识符的字符在unicode编码表中都是连续的,如A-Z,如下图所示

const unicodeESNextIdentifierStart = [65, 90, 97, 122, 170, 170/*...(more)*/ ]
const unicodeESNextIdentifierPart = [48, 57, 65, 90, 95/*...(more)*/ ]

scanner中用数组的形式记录下可以作为标识符的合法字符段的位置,如65是A在unicode编码表的位置,90是 Z在unicode编码表的位置,数组的奇数位记录合法字符段的开始位置,偶数位记录合法字符段的结束位置。只记录每段的开头和结尾部分,比记录该段的所有字符更节约内存。

当需要查找一个字符是否符合标识符规范时,采用折半查找法查找即可。

function lookupInUnicodeMap(code: number, map: readonly number[]): boolean {
    // Bail out quickly if it couldn't possibly be in the map.
    if (code < map[0]) {
        return false;
    }

    // Perform binary search in one of the Unicode range maps
    let lo = 0;
    let hi: number = map.length;
    let mid: number;

    while (lo + 1 < hi) {
        mid = lo + (hi - lo) / 2;
        // mid has to be even to catch a range's beginning
        mid -= mid % 2;
        if (map[mid] <= code && code <= map[mid + 1]) {
            return true;
        }

        if (code < map[mid]) {
            hi = mid;
        }
        else {
            lo = mid + 2;
        }
    }

    return false;
}

索引

一般来说,要记录一个字符的位置有两种方式,一是记录一个字符的索引,二是记录一个字符的行列信息。若存储索引信息,只需要一个字段便可,输出时需要计算索引之前有多少个换行符,从而得到行列信息;若存储行列信息,则代表需要用两个字段存储信息,二者各有优劣。

TS compiler采用存储索引信息的方式,并做了一定的优化:记录每一行第一个字符的索引,使索引转换为行列信息时更高效。

计算每一行第一个字符的索引,建立索引表

export function computeLineStarts(text: string): number[] {
    const result: number[] = new Array();
    let pos = 0;
    let lineStart = 0;
    while (pos < text.length) {
        const ch = text.charCodeAt(pos);
        pos++;
        switch (ch) {
            case CharacterCodes.carriageReturn:
                if (text.charCodeAt(pos) === CharacterCodes.lineFeed) {
                    pos++;
                }
            // falls through
            case CharacterCodes.lineFeed:
                result.push(lineStart);
                lineStart = pos;
                break;
            default:
                if (ch > CharacterCodes.maxAsciiCharacter && isLineBreak(ch)) {
                    result.push(lineStart);
                    lineStart = pos;
                }
                break;
        }
    }
    result.push(lineStart);
    return result;
}

通过索引表查询行列号

export function computePositionOfLineAndCharacter(lineStarts: readonly number[], line: number, character: number, debugText?: string, allowEdits?: true): number {
    if (line < 0 || line >= lineStarts.length) {
        if (allowEdits) {
            // Clamp line to nearest allowable value
            line = line < 0 ? 0 : line >= lineStarts.length ? lineStarts.length - 1 : line;
        }
        else {
            Debug.fail(`Bad line number. Line: ${line}, lineStarts.length: ${lineStarts.length} , line map is correct? ${debugText !== undefined ? arraysEqual(lineStarts, computeLineStarts(debugText)) : "unknown"}`);
        }
    }

    const res = lineStarts[line] + character;
    if (allowEdits) {
        // Clamp to nearest allowable values to allow the underlying to be edited without crashing (accuracy is lost, instead)
        // TODO: Somehow track edits between file as it was during the creation of sourcemap we have and the current file and
        // apply them to the computed position to improve accuracy
        return res > lineStarts[line + 1] ? lineStarts[line + 1] : typeof debugText === "string" && res > debugText.length ? debugText.length : res;
    }
    if (line < lineStarts.length - 1) {
        Debug.assert(res < lineStarts[line + 1]);
    }
    else if (debugText !== undefined) {
        Debug.assert(res <= debugText.length); // Allow single character overflow for trailing newline
    }
    return res;
}

主体流程

一份源代码有可能会解析出成千上万的token,如果将全部token都保存的话会占用大量的内存。就像大家看书的方式一样,大家只需要专注于眼前的一行文字就可以,而无需读入整篇文章再理解其中的意思。

TS compiler采用的扫描方式是逐个扫描。scanner设置了一个全局变量来存储token,每调用一次扫描函数(scan()),变量的值便会更新为下一个token的信息,你可以从变量中获取当前token的信息,然后再调用一次scan(),逐个获取token的信息。

以上为scanner工作主流程的函数调用关系,其中的核心函数是scan函数

function scan(): SyntaxKind {
    startPos = pos; // 记录扫描之前的位置
    while (true) {
        // 这是一个大循环
        // 如果发现空格、注释,会重新循环(此时重新设置 tokenPos,即让 tokenPos 忽略了空格)
        // 如果发现一个标记,则退出函数
        tokenPos = pos;
        // 到字符串末尾,返回结束的token
        if (pos >= end) {
            return token = SyntaxKind.EndOfFileToken;
        }
        // 获取当前字符的编码
        let ch = codePointAt(text, pos);

        switch (ch) {
            // 接下来就开始判断不同的字符可能并组装token
            case CharacterCodes.exclamation: 
                if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { // 后面是不是“=”
                    if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { // 后面是不是还是“=”
                        return pos += 3, token = SyntaxKind.ExclamationEqualsEqualsToken; // 获得“!==”token
                    }
                    return pos += 2, token = SyntaxKind.ExclamationEqualsToken; // 获得“!=”token
                }
                pos++;
                return token = SyntaxKind.ExclamationToken; //获得“!”token
            case CharacterCodes.doubleQuote:
            case CharacterCodes.singleQuote:
                // ...(略)
        }
    }
}

scan函数400多行代码,其实做的工作逻辑也比较简单:扫描字符串,判断不同的字符串组装不同的token。

parser解析器

源代码本质上只是一些文本,若想要进行类型检查或者转换成JS代码,将源代码转换成一种有组织性的数据结构是必不可少的,AST是比较好的选择,既能在节点中存储需要的信息,也可以很好的表示各个节点的从属关系。

主体流程

paser解析器的功能主要是通过源码生成不同的node节点,并且组成一棵抽象语法树。

其中的核心函数是parseSourceFileWorker

function parseSourceFileWorker(languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile {
    const isDeclarationFile = isDeclarationFileName(fileName);
    if (isDeclarationFile) {
        contextFlags |= NodeFlags.Ambient;
    }

    sourceFlags = contextFlags;

    // 扫描
    nextToken();
    //解析token,生成node
    const statements = parseList(ParsingContext.SourceElements, parseStatement);
    Debug.assert(token() === SyntaxKind.EndOfFileToken);
    const endOfFileToken = addJSDocComment(parseTokenNode<EndOfFileToken>());

    //创建AST
    const sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile, statements, endOfFileToken, sourceFlags);

    // ...(more)

    return sourceFile;

    function reportPragmaDiagnostic(pos: number, end: number, diagnostic: DiagnosticMessage) {
        parseDiagnostics.push(createDetachedDiagnostic(fileName, pos, end, diagnostic));
    }
}

大致流程是这样的:

  • nextToken()函数执行了一次扫描,新的token取代旧的token进行解析。

  • parseList函数进行解析,可以看到parseList的第二个参数传了parseStatement这个函数,这个函数其实是真正的核心执行函数:根据token的类别创建node节点。

node节点创建

让我先来看看一个node节点中都包含着什么信息,一个node节点包含的基础信息可以在TS compiler的types.ts中找到相关的定义。可以看到,包含着pos(在源代码中的开始位置)、end(在源代码中的结束位置)、kind(节点类型,定义于SyntaxKind中)等基础信息

export interface ReadonlyTextRange {
    readonly pos: number;
    readonly end: number;
}

export interface Node extends ReadonlyTextRange {
    readonly kind: SyntaxKind;
    readonly flags: NodeFlags;
    /* @internal */ modifierFlagsCache: ModifierFlags;
    /* @internal */ readonly transformFlags: TransformFlags; // Flags for transforms
    readonly decorators?: NodeArray<Decorator>;           // Array of decorators (in document order)
    readonly modifiers?: ModifiersArray;                  // Array of modifiers
    /* @internal */ id?: NodeId;                          // Unique id (used to look up NodeLinks)
    readonly parent: Node;                                // Parent node (initialized by binding)
    /* @internal */ original?: Node;                      // The original node if this is an updated node.
    /* @internal */ symbol: Symbol;                       // Symbol declared by node (initialized by binding)
    /* @internal */ locals?: SymbolTable;                 // Locals associated with node (initialized by binding)
    /* @internal */ nextContainer?: Node;                 // Next container in declaration order (initialized by binding)
    /* @internal */ localSymbol?: Symbol;                 // Local symbol declared by node (initialized by binding only for exported nodes)
    /* @internal */ flowNode?: FlowNode;                  // Associated FlowNode (initialized by binding)
    /* @internal */ emitNode?: EmitNode;                  // Associated EmitNode (initialized by transforms)
    /* @internal */ contextualType?: Type;                // Used to temporarily assign a contextual type during overload resolution
    /* @internal */ inferenceContext?: InferenceContext;  // Inference context for contextual type
}

我们拿parseVariableStatement举例看看node节点的创建

function parseVariableStatement(pos: number, hasJSDoc: boolean, decorators: NodeArray<Decorator> | undefined, modifiers: NodeArray<Modifier> | undefined): VariableStatement {
   //生成节点描述信息 
    const declarationList = parseVariableDeclarationList(/*inForStatementInitializer*/ false);
    //解析分号
    parseSemicolon();
    //创建节点
    const node = factory.createVariableStatement(modifiers, declarationList);
    // Decorators are not allowed on a variable statement, so we keep track of them to report them in the grammar checker.
    node.decorators = decorators;
    //添加节点边界信息
    return withJSDoc(finishNode(node, pos), hasJSDoc);
}

parseVariableDeclarationList生成节点的一些描述信息,如kind、parent等等,将描述信息作为参数传入createVariableStatement中,生成node节点,最后再调用finishNode函数,为node添加range信息(pos、end)

function createVariableStatement(modifiers: readonly Modifier[] | undefined, declarationList: VariableDeclarationList | readonly VariableDeclaration[]) {
    const node = createBaseDeclaration<VariableStatement>(SyntaxKind.VariableStatement, /*decorators*/ undefined, modifiers);
    node.declarationList = isArray(declarationList) ? createVariableDeclarationList(declarationList) : declarationList;
    node.transformFlags |=
        propagateChildFlags(node.declarationList);
    if (modifiersToFlags(node.modifiers) & ModifierFlags.Ambient) {
        node.transformFlags = TransformFlags.ContainsTypeScript;
    }
    return node;
}

binder绑定器

binder的主要工作是创建符号(symbol变量,与ES6的symbol没有关系),并且把符号与AST上的节点关联起来。

符号(symbol)

当我们初次定义一个变量、函数或类时,binder会为其创建一个符号(其实符号就是一个标识符的唯一标识),binder会先将所有的符号收集起来,建立符号表。当在其他地方使用一个名称(例如变量)时,就查表找出这个名称所代表的符号。

binder调用了如下的Symbol函数,初始化了一个符号的信息,其中SymbolFlags符号标志是个标志枚举,用于识别符号类别(例如:变量作用域标志 FunctionScopedVariableBlockScopedVariable 等)。

function Symbol(this: Symbol, flags: SymbolFlags, name: __String) {
    this.flags = flags;
    this.escapedName = name;
    this.declarations = undefined;
    this.valueDeclaration = undefined;
    this.id = undefined;
    this.mergeId = undefined;
    this.parent = undefined;
}

主体流程

核心函数:

  • bindWorker:根据节点的kind分发不同的bindXXX函数

  • createSymbol:创建符号

  • addDeclarationToSymbol:为node节点添加声明

function bind(node: Node | undefined): void {
    if (!node) {
        return;
    }
    //设置父节点
    setParent(node, parent);
    const saveInStrictMode = inStrictMode;
    bindWorker(node);
    if (node.kind > SyntaxKind.LastToken) {
        const saveParent = parent;
        parent = node;
        const containerFlags = getContainerFlags(node);
        if (containerFlags === ContainerFlags.None) {
            //对子节点进行绑定
            bindChildren(node);
        }
        // ...(more)
}

bind函数先设置当前节点的父节点信息,紧接着执行bindWorker,根据不同的节点调用与之对应的绑定函数 最后调用bindChildren, 对当前节点的每个子节点进行一一绑定, bindChildren内部也是通过递归调用bind 对每一个节点进行绑定。

在不同的bindXXX函数中,其中的核心函数是declareSymbol(declareModuleMember函数内部其实也是调用了declareSymbol方法)

function declareSymbol(symbolTable: SymbolTable, parent: Symbol | undefined, node: Declaration, includes: SymbolFlags, excludes: SymbolFlags, isReplaceableByMethod?: boolean, isComputedName?: boolean): Symbol {
    Debug.assert(isComputedName || !hasDynamicName(node));

    const isDefaultExport = hasSyntacticModifier(node, ModifierFlags.Default) || isExportSpecifier(node) && node.name.escapedText === "default";

    const name = isComputedName ? InternalSymbolName.Computed
        : isDefaultExport && parent ? InternalSymbolName.Default
        : getDeclarationName(node);

    let symbol: Symbol | undefined;
    if (name === undefined) {
        symbol = createSymbol(SymbolFlags.None, InternalSymbolName.Missing);
    }
    else {

        symbol = symbolTable.get(name);

        if (includes & SymbolFlags.Classifiable) {
            classifiableNames.add(name);
        }

        if (!symbol) {
            symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name));
            if (isReplaceableByMethod) symbol.isReplaceableByMethod = true;
        }
        //...(more)
    }

    addDeclarationToSymbol(symbol, node, includes);
   //...(more)

    return symbol;
}

需要说明的是binder中会维护符号表,在declareSymbol函数中,会判断符号表中是否有同名的符号,若没有的话,创建新的符号并加入符号表中;有的话就会直接从符号表中取出符号信息。接下来是调用addDeclarationToSymbol函数,这个函数主要进行两个工作:1.创建 AST 节点到 symbol 的连接 ( node.symbol = symbol;) 2. 为符号添加一个关于节点的声明(symbol.declarations = appendIfUnique(symbol.declarations, ``node``))

夹带私货:TypeScript AST Viewer

这里夹带一下私货,给大家推荐一个非常好用的网站:TypeScript ASTViewer (ts-ast-viewer.com)[1]

可以看到,只要在左上方的编辑区域写上ts代码,中间一栏就会显示生成的AST,点击中间AST上的具体节点,左下角就会显示创建此节点的函数调用,右侧栏会显示这个节点的一些信息(包括node相关信息和Symbol的信息)

checker检查器

Checker部分的代码超过了4w行,是整个compiler中最重的部分,我也无法细细的读每一个细节,在此就对其流程做一个分析。

如何检查

例如const b:number = 1;这一行代码,在AST中的结构如下:

我们来看看变量b对应的Identifier节点:

在变量b所绑定的Symbol符号中,declarations中保存着b的声明信息,其中有type属性,其中的kind存着b绑定的类型的类别,checker会去检查这个kind和NumericLiteral是否匹配,若不匹配,则调用本地的error函数生成错误报告。

主体流程

开始类型检查的入口函数为getDiagnostics,而其中最核心的函数是深层的checkSourceFileWorker

function checkSourceFileWorker(node: SourceFile) {
    const links = getNodeLinks(node);
    if (!(links.flags & NodeCheckFlags.TypeChecked)) {
        if (skipTypeChecking(node, compilerOptions, host)) {
            return;
        }

        // 语法检查
        checkGrammarSourceFile(node);

        clear(potentialThisCollisions);
        clear(potentialNewTargetCollisions);
        clear(potentialWeakMapSetCollisions);
        clear(potentialReflectCollisions);
        //类型检查
        forEach(node.statements, checkSourceElement);
        checkSourceElement(node.endOfFileToken);

        checkDeferredNodes(node);

        if (isExternalOrCommonJsModule(node)) {
            registerForUnusedIdentifiersCheck(node);
        }

        if (!node.isDeclarationFile && (compilerOptions.noUnusedLocals || compilerOptions.noUnusedParameters)) {
            checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(node), (containingNode, kind, diag) => {
                if (!containsParseError(containingNode) && unusedIsError(kind, !!(containingNode.flags & NodeFlags.Ambient))) {
                    diagnostics.add(diag);
                }
            });
        }

    // ...(more)
}

我们发现在checkSourceFileWorker函数内有各种各样的check操作,先执行了checkGrammarSourceFile进行语法检查,后面执行checkSourceElementcheckDeferredNodes等才对具体的节点进行具体的语义检查。其中其实也是做了一个分发,根据node.kind来判断节点的类别,执行不同类型节点的检查函数。

function checkSourceElementWorker(node: Node): void {
    if (isInJSFile(node)) {
        forEach((node as JSDocContainer).jsDoc, ({ tags }) => forEach(tags, checkSourceElement));
    }

    const kind = node.kind;
      // ...(more)
    if (kind >= SyntaxKind.FirstStatement && kind <= SyntaxKind.LastStatement && node.flowNode && !isReachableFlowNode(node.flowNode)) {
        errorOrSuggestion(compilerOptions.allowUnreachableCode === false, node, Diagnostics.Unreachable_code_detected);
    }
    //根据node类型执行不同的检查函数
    switch (kind) {
        case SyntaxKind.TypeParameter:
            return checkTypeParameter(node as TypeParameterDeclaration);
        case SyntaxKind.Parameter:
            return checkParameter(node as ParameterDeclaration);
        case SyntaxKind.PropertyDeclaration:
            return checkPropertyDeclaration(node as PropertyDeclaration);
        case SyntaxKind.PropertySignature:
            return checkPropertySignature(node as PropertySignature);
       //...(more)
    }
 }

检查之后,通过error函数报告错误

function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic {
    //生成单条错误
    const diagnostic = createError(location, message, arg0, arg1, arg2, arg3);
    //加入错误报告中
    diagnostics.add(diagnostic);
    return diagnostic;
}
export function createFileDiagnostic(file: SourceFile, start: number, length: number, message: DiagnosticMessage): DiagnosticWithLocation {
    assertDiagnosticLocation(file, start, length);

        let text = getLocaleSpecificMessage(message);

        if (arguments.length > 4) {
        text = formatStringFromArgs(text, arguments, 4);
    }
    return {
        file,
        start,
        length,

        messageText: text,
        category: message.category,
        code: message.code,
        reportsUnnecessary: message.reportsUnnecessary,
        reportsDeprecated: message.reportsDeprecated
    };
}

emitter发射器

发射器所做的事情主要是通过AST输出对应的JS代码以及声明文件(d.ts)

主体流程

Emitter的主要流程核心函数为emitFiles

export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile | undefined, { scriptTransformers, declarationTransformers }: EmitTransformers, emitOnlyDtsFiles?: boolean, onlyBuildInfo?: boolean, forceDtsEmit?: boolean): EmitResult {
    const compilerOptions = host.getCompilerOptions();
    const sourceMapDataList: SourceMapEmitResult[] | undefined = (compilerOptions.sourceMap || compilerOptions.inlineSourceMap || getAreDeclarationMapsEnabled(compilerOptions)) ? [] : undefined;
    const emittedFilesList: string[] | undefined = compilerOptions.listEmittedFiles ? [] : undefined;
    const emitterDiagnostics = createDiagnosticCollection();
    const newLine = getNewLineCharacter(compilerOptions, () => host.getNewLine());
    const writer = createTextWriter(newLine);
    const { enter, exit } = performance.createTimer("printTime", "beforePrint", "afterPrint");
    let bundleBuildInfo: BundleBuildInfo | undefined;
    let emitSkipped = false;
    let exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined;

    // Emit each output file
    enter();
    forEachEmittedFile(
        host,
        emitSourceFileOrBundle,
        getSourceFilesToEmit(host, targetSourceFile, forceDtsEmit),
        forceDtsEmit,
        onlyBuildInfo,
        !targetSourceFile
    );
    exit();


    return {
        emitSkipped,
        diagnostics: emitterDiagnostics.getDiagnostics(),
        emittedFiles: emittedFilesList,
        sourceMaps: sourceMapDataList,
        exportedModulesFromDeclarationEmit
    };

可以看到这里创建了三个变量sourceMapDataList、emittedFilesList、emitterDiagnostics,这三个分别对应了需要输出的三种文件数据:sourceMap、JS代码和声明文件、类型检查的错误报告

function emitJsFileOrBundle(
    sourceFileOrBundle: SourceFile | Bundle | undefined,
    jsFilePath: string | undefined,
    sourceMapFilePath: string | undefined,
    relativeToBuildInfo: (path: string) => string) {
   // ...(more)
    // 将TS语法转换成js语法
    const transform = transformNodes(resolver, host, factory, compilerOptions, [sourceFileOrBundle], scriptTransformers, /*allowDtsFiles*/ false);

    // ...(more)

    // 创建一个printer
    const printer = createPrinter(printerOptions, {
        // resolver hooks
        hasGlobalName: resolver.hasGlobalName,

        // transform hooks
        onEmitNode: transform.emitNodeWithNotification,
        isEmitNotificationEnabled: transform.isEmitNotificationEnabled,
        substituteNode: transform.substituteNode,
    });

    Debug.assert(transform.transformed.length === 1, "Should only see one output from the transform");
    // 输出JS文件
    printSourceFileOrBundle(jsFilePath, sourceMapFilePath, transform.transformed[0], printer, compilerOptions);

    // ...(more)
}

再往深处找,发现了emitJsFileOrBundle与emitDeclarationFileOrBundle两个函数,一个用于输出JS代码,一个用于输出声明文件(d.ts)。我们看看emitJsFileOrBundle中的逻辑,可以看到做了两件事情:1.对每一个节点做了一次transform的操作,将TS语法转化为JS语法 。2.创建一个printer,调用printSourceFileOrBundle输出js文件。

总结

TypeScript Compiler的代码量其实非常大,内部的细节逻辑也挺复杂的,我分享的仅仅是其中的冰山一角,这都是因为TypeScript设计了一套比较完善的类型系统。这里给大家推荐篇论文,是一篇关于类型系统的入门介绍,如果对类型系统相关设计有兴趣的同学可以看一下: 类型系统[2]

最后是一点建议吧,建议初读TypeScript Compiler这部分源码的同学可以注重于摸清整个编译的流程,之后再慢慢研究其中的设计细节,否则巨大的代码量随时让人想弃坑

参考资料

[1]TypeScript-AST-Viewer (ts-ast-viewer.com): https://ts-ast-viewer.com

[2]类型系统: http://lucacardelli.name/papers/typesystems.pdf

❤️ 谢谢支持


以上便是本次分享的全部内容,希望对你有所帮助^_^

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/JsEIT8an235aOROgP1YDeA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237228次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录