Open14

microsoft/typescript のコードリーディング Day2 ~ TypeChecker

mizchimizchi

続き

https://zenn.dev/mizchi/scraps/3c30ea6fa9f8e5

あらすじ

主に src/compiler/program.ts

  • ts.sys で環境ごとの IO を抽象(主に node用のものがセットアップされる)
  • CompilerHost は ts.sys をもらってコンパイラとして使う環境を抽象
  • Program は CompilerHost を使って エントリポイントの rootNames から再帰的に SourceFile を組み立てていく
    -findSourceFileWorker(...) が名前から sourceFile を探し、なければ host から readFile して生成
    • collectModuleReferences() で再帰的に findSourceFileWorker() を呼び、解析した依存は sourceFile の内部プロパティに書き込まれる。これは他のモジュールから参照され、compiler api を使うだけでは型的には見えない。
    • シンボル解決は node_modules や amibent module など、 src/compiler/moduleNameResolver.ts によって解決され、これも file の imports や resolvedModule に書き込まれる。

余談: あまりにもvscodeが重かったので git clone --depth 1 し直したら治った

mizchimizchi

今回は program.getSemanticDiagnostics で得られる型の検査結果をどう生成しているか追っていく。

このコードから考える。

const tsconfig = ts.readConfigFile("./tsconfig.json", ts.sys.readFile);
const options = ts.parseJsonConfigFileContent(
  tsconfig.config,
  ts.sys,
  "./",
);
const program = ts.createProgram(options.fileNames, options.options);
const diagnostics = program.getSemanticDiagnostics();

この中に 例えば const x: number = "" したときの型違反情報が入っている。

予想では typeChecker を使ってそれを何らかの方法で払い出してる。

getSemanticDiagnostics の初期化

src/compiler/program.ts で getSemanticDiagnostics の初期化を探す

src/compiler/program.ts
    function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] {
        return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken);
    }

// ...

    function getDiagnosticsHelper<T extends Diagnostic>(
        sourceFile: SourceFile | undefined,
        getDiagnostics: (sourceFile: SourceFile, cancellationToken: CancellationToken | undefined) => readonly T[],
        cancellationToken: CancellationToken | undefined): readonly T[] {
        if (sourceFile) {
            return sortAndDeduplicateDiagnostics(getDiagnostics(sourceFile, cancellationToken));
        }
        return sortAndDeduplicateDiagnostics(flatMap(program.getSourceFiles(), sourceFile => {
            if (cancellationToken) {
                cancellationToken.throwIfCancellationRequested();
            }
            return getDiagnostics(sourceFile, cancellationToken);
        }));
    }

sortAndDeduplicateDiagnostics を見るに、 その sourceFile にまつわるものだけを取得できるようになってるっぽい。第二引数の getDiagnostics つまり getSyntacticDiagnosticsForFile もしくは getSemanticDiagnosticsForFileが本体か。

最初に Syntax のほうを見てみる。

    function getSyntacticDiagnosticsForFile(sourceFile: SourceFile): readonly DiagnosticWithLocation[] {
        // For JavaScript files, we report semantic errors for using TypeScript-only
        // constructs from within a JavaScript file as syntactic errors.
        if (isSourceFileJS(sourceFile)) {
            if (!sourceFile.additionalSyntacticDiagnostics) {
                sourceFile.additionalSyntacticDiagnostics = getJSSyntacticDiagnosticsForFile(sourceFile);
            }
            return concatenate(sourceFile.additionalSyntacticDiagnostics, sourceFile.parseDiagnostics);
        }
        return sourceFile.parseDiagnostics;
    }

あれ、sourceFile.parseDiagnostics なんていつ生えたんだ。

型はこう

    /** @internal */ parseDiagnostics: DiagnosticWithLocation[];

findReferences で初期化してる箇所を探す。

多すぎ!

しゃーないので parseDiagnostics = で検索。parser で初期化している?

勘違いしていた。これは関数ではなく parse 時に発生する parser の diagnostics だ。

呼び出し元の getSyntacticDiagnosticsForFile の名前をみるに、おそらく構文エラー関連で、まだ型チェッカーはうごきだしてない。

じゃあ次に getSemantic~ をみる

    function getSemanticDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] {
        return concatenate(
            filterSemanticDiagnostics(getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken), options),
            getProgramDiagnostics(sourceFile)
        );
    }

getBindAndCheckDiagnosticsForFile と getProgramDiagnostics を使っている。filter は飛ばしていいか。

    function getBindAndCheckDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] {
        return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedBindAndCheckDiagnosticsForFile, getBindAndCheckDiagnosticsForFileNoCache);
    }

キャッシュ部分は飛ばして生成部分であろう getBindAndCheckDiagnosticsForFileNoCacheを見たほうがよさそう。なんか delegate できそうな実装になってる割には一意な実装。

(さっきから cancelalationToken を読み飛ばしてるが、これは Ctrl-C や何らかのIDEの事情で中断する時の処理だろうと思っている。その割にはシングルスレッド実装だが...)

    function getBindAndCheckDiagnosticsForFileNoCache(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] {
        return runWithCancellationToken(() => {
            if (skipTypeChecking(sourceFile, options, program)) {
                return emptyArray;
            }

            const typeChecker = getTypeChecker();

            Debug.assert(!!sourceFile.bindDiagnostics);

            const isJs = sourceFile.scriptKind === ScriptKind.JS || sourceFile.scriptKind === ScriptKind.JSX;
            const isCheckJs = isJs && isCheckJsEnabledForFile(sourceFile, options);
            const isPlainJs = isPlainJsFile(sourceFile, options.checkJs);
            const isTsNoCheck = !!sourceFile.checkJsDirective && sourceFile.checkJsDirective.enabled === false;

            // By default, only type-check .ts, .tsx, Deferred, plain JS, checked JS and External
            // - plain JS: .js files with no // ts-check and checkJs: undefined
            // - check JS: .js files with either // ts-check or checkJs: true
            // - external: files that are added by plugins
            const includeBindAndCheckDiagnostics = !isTsNoCheck && (sourceFile.scriptKind === ScriptKind.TS || sourceFile.scriptKind === ScriptKind.TSX
                    || sourceFile.scriptKind === ScriptKind.External || isPlainJs || isCheckJs || sourceFile.scriptKind === ScriptKind.Deferred);
            let bindDiagnostics: readonly Diagnostic[] = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : emptyArray;
            let checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : emptyArray;
            if (isPlainJs) {
                bindDiagnostics = filter(bindDiagnostics, d => plainJSErrors.has(d.code));
                checkDiagnostics = filter(checkDiagnostics, d => plainJSErrors.has(d.code));
            }
            // skip ts-expect-error errors in plain JS files, and skip JSDoc errors except in checked JS
            return getMergedBindAndCheckDiagnostics(sourceFile, includeBindAndCheckDiagnostics && !isPlainJs, bindDiagnostics, checkDiagnostics, isCheckJs ? sourceFile.jsDocDiagnostics : undefined);
        });
    }

ここでやっと typeChecker が作られる。 typeChecker のコードを読むのは後回ししてこの関数処理を読み切る。

要は typeChecker.getDiagnostics(sourceFile, cancellationToken) だと思われるので、ここで typeChecker を見る必要があるってことになりそう。

ところで、このAPI直接は露出してない。 checker の型をみるにこう。

    /** @internal */ createSymbol(flags: SymbolFlags, name: __String): TransientSymbol;
    /** @internal */ createIndexInfo(keyType: Type, type: Type, isReadonly: boolean, declaration?: SignatureDeclaration): IndexInfo;
    /** @internal */ isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult;
    /** @internal */ tryFindAmbientModule(moduleName: string): Symbol | undefined;
    /** @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol | undefined;

    /** @internal */ getSymbolWalker(accept?: (symbol: Symbol) => boolean): SymbolWalker;

    // Should not be called directly.  Should only be accessed through the Program instance.
    /** @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[];
    /** @internal */ getGlobalDiagnostics(): Diagnostic[];
    /** @internal */ getEmitResolver(sourceFile?: SourceFile, cancellationToken?: CancellationToken): EmitResolver;

    /** @internal */ getNodeCount(): number;
    /** @internal */ getIdentifierCount(): number;
    /** @internal */ getSymbolCount(): number;
    /** @internal */ getTypeCount(): number;
    /** @internal */ getInstantiationCount(): number;
    /** @internal */ getRelationCacheSizes(): { assignable: number, identity: number, subtype: number, strictSubtype: number };
    /** @internal */ getRecursionIdentity(type: Type): object | undefined;
    /** @internal */ getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol>;

compiler api で見えてる checker のAPIは残り滓みたいなもんで、この getSemanticDiagnostics() が本体のように見える。

mizchimizchi

TypeChecker 編

prorgram に対して getTypeChecker を呼ぶと、型解析機が得られる。

    function getTypeChecker() {
        return typeChecker || (typeChecker = createTypeChecker(program));
    }

この createTypeChecker は src/compiler/checker.ts にある。ちなみにこのファイルはTSの型の解析が全部ここにあるので、parser.ts に次にでかい。

とりあえず local 変数を眺めて内部状態をイメージしておく。


/** @internal */
export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
    // Why var? It avoids TDZ checks in the runtime which can be costly.
    // See: https://github.com/microsoft/TypeScript/issues/52924
    /* eslint-disable no-var */
    var deferredDiagnosticsCallbacks: (() => void)[] = [];

    var addLazyDiagnostic = (arg: () => void) => {
        deferredDiagnosticsCallbacks.push(arg);
    };

    // Cancellation that controls whether or not we can cancel in the middle of type checking.
    // In general cancelling is *not* safe for the type checker.  We might be in the middle of
    // computing something, and we will leave our internals in an inconsistent state.  Callers
    // who set the cancellation token should catch if a cancellation exception occurs, and
    // should throw away and create a new TypeChecker.
    //
    // Currently we only support setting the cancellation token when getting diagnostics.  This
    // is because diagnostics can be quite expensive, and we want to allow hosts to bail out if
    // they no longer need the information (for example, if the user started editing again).
    var cancellationToken: CancellationToken | undefined;

    var requestedExternalEmitHelperNames = new Set<string>();
    var requestedExternalEmitHelpers: ExternalEmitHelpers;
    var externalHelpersModule: Symbol;

    var Symbol = objectAllocator.getSymbolConstructor();
    var Type = objectAllocator.getTypeConstructor();
    var Signature = objectAllocator.getSignatureConstructor();

    var typeCount = 0;
    var symbolCount = 0;
    var totalInstantiationCount = 0;
    var instantiationCount = 0;
    var instantiationDepth = 0;
    var inlineLevel = 0;
    var currentNode: Node | undefined;
    var varianceTypeParameter: TypeParameter | undefined;
    var isInferencePartiallyBlocked = false;

    var emptySymbols = createSymbolTable();
    var arrayVariances = [VarianceFlags.Covariant];

    var compilerOptions = host.getCompilerOptions();
    var languageVersion = getEmitScriptTarget(compilerOptions);
    var moduleKind = getEmitModuleKind(compilerOptions);
    var legacyDecorators = !!compilerOptions.experimentalDecorators;
    var useDefineForClassFields = getUseDefineForClassFields(compilerOptions);
    var allowSyntheticDefaultImports = getAllowSyntheticDefaultImports(compilerOptions);
    var strictNullChecks = getStrictOptionValue(compilerOptions, "strictNullChecks");
    var strictFunctionTypes = getStrictOptionValue(compilerOptions, "strictFunctionTypes");
    var strictBindCallApply = getStrictOptionValue(compilerOptions, "strictBindCallApply");
    var strictPropertyInitialization = getStrictOptionValue(compilerOptions, "strictPropertyInitialization");
    var noImplicitAny = getStrictOptionValue(compilerOptions, "noImplicitAny");
    var noImplicitThis = getStrictOptionValue(compilerOptions, "noImplicitThis");
    var useUnknownInCatchVariables = getStrictOptionValue(compilerOptions, "useUnknownInCatchVariables");
    var keyofStringsOnly = !!compilerOptions.keyofStringsOnly;
    var defaultIndexFlags = keyofStringsOnly ? IndexFlags.StringsOnly : IndexFlags.None;
    var freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ObjectFlags.FreshLiteral;
    var exactOptionalPropertyTypes = compilerOptions.exactOptionalPropertyTypes;

    var checkBinaryExpression = createCheckBinaryExpression();
    var emitResolver = createResolver();
    var nodeBuilder = createNodeBuilder();

    var globals = createSymbolTable();
    var undefinedSymbol = createSymbol(SymbolFlags.Property, "undefined" as __String);
    undefinedSymbol.declarations = [];

    var globalThisSymbol = createSymbol(SymbolFlags.Module, "globalThis" as __String, CheckFlags.Readonly);
    globalThisSymbol.exports = globals;
    globalThisSymbol.declarations = [];
    globals.set(globalThisSymbol.escapedName, globalThisSymbol);

    var argumentsSymbol = createSymbol(SymbolFlags.Property, "arguments" as __String);
    var requireSymbol = createSymbol(SymbolFlags.Property, "require" as __String);
    var isolatedModulesLikeFlagName = compilerOptions.verbatimModuleSyntax ? "verbatimModuleSyntax" : "isolatedModules";
    // It is an error to use `importsNotUsedAsValues` alongside `verbatimModuleSyntax`, but we still need to not crash.
    // Given that, in such a scenario, `verbatimModuleSyntax` is basically disabled, as least as far as alias visibility tracking goes.
    var canCollectSymbolAliasAccessabilityData = !compilerOptions.verbatimModuleSyntax || !!compilerOptions.importsNotUsedAsValues;

    /** This will be set during calls to `getResolvedSignature` where services determines an apparent number of arguments greater than what is actually provided. */
    var apparentArgumentCount: number | undefined;

ほとんどは compilerOptions からのフラグ。emitResolver はコード生成する部分だと思われるのででかそうだが型チェッカーを読みたいだけの自分は読まなくていい(本当か?)。

最初に globalThisundefined のシンボルを登録している。これらは予約語ではないが EcmaScript で定義されてるシンボル。

globals はグローバル変数を管理してる部分だと思う。

あとは cherker インスタンスを作って返してるだけだった。つまり、program.getSemanticDiagnostics() から呼ばれる checker.getDiagnostics() を読めばいい。

mizchimizchi

checker.getDiagnostics()

createCheckBinaryExpression してるのは、おそらく expression だけ評価する処理を分離したかったやつかな?まだユースケースがわからないので、呼び出す時に読むことにする。

というわけで gitDiagnostics から

    function getDiagnostics(sourceFile: SourceFile, ct: CancellationToken): Diagnostic[] {
        try {
            // Record the cancellation token so it can be checked later on during checkSourceElement.
            // Do this in a finally block so we can ensure that it gets reset back to nothing after
            // this call is done.
            cancellationToken = ct;
            return getDiagnosticsWorker(sourceFile);
        }
        finally {
            cancellationToken = undefined;
        }
    }

// ...

    function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {
        if (sourceFile) {
            ensurePendingDiagnosticWorkComplete();
            // Some global diagnostics are deferred until they are needed and
            // may not be reported in the first call to getGlobalDiagnostics.
            // We should catch these changes and report them.
            const previousGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
            const previousGlobalDiagnosticsSize = previousGlobalDiagnostics.length;

            checkSourceFileWithEagerDiagnostics(sourceFile);

            const semanticDiagnostics = diagnostics.getDiagnostics(sourceFile.fileName);
            const currentGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
            if (currentGlobalDiagnostics !== previousGlobalDiagnostics) {
                // If the arrays are not the same reference, new diagnostics were added.
                const deferredGlobalDiagnostics = relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, compareDiagnostics);
                return concatenate(deferredGlobalDiagnostics, semanticDiagnostics);
            }
            else if (previousGlobalDiagnosticsSize === 0 && currentGlobalDiagnostics.length > 0) {
                // If the arrays are the same reference, but the length has changed, a single
                // new diagnostic was added as DiagnosticCollection attempts to reuse the
                // same array.
                return concatenate(currentGlobalDiagnostics, semanticDiagnostics);
            }

            return semanticDiagnostics;
        }

        // Global diagnostics are always added when a file is not provided to
        // getDiagnostics
        forEach(host.getSourceFiles(), checkSourceFileWithEagerDiagnostics);
        return diagnostics.getDiagnostics();
    }

strictNullCheck が効いてるなら sourceFile が undefined にはならないはずだが、もしなければ forEach で host の sourceFile を全部舐める。PR出すかと思ったが、なにか特殊な呼び出し経路がありそう。

ensurePendingDiagnosticWorkComplete は cancel された際の処理を再実行かな。

ところで diagnostics とはなんだ

    var diagnostics = createDiagnosticCollection();

///...

/** @internal */
export function createDiagnosticCollection(): DiagnosticCollection {
    let nonFileDiagnostics = [] as Diagnostic[] as SortedArray<Diagnostic>; // See GH#19873
    const filesWithDiagnostics = [] as string[] as SortedArray<string>;
    const fileDiagnostics = new Map<string, SortedArray<DiagnosticWithLocation>>();
    let hasReadNonFileDiagnostics = false;

    return {
        add,
        lookup,
        getGlobalDiagnostics,
        getDiagnostics,
    };

    function lookup(diagnostic: Diagnostic): Diagnostic | undefined {
        let diagnostics: SortedArray<Diagnostic> | undefined;
        if (diagnostic.file) {
            diagnostics = fileDiagnostics.get(diagnostic.file.fileName);
        }
        else {
            diagnostics = nonFileDiagnostics;
        }
        if (!diagnostics) {
            return undefined;
        }
        const result = binarySearch(diagnostics, diagnostic, identity, compareDiagnosticsSkipRelatedInformation);
        if (result >= 0) {
            return diagnostics[result];
        }
        return undefined;
    }

    function add(diagnostic: Diagnostic): void {
        let diagnostics: SortedArray<Diagnostic> | undefined;
        if (diagnostic.file) {
            diagnostics = fileDiagnostics.get(diagnostic.file.fileName);
            if (!diagnostics) {
                diagnostics = [] as Diagnostic[] as SortedArray<DiagnosticWithLocation>; // See GH#19873
                fileDiagnostics.set(diagnostic.file.fileName, diagnostics as SortedArray<DiagnosticWithLocation>);
                insertSorted(filesWithDiagnostics, diagnostic.file.fileName, compareStringsCaseSensitive);
            }
        }
        else {
            // If we've already read the non-file diagnostics, do not modify the existing array.
            if (hasReadNonFileDiagnostics) {
                hasReadNonFileDiagnostics = false;
                nonFileDiagnostics = nonFileDiagnostics.slice() as SortedArray<Diagnostic>;
            }

            diagnostics = nonFileDiagnostics;
        }

        insertSorted(diagnostics, diagnostic, compareDiagnosticsSkipRelatedInformation);
    }

    function getGlobalDiagnostics(): Diagnostic[] {
        hasReadNonFileDiagnostics = true;
        return nonFileDiagnostics;
    }

    function getDiagnostics(fileName: string): DiagnosticWithLocation[];
    function getDiagnostics(): Diagnostic[];
    function getDiagnostics(fileName?: string): Diagnostic[] {
        if (fileName) {
            return fileDiagnostics.get(fileName) || [];
        }

        const fileDiags: Diagnostic[] = flatMapToMutable(filesWithDiagnostics, f => fileDiagnostics.get(f));
        if (!nonFileDiagnostics.length) {
            return fileDiags;
        }
        fileDiags.unshift(...nonFileDiagnostics);
        return fileDiags;
    }
}

/** @internal */
export interface DiagnosticCollection {
    // Adds a diagnostic to this diagnostic collection.
    add(diagnostic: Diagnostic): void;

    // Returns the first existing diagnostic that is equivalent to the given one (sans related information)
    lookup(diagnostic: Diagnostic): Diagnostic | undefined;

    // Gets all the diagnostics that aren't associated with a file.
    getGlobalDiagnostics(): Diagnostic[];

    // If fileName is provided, gets all the diagnostics associated with that file name.
    // Otherwise, returns all the diagnostics (global and file associated) in this collection.
    getDiagnostics(): Diagnostic[];
    getDiagnostics(fileName: string): DiagnosticWithLocation[];
}

diagnostic を登録したり、 その diagnostic がどのファイルに対するものか loopup したりしてるっぽい。

getDiagnosticsWorker に戻る。この辺

            // Some global diagnostics are deferred until they are needed and
            // may not be reported in the first call to getGlobalDiagnostics.
            // We should catch these changes and report them.
            const previousGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
            const previousGlobalDiagnosticsSize = previousGlobalDiagnostics.length;

            checkSourceFileWithEagerDiagnostics(sourceFile);

            const semanticDiagnostics = diagnostics.getDiagnostics(sourceFile.fileName);
            const currentGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
            if (currentGlobalDiagnostics !== previousGlobalDiagnostics) {
                // If the arrays are not the same reference, new diagnostics were added.
                const deferredGlobalDiagnostics = relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, compareDiagnostics);
                return concatenate(deferredGlobalDiagnostics, semanticDiagnostics);
            }
            else if (previousGlobalDiagnosticsSize === 0 && currentGlobalDiagnostics.length > 0) {
                // If the arrays are the same reference, but the length has changed, a single
                // new diagnostic was added as DiagnosticCollection attempts to reuse the
                // same array.
                return concatenate(currentGlobalDiagnostics, semanticDiagnostics);
            }

その後、実行前後の diagnostics を比較している。つまり checkSourceFileWithEagerDiagnostics(file) で実際に検査して、ローカルスコープに書き込まれた副作用をみてそう。

ところで、ここまで diagnostics と書いてきたのでめちゃくちゃタイポしそうな長い単語なので、単に日本語で診断と書くことにする。

mizchimizchi

getDiagnosticsWorker → checkSourceFileWithEagerDiagnostics

    function checkSourceFileWithEagerDiagnostics(sourceFile: SourceFile) {
        ensurePendingDiagnosticWorkComplete();
        // then setup diagnostics for immediate invocation (as we are about to collect them, and
        // this avoids the overhead of longer-lived callbacks we don't need to allocate)
        // This also serves to make the shift to possibly lazy diagnostics transparent to serial command-line scenarios
        // (as in those cases, all the diagnostics will still be computed as the appropriate place in the tree,
        // thus much more likely retaining the same union ordering as before we had lazy diagnostics)
        const oldAddLazyDiagnostics = addLazyDiagnostic;
        addLazyDiagnostic = cb => cb();
        checkSourceFile(sourceFile);
        addLazyDiagnostic = oldAddLazyDiagnostics;
    }

なるほど、おそらく定義順に型を解決するわけじゃないから、同期の遅延コールバックみたいになってるのか。

じゃあ checkSoureFile が実体か。というか命名規則的に check[AST-Kind] みたいに再帰下降になってると予想。

    function checkSourceFile(node: SourceFile) {
        tracing?.push(tracing.Phase.Check, "checkSourceFile", { path: node.path }, /*separateBeginAndEnd*/ true);
        performance.mark("beforeCheck");
        checkSourceFileWorker(node);
        performance.mark("afterCheck");
        performance.measure("Check", "beforeCheck", "afterCheck");
        tracing?.pop();
    }

何度も出てるパターンで今更だが、 関数名~関数名Worker というパターンはだいたい重い処理で、trace と performance.mark が付いてる。処理としては Worker が実体になる。

中身をみる。どうせ長いので腰を据える

    // Fully type check a source file and collect the relevant diagnostics.
    function checkSourceFileWorker(node: SourceFile) {
        const links = getNodeLinks(node);
        if (!(links.flags & NodeCheckFlags.TypeChecked)) {
            if (skipTypeChecking(node, compilerOptions, host)) {
                return;
            }

            // Grammar checking
            checkGrammarSourceFile(node);

            clear(potentialThisCollisions);
            clear(potentialNewTargetCollisions);
            clear(potentialWeakMapSetCollisions);
            clear(potentialReflectCollisions);
            clear(potentialUnusedRenamedBindingElementsInTypes);

            forEach(node.statements, checkSourceElement);
            checkSourceElement(node.endOfFileToken);

            checkDeferredNodes(node);

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

            addLazyDiagnostic(() => {
                // This relies on the results of other lazy diagnostics, so must be computed after them
                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);
                        }
                    });
                }
                if (!node.isDeclarationFile) {
                    checkPotentialUncheckedRenamedBindingElementsInTypes();
                }
            });

            if (compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error &&
                !node.isDeclarationFile &&
                isExternalModule(node)
            ) {
                checkImportsForTypeOnlyConversion(node);
            }

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

            if (potentialThisCollisions.length) {
                forEach(potentialThisCollisions, checkIfThisIsCapturedInEnclosingScope);
                clear(potentialThisCollisions);
            }

            if (potentialNewTargetCollisions.length) {
                forEach(potentialNewTargetCollisions, checkIfNewTargetIsCapturedInEnclosingScope);
                clear(potentialNewTargetCollisions);
            }

            if (potentialWeakMapSetCollisions.length) {
                forEach(potentialWeakMapSetCollisions, checkWeakMapSetCollision);
                clear(potentialWeakMapSetCollisions);
            }

            if (potentialReflectCollisions.length) {
                forEach(potentialReflectCollisions, checkReflectCollision);
                clear(potentialReflectCollisions);
            }

            links.flags |= NodeCheckFlags.TypeChecked;
        }
    }

大雑把に

  • linkFlags をみて bit mask で未チェックかどうかを確認
  • 構文チェック
  • 各種ステートの初期化
  • statement 単位で上から checkSourceElement
  • EoF トークンを check している。意味はあとで調べる。
  • checkDeferredNodes(node) でさっきの遅延されたタスクを登録している?
  • 自身が commonjs module なら未使用未チェックの identifier として登録
  • 遅延タスクの登録。 getPotentiallyUnusedIdentifiers(node) で未使用候補を列挙して、構文エラー以外なら diagnostics に登録して、 そこから checkUnusedIdentifiers で check
  • dts でなければ checkPotentialUncheckedRenamedBindingElementsInTypes();
  • tsconfig の importsNotUsedAsValue が有効ならば checkImportsForTypeOnlyConversion(node)
  • 自身が external か commonjs なら checkExternalModuleExports
  • this が衝突していそうなら checkIfThisIsCapturedInEnclosingScope()
  • new Target が衝突していそうなら checkIfNewTargetIsCapturedInEnclosingScope()
  • WeakMapSet が衝突していそうなら checkWeakMapSetCollision()
  • リフレクションが衝突していそうなら checkReflectCollision
  • 終わったらビット演算で NodeCheckFlags.TypeChecked のフラグを立てる

まず、 getNodeLinks(node) でどういうビットマスクが生成されてるかをみるべきか。

    function getNodeLinks(node: Node): NodeLinks {
        const nodeId = getNodeId(node);
        return nodeLinks[nodeId] || (nodeLinks[nodeId] = new (NodeLinks as any)());
    }

// ...

/** @internal */
export function getNodeId(node: Node): number {
    if (!node.id) {
        node.id = nextNodeId;
        nextNodeId++;
    }
    return node.id;
}

正直あんまり意図がわからないが、確認すべき nodeLinks がどこかに列挙されてて、自身のフラグが入ってる? sourceFile を先頭に上から順にノードを舐めて、 全ノードが flatten されたリストが返ってそう? たぶん遅延解決する node はフラグを建てずに後回しにしておいて、全部終わるまで繰り返すんだろう、という予測。

mizchimizchi

checkSourceFile -> checkSourceElement

    function checkSourceElement(node: Node | undefined): void {
        if (node) {
            const saveCurrentNode = currentNode;
            currentNode = node;
            instantiationCount = 0;
            checkSourceElementWorker(node);
            currentNode = saveCurrentNode;
        }
    }
// ...

    function checkSourceElementWorker(node: Node): void {
        if (canHaveJSDoc(node)) {
            forEach(node.jsDoc, ({ comment, tags }) => {
                checkJSDocCommentWorker(comment);
                forEach(tags, tag => {
                    checkJSDocCommentWorker(tag.comment);
                    if (isInJSFile(node)) {
                        checkSourceElement(tag);
                    }
                });
            });
        }

        const kind = node.kind;
        if (cancellationToken) {
            // Only bother checking on a few construct kinds.  We don't want to be excessively
            // hitting the cancellation token on every node we check.
            switch (kind) {
                case SyntaxKind.ModuleDeclaration:
                case SyntaxKind.ClassDeclaration:
                case SyntaxKind.InterfaceDeclaration:
                case SyntaxKind.FunctionDeclaration:
                    cancellationToken.throwIfCancellationRequested();
            }
        }
        if (kind >= SyntaxKind.FirstStatement && kind <= SyntaxKind.LastStatement && canHaveFlowNode(node) && node.flowNode && !isReachableFlowNode(node.flowNode)) {
            errorOrSuggestion(compilerOptions.allowUnreachableCode === false, node, Diagnostics.Unreachable_code_detected);
        }

        switch (kind) {
            case SyntaxKind.TypeParameter:
                return checkTypeParameter(node as TypeParameterDeclaration);
// 以下、Nodeごとのcase処理

勘だがトップレベル限定の処理かな。

  • JSDoc の確認。ここは飛ばす
  • cancellationToken があれば、特定のノードのとき例外リクエストを叩く。全ノードでやらないのは大量の例外リクエストを送らないようにするためとコメントで書いてある
  • 開始、終了、または FlowNode かの確認

flowNode 概念とはなんだろう。めちゃくちゃエスパーすると、入力途中で場合によっては空になってしまう可能性があるNodeとか?

/** @internal */
export function canHaveFlowNode(node: Node): node is HasFlowNode {
    if (node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement) {
        return true;
    }

    switch (node.kind) {
        case SyntaxKind.Identifier:
        case SyntaxKind.ThisKeyword:
        case SyntaxKind.SuperKeyword:
        case SyntaxKind.QualifiedName:
        case SyntaxKind.MetaProperty:
        case SyntaxKind.ElementAccessExpression:
        case SyntaxKind.PropertyAccessExpression:
        case SyntaxKind.BindingElement:
        case SyntaxKind.FunctionExpression:
        case SyntaxKind.ArrowFunction:
        case SyntaxKind.MethodDeclaration:
        case SyntaxKind.GetAccessor:
        case SyntaxKind.SetAccessor:
            return true;
        default:
            return false;
    }
}

いやもしかして facebook の型検査器の flow のことか?パースだけ試みてるとか? わからん...

export type FlowNode =
    | FlowStart
    | FlowLabel
    | FlowAssignment
    | FlowCondition
    | FlowSwitchClause
    | FlowArrayMutation
    | FlowCall
    | FlowReduceLabel;

// FlowStart represents the start of a control flow. For a function expression or arrow
// function, the node property references the function (which in turn has a flowNode
// property for the containing control flow).
export interface FlowStart extends FlowNodeBase {
    node?: FunctionExpression | ArrowFunction | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration;
}

// FlowLabel represents a junction with multiple possible preceding control flows.
export interface FlowLabel extends FlowNodeBase {
    antecedents: FlowNode[] | undefined;
}

// FlowAssignment represents a node that assigns a value to a narrowable reference,
// i.e. an identifier or a dotted name that starts with an identifier or 'this'.
export interface FlowAssignment extends FlowNodeBase {
    node: Expression | VariableDeclaration | BindingElement;
    antecedent: FlowNode;
}

export interface FlowCall extends FlowNodeBase {
    node: CallExpression;
    antecedent: FlowNode;
}
    function isReachableFlowNode(flow: FlowNode) {
        const result = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ false);
        lastFlowNode = flow;
        lastFlowNodeReachable = result;
        return result;
    }

Deepl

// FlowStartは、制御フローの開始を表す。関数式や矢印の場合
// 関数の場合、node プロパティは関数を参照します(関数には、制御フローを含む flowNode プロパティがあります)。
// プロパティを持つ)。

制御フローの開始になりうるか、という分類っぽい。さすがに flowtype ではなさそう。

checkSourceElement から呼ばれてる isReachableFlowNodeWorker をみる。

    function isReachableFlowNodeWorker(flow: FlowNode, noCacheCheck: boolean): boolean {
        while (true) {
            if (flow === lastFlowNode) {
                return lastFlowNodeReachable;
            }
            const flags = flow.flags;
            if (flags & FlowFlags.Shared) {
                if (!noCacheCheck) {
                    const id = getFlowNodeId(flow);
                    const reachable = flowNodeReachable[id];
                    return reachable !== undefined ? reachable : (flowNodeReachable[id] = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ true));
                }
                noCacheCheck = false;
            }
            if (flags & (FlowFlags.Assignment | FlowFlags.Condition | FlowFlags.ArrayMutation)) {
                flow = (flow as FlowAssignment | FlowCondition | FlowArrayMutation).antecedent;
            }
            else if (flags & FlowFlags.Call) {
                const signature = getEffectsSignature((flow as FlowCall).node);
                if (signature) {
                    const predicate = getTypePredicateOfSignature(signature);
                    if (predicate && predicate.kind === TypePredicateKind.AssertsIdentifier && !predicate.type) {
                        const predicateArgument = (flow as FlowCall).node.arguments[predicate.parameterIndex];
                        if (predicateArgument && isFalseExpression(predicateArgument)) {
                            return false;
                        }
                    }
                    if (getReturnTypeOfSignature(signature).flags & TypeFlags.Never) {
                        return false;
                    }
                }
                flow = (flow as FlowCall).antecedent;
            }
            else if (flags & FlowFlags.BranchLabel) {
                // A branching point is reachable if any branch is reachable.
                return some((flow as FlowLabel).antecedents, f => isReachableFlowNodeWorker(f, /*noCacheCheck*/ false));
            }
            else if (flags & FlowFlags.LoopLabel) {
                const antecedents = (flow as FlowLabel).antecedents;
                if (antecedents === undefined || antecedents.length === 0) {
                    return false;
                }
                // A loop is reachable if the control flow path that leads to the top is reachable.
                flow = antecedents[0];
            }
            else if (flags & FlowFlags.SwitchClause) {
                // The control flow path representing an unmatched value in a switch statement with
                // no default clause is unreachable if the switch statement is exhaustive.
                if ((flow as FlowSwitchClause).clauseStart === (flow as FlowSwitchClause).clauseEnd && isExhaustiveSwitchStatement((flow as FlowSwitchClause).switchStatement)) {
                    return false;
                }
                flow = (flow as FlowSwitchClause).antecedent;
            }
            else if (flags & FlowFlags.ReduceLabel) {
                // Cache is unreliable once we start adjusting labels
                lastFlowNode = undefined;
                const target = (flow as FlowReduceLabel).target;
                const saveAntecedents = target.antecedents;
                target.antecedents = (flow as FlowReduceLabel).antecedents;
                const result = isReachableFlowNodeWorker((flow as FlowReduceLabel).antecedent, /*noCacheCheck*/ false);
                target.antecedents = saveAntecedents;
                return result;
            }
            else {
                return !(flags & FlowFlags.Unreachable);
            }
        }
    }

関数呼び出し時に effects signature というものを収集している。

    function getEffectsSignature(node: CallExpression) {
        const links = getNodeLinks(node);
        let signature = links.effectsSignature;
        if (signature === undefined) {
            // A call expression parented by an expression statement is a potential assertion. Other call
            // expressions are potential type predicate function calls. In order to avoid triggering
            // circularities in control flow analysis, we use getTypeOfDottedName when resolving the call
            // target expression of an assertion.
            let funcType: Type | undefined;
            if (node.parent.kind === SyntaxKind.ExpressionStatement) {
                funcType = getTypeOfDottedName(node.expression, /*diagnostic*/ undefined);
            }
            else if (node.expression.kind !== SyntaxKind.SuperKeyword) {
                if (isOptionalChain(node)) {
                    funcType = checkNonNullType(
                        getOptionalExpressionType(checkExpression(node.expression), node.expression),
                        node.expression
                    );
                }
                else {
                    funcType = checkNonNullExpression(node.expression);
                }
            }
            const signatures = getSignaturesOfType(funcType && getApparentType(funcType) || unknownType, SignatureKind.Call);
            const candidate = signatures.length === 1 && !signatures[0].typeParameters ? signatures[0] :
                some(signatures, hasTypePredicateOrNeverReturnType) ? getResolvedSignature(node) :
                undefined;
            signature = links.effectsSignature = candidate && hasTypePredicateOrNeverReturnType(candidate) ? candidate : unknownSignature;
        }
        return signature === unknownSignature ? undefined : signature;
    }

コメントいわく、関数呼び出しが親となる expression statement は潜在的にアサーションであり、 他は型が予想可能な関数呼び出しである、みたいな感じ?。 アサーションとはどのアサーションだろう。 foo(): x is number のことだろうか。

コントロールフロー中の循環参照を防ぐために getTypeOfDottedName というものを使ってるよ、とのこと。

つまり、ここでいう flow は control flow のことか。

NodeLinks とは その node 中の解析情報のことっぽい。

/** @internal */
export interface NodeLinks {
    flags: NodeCheckFlags;              // Set of flags specific to Node
    resolvedType?: Type;                // Cached type of type node
    resolvedEnumType?: Type;            // Cached constraint type from enum jsdoc tag
    resolvedSignature?: Signature;      // Cached signature of signature node or call expression
    resolvedSymbol?: Symbol;            // Cached name resolution result
    resolvedIndexInfo?: IndexInfo;      // Cached indexing info resolution result
    effectsSignature?: Signature;       // Signature with possible control flow effects
    enumMemberValue?: string | number;  // Constant value of enum member
    isVisible?: boolean;                // Is this node visible
    containsArgumentsReference?: boolean; // Whether a function-like declaration contains an 'arguments' reference
    hasReportedStatementInAmbientContext?: boolean; // Cache boolean if we report statements in ambient context
    jsxFlags: JsxFlags;                 // flags for knowing what kind of element/attributes we're dealing with
    resolvedJsxElementAttributesType?: Type; // resolved element attributes type of a JSX openinglike element
    resolvedJsxElementAllAttributesType?: Type; // resolved all element attributes type of a JSX openinglike element
    resolvedJSDocType?: Type;           // Resolved type of a JSDoc type reference
    switchTypes?: Type[];               // Cached array of switch case expression types
    jsxNamespace?: Symbol | false;      // Resolved jsx namespace symbol for this node
    jsxImplicitImportContainer?: Symbol | false; // Resolved module symbol the implicit jsx import of this file should refer to
    contextFreeType?: Type;             // Cached context-free type used by the first pass of inference; used when a function's return is partially contextually sensitive
    deferredNodes?: Set<Node>;          // Set of nodes whose checking has been deferred
    capturedBlockScopeBindings?: Symbol[]; // Block-scoped bindings captured beneath this part of an IterationStatement
    outerTypeParameters?: TypeParameter[]; // Outer type parameters of anonymous object type
    isExhaustive?: boolean | 0;         // Is node an exhaustive switch statement (0 indicates in-process resolution)
    skipDirectInference?: true;         // Flag set by the API `getContextualType` call on a node when `Completions` is passed to force the checker to skip making inferences to a node's type
    declarationRequiresScopeChange?: boolean; // Set by `useOuterVariableScopeInParameter` in checker when downlevel emit would change the name resolution scope inside of a parameter.
    serializedTypes?: Map<string, SerializedTypeEntry>; // Collection of types serialized at this location
    decoratorSignature?: Signature;     // Signature for decorator as if invoked by the runtime.
    spreadIndices?: { first: number | undefined, last: number | undefined }; // Indices of first and last spread elements in array literal
    parameterInitializerContainsUndefined?: boolean; // True if this is a parameter declaration whose type annotation contains "undefined".
    fakeScopeForSignatureDeclaration?: boolean; // True if this is a fake scope injected into an enclosing declaration chain.
    assertionExpressionType?: Type;     // Cached type of the expression of a type assertion
}

これ先にでてきたな。先に getNodeLinks をちゃんと読むべきだった。

わかってきたかも。木構造と1:1対応する線形の NodeLinks があり、それぞれの Node で解決された型やシンボルをキャッシュしている。今解決できなかったものは deferred に入って、いずれ全部の解析が終わらないといけない、みたいな感じだろうか。終了検知大変そう。だから cancellation token があるのかな。

mizchimizchi

続き。super でない関数は checkNonNullType()funcType を推論する。

            let funcType: Type | undefined;
            if (node.parent.kind === SyntaxKind.ExpressionStatement) {
                funcType = getTypeOfDottedName(node.expression, /*diagnostic*/ undefined);
            }
            else if (node.expression.kind !== SyntaxKind.SuperKeyword) {
                if (isOptionalChain(node)) {
                    funcType = checkNonNullType(
                        getOptionalExpressionType(checkExpression(node.expression), node.expression),
                        node.expression
                    );
                }
                else {
                    funcType = checkNonNullExpression(node.expression);
                }
            }

で、funcType 見つかった場合は ApparentType (たぶん明示的に宣言された型のこと) もしくは unknownType で getSignaturesOfType() で signatures を返す。

            const signatures = getSignaturesOfType(funcType && getApparentType(funcType) || unknownType, SignatureKind.Call);
            const candidate = signatures.length === 1 && !signatures[0].typeParameters ? signatures[0] :
                some(signatures, hasTypePredicateOrNeverReturnType) ? getResolvedSignature(node) :
                undefined;
            signature = links.effectsSignature = candidate && hasTypePredicateOrNeverReturnType(candidate) ? candidate : unknownSignature;

signatures が 1つなら 0 番目の typeParameters を、 そうでなければ signatures のいずれかが hasTypePredicateOrNeverReturnType なら、 getResolvedSignature(node) する。(書いてて、全然わかってない)

最後に、推論された型の候補が hasTypePredicateOrNeverReturnType() を満たせば、links.effectsSignature にそれを代入する

たぶんこういうコードか?

function isFoo(t: string): t is "foo" {
  return t === "foo"
}

let x = "foo";
if (isFoo(x)) {
  console.log(x); // この context では x: "foo" と narrowing される
};

読む順番が悪かった気がする。いきなりコントロールフローの解析からはじまるのか。ただヒントは得られていて、 Type, Signature, ApparentType の関係を整理するとシュッと読めるような気がする。

まず Signature の型をみる。

export interface Signature {
    /** @internal */ flags: SignatureFlags;
    /** @internal */ checker?: TypeChecker;
    declaration?: SignatureDeclaration | JSDocSignature; // Originating declaration
    typeParameters?: readonly TypeParameter[];   // Type parameters (undefined if non-generic)
    parameters: readonly Symbol[];               // Parameters
    /** @internal */
    thisParameter?: Symbol;             // symbol of this-type parameter
    /** @internal */
    // See comment in `instantiateSignature` for why these are set lazily.
    resolvedReturnType?: Type;          // Lazily set by `getReturnTypeOfSignature`.
    /** @internal */
    // Lazily set by `getTypePredicateOfSignature`.
    // `undefined` indicates a type predicate that has not yet been computed.
    // Uses a special `noTypePredicate` sentinel value to indicate that there is no type predicate. This looks like a TypePredicate at runtime to avoid polymorphism.
    resolvedTypePredicate?: TypePredicate;
    /** @internal */
    minArgumentCount: number;           // Number of non-optional parameters
    /** @internal */
    resolvedMinArgumentCount?: number;  // Number of non-optional parameters (excluding trailing `void`)
    /** @internal */
    target?: Signature;                 // Instantiation target
    /** @internal */
    mapper?: TypeMapper;                // Instantiation mapper
    /** @internal */
    compositeSignatures?: Signature[];  // Underlying signatures of a union/intersection signature
    /** @internal */
    compositeKind?: TypeFlags;          // TypeFlags.Union if the underlying signatures are from union members, otherwise TypeFlags.Intersection
    /** @internal */
    erasedSignatureCache?: Signature;   // Erased version of signature (deferred)
    /** @internal */
    canonicalSignatureCache?: Signature; // Canonical version of signature (deferred)
    /** @internal */
    baseSignatureCache?: Signature;      // Base version of signature (deferred)
    /** @internal */
    optionalCallSignatureCache?: { inner?: Signature, outer?: Signature }; // Optional chained call version of signature (deferred)
    /** @internal */
    isolatedSignatureType?: ObjectType; // A manufactured type that just contains the signature for purposes of signature comparison
    /** @internal */
    instantiations?: Map<string, Signature>;    // Generic signature instantiation cache
}

SignatureDeclaration の宣言、型引数、遅延で解決される resolvedTypePredicate などを持ってる。union の場合は compositeSignatures になるのだろうか。

SignatureDeclarationの定義をみる

export type SignatureDeclaration =
    | CallSignatureDeclaration
    | ConstructSignatureDeclaration
    | MethodSignature
    | IndexSignatureDeclaration
    | FunctionTypeNode
    | ConstructorTypeNode
    | JSDocFunctionType
    | FunctionDeclaration
    | MethodDeclaration
    | ConstructorDeclaration
    | AccessorDeclaration
    | FunctionExpression
    | ArrowFunction;

Signature、 型シグネチャのことだろうが、頭の中でふわっとしてるので、用語の定義をちゃんとみる

https://ja.wikipedia.org/wiki/シグネチャ

プログラミングで、メソッドや関数の、名前および引数の数や型の順序などの書式。戻り値の型を含む場合もある。

ですよね。

Type 型の定義(メタい)

// Properties common to all types
export interface Type {
    flags: TypeFlags;                // Flags
    /** @internal */ id: TypeId;      // Unique ID
    /** @internal */ checker: TypeChecker;
    symbol: Symbol;                  // Symbol associated with type (if any)
    pattern?: DestructuringPattern;  // Destructuring pattern represented by type (if any)
    aliasSymbol?: Symbol;            // Alias associated with type
    aliasTypeArguments?: readonly Type[]; // Alias type arguments (if any)
    /** @internal */
    permissiveInstantiation?: Type;  // Instantiation with type parameters mapped to wildcard type
    /** @internal */
    restrictiveInstantiation?: Type; // Instantiation with type parameters mapped to unconstrained form
    /** @internal */
    immediateBaseConstraint?: Type;  // Immediate base constraint cache
    /** @internal */
    widened?: Type; // Cached widened form of the type
}

TypeFlags を見る

export const enum TypeFlags {
    Any             = 1 << 0,
    Unknown         = 1 << 1,
    String          = 1 << 2,
    Number          = 1 << 3,
    Boolean         = 1 << 4,
    Enum            = 1 << 5,   // Numeric computed enum member value
    BigInt          = 1 << 6,
    StringLiteral   = 1 << 7,
    NumberLiteral   = 1 << 8,
    BooleanLiteral  = 1 << 9,
    EnumLiteral     = 1 << 10,  // Always combined with StringLiteral, NumberLiteral, or Union
    BigIntLiteral   = 1 << 11,
    ESSymbol        = 1 << 12,  // Type of symbol primitive introduced in ES6
    UniqueESSymbol  = 1 << 13,  // unique symbol
    Void            = 1 << 14,
    Undefined       = 1 << 15,
    Null            = 1 << 16,
    Never           = 1 << 17,  // Never type
    TypeParameter   = 1 << 18,  // Type parameter
    Object          = 1 << 19,  // Object type
    Union           = 1 << 20,  // Union (T | U)
    Intersection    = 1 << 21,  // Intersection (T & U)
    Index           = 1 << 22,  // keyof T
    IndexedAccess   = 1 << 23,  // T[K]
    Conditional     = 1 << 24,  // T extends U ? X : Y
    Substitution    = 1 << 25,  // Type parameter substitution
    NonPrimitive    = 1 << 26,  // intrinsic object type
    TemplateLiteral = 1 << 27,  // Template literal type
    StringMapping   = 1 << 28,  // Uppercase/Lowercase type

    /** @internal */
    AnyOrUnknown = Any | Unknown,
    /** @internal */
    Nullable = Undefined | Null,
    Literal = StringLiteral | NumberLiteral | BigIntLiteral | BooleanLiteral,
    Unit = Enum | Literal | UniqueESSymbol | Nullable,
    Freshable = Enum | Literal,
    StringOrNumberLiteral = StringLiteral | NumberLiteral,
    /** @internal */
    StringOrNumberLiteralOrUnique = StringLiteral | NumberLiteral | UniqueESSymbol,
    /** @internal */
    DefinitelyFalsy = StringLiteral | NumberLiteral | BigIntLiteral | BooleanLiteral | Void | Undefined | Null,
    PossiblyFalsy = DefinitelyFalsy | String | Number | BigInt | Boolean,
    /** @internal */
    Intrinsic = Any | Unknown | String | Number | BigInt | Boolean | BooleanLiteral | ESSymbol | Void | Undefined | Null | Never | NonPrimitive,
    StringLike = String | StringLiteral | TemplateLiteral | StringMapping,
    NumberLike = Number | NumberLiteral | Enum,
    BigIntLike = BigInt | BigIntLiteral,
    BooleanLike = Boolean | BooleanLiteral,
    EnumLike = Enum | EnumLiteral,
    ESSymbolLike = ESSymbol | UniqueESSymbol,
    VoidLike = Void | Undefined,
    /** @internal */
    Primitive = StringLike | NumberLike | BigIntLike | BooleanLike | EnumLike | ESSymbolLike | VoidLike | Null,
    /** @internal */
    DefinitelyNonNullable = StringLike | NumberLike | BigIntLike | BooleanLike | EnumLike | ESSymbolLike | Object | NonPrimitive,
    /** @internal */
    DisjointDomains = NonPrimitive | StringLike | NumberLike | BigIntLike | BooleanLike | ESSymbolLike | VoidLike | Null,
    UnionOrIntersection = Union | Intersection,
    StructuredType = Object | Union | Intersection,
    TypeVariable = TypeParameter | IndexedAccess,
    InstantiableNonPrimitive = TypeVariable | Conditional | Substitution,
    InstantiablePrimitive = Index | TemplateLiteral | StringMapping,
    Instantiable = InstantiableNonPrimitive | InstantiablePrimitive,
    StructuredOrInstantiable = StructuredType | Instantiable,
    /** @internal */
    ObjectFlagsType = Any | Nullable | Never | Object | Union | Intersection | TemplateLiteral,
    /** @internal */
    Simplifiable = IndexedAccess | Conditional,
    /** @internal */
    Singleton = Any | Unknown | String | Number | Boolean | BigInt | ESSymbol | Void | Undefined | Null | Never | NonPrimitive,
    // 'Narrowable' types are types where narrowing actually narrows.
    // This *should* be every type other than null, undefined, void, and never
    Narrowable = Any | Unknown | StructuredOrInstantiable | StringLike | NumberLike | BigIntLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive,
    // The following flags are aggregated during union and intersection type construction
    /** @internal */
    IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral,
    // The following flags are used for different purposes during union and intersection type construction
    /** @internal */
    IncludesMissingType = TypeParameter,
    /** @internal */
    IncludesNonWideningType = Index,
    /** @internal */
    IncludesWildcard = IndexedAccess,
    /** @internal */
    IncludesEmptyObject = Conditional,
    /** @internal */
    IncludesInstantiable = Substitution,
    /** @internal */
    NotPrimitiveUnion = Any | Unknown | Void | Never | Object | Intersection | IncludesInstantiable,
}

色々な推論状態を格納するためにビットフラグで押し込んでるのかな。

よく見る compiler api でよく見る Symbol ってやつも、ふわっとした理解だったので、この機にちゃんと定義を見ておく。

export interface Symbol {
    flags: SymbolFlags;                     // Symbol flags
    escapedName: __String;                  // Name of symbol
    declarations?: Declaration[];           // Declarations associated with this symbol
    valueDeclaration?: Declaration;         // First value declaration of the symbol
    members?: SymbolTable;                  // Class, interface or object literal instance members
    exports?: SymbolTable;                  // Module exports
    globalExports?: SymbolTable;            // Conditional global UMD exports
    /** @internal */ id: SymbolId;          // Unique id (used to look up SymbolLinks)
    /** @internal */ mergeId: number;       // Merge id (used to look up merged symbol)
    /** @internal */ parent?: Symbol;       // Parent symbol
    /** @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol
    /** @internal */ constEnumOnlyModule: boolean | undefined; // True if module contains only const enums or other modules with only const enums
    /** @internal */ isReferenced?: SymbolFlags; // True if the symbol is referenced elsewhere. Keeps track of the meaning of a reference in case a symbol is both a type parameter and parameter.
    /** @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol?
    /** @internal */ isAssigned?: boolean;  // True if the symbol is a parameter with assignments
    /** @internal */ assignmentDeclarationMembers?: Map<number, Declaration>; // detected late-bound assignment declarations associated with the symbol
}

雑に Identifie より上の概念という理解をしていた。名前を持ち、宣言元情報がある。 class, interface の場合は members を持っていて、 誰によって exports されたか追跡できるっぽい。

SymbolFlags と SymbolTable, そして Declaration の概念も必要だ。

export const enum SymbolFlags {
    None                    = 0,
    FunctionScopedVariable  = 1 << 0,   // Variable (var) or parameter
    BlockScopedVariable     = 1 << 1,   // A block-scoped variable (let or const)
    Property                = 1 << 2,   // Property or enum member
    EnumMember              = 1 << 3,   // Enum member
    Function                = 1 << 4,   // Function
    Class                   = 1 << 5,   // Class
    Interface               = 1 << 6,   // Interface
    ConstEnum               = 1 << 7,   // Const enum
    RegularEnum             = 1 << 8,   // Enum
    ValueModule             = 1 << 9,   // Instantiated module
    NamespaceModule         = 1 << 10,  // Uninstantiated module
    TypeLiteral             = 1 << 11,  // Type Literal or mapped type
    ObjectLiteral           = 1 << 12,  // Object Literal
    Method                  = 1 << 13,  // Method
    Constructor             = 1 << 14,  // Constructor
    GetAccessor             = 1 << 15,  // Get accessor
    SetAccessor             = 1 << 16,  // Set accessor
    Signature               = 1 << 17,  // Call, construct, or index signature
    TypeParameter           = 1 << 18,  // Type parameter
    TypeAlias               = 1 << 19,  // Type alias
    ExportValue             = 1 << 20,  // Exported value marker (see comment in declareModuleMember in binder)
    Alias                   = 1 << 21,  // An alias for another symbol (see comment in isAliasSymbolDeclaration in checker)
    Prototype               = 1 << 22,  // Prototype property (no source representation)
    ExportStar              = 1 << 23,  // Export * declaration
    Optional                = 1 << 24,  // Optional property
    Transient               = 1 << 25,  // Transient symbol (created during type check)
    Assignment              = 1 << 26,  // Assignment treated as declaration (eg `this.prop = 1`)
    ModuleExports           = 1 << 27,  // Symbol for CommonJS `module` of `module.exports`
    /** @internal */
    All = FunctionScopedVariable | BlockScopedVariable | Property | EnumMember | Function | Class | Interface | ConstEnum | RegularEnum | ValueModule | NamespaceModule | TypeLiteral
        | ObjectLiteral | Method | Constructor | GetAccessor | SetAccessor | Signature | TypeParameter | TypeAlias | ExportValue | Alias | Prototype | ExportStar | Optional | Transient,

    Enum = RegularEnum | ConstEnum,
    Variable = FunctionScopedVariable | BlockScopedVariable,
    Value = Variable | Property | EnumMember | ObjectLiteral | Function | Class | Enum | ValueModule | Method | GetAccessor | SetAccessor,
    Type = Class | Interface | Enum | EnumMember | TypeLiteral | TypeParameter | TypeAlias,
    Namespace = ValueModule | NamespaceModule | Enum,
    Module = ValueModule | NamespaceModule,
    Accessor = GetAccessor | SetAccessor,

    // Variables can be redeclared, but can not redeclare a block-scoped declaration with the
    // same name, or any other value that is not a variable, e.g. ValueModule or Class
    FunctionScopedVariableExcludes = Value & ~FunctionScopedVariable,

    // Block-scoped declarations are not allowed to be re-declared
    // they can not merge with anything in the value space
    BlockScopedVariableExcludes = Value,

    ParameterExcludes = Value,
    PropertyExcludes = None,
    EnumMemberExcludes = Value | Type,
    FunctionExcludes = Value & ~(Function | ValueModule | Class),
    ClassExcludes = (Value | Type) & ~(ValueModule | Interface | Function), // class-interface mergability done in checker.ts
    InterfaceExcludes = Type & ~(Interface | Class),
    RegularEnumExcludes = (Value | Type) & ~(RegularEnum | ValueModule), // regular enums merge only with regular enums and modules
    ConstEnumExcludes = (Value | Type) & ~ConstEnum, // const enums merge only with const enums
    ValueModuleExcludes = Value & ~(Function | Class | RegularEnum | ValueModule),
    NamespaceModuleExcludes = 0,
    MethodExcludes = Value & ~Method,
    GetAccessorExcludes = Value & ~SetAccessor,
    SetAccessorExcludes = Value & ~GetAccessor,
    AccessorExcludes = Value & ~Accessor,
    TypeParameterExcludes = Type & ~TypeParameter,
    TypeAliasExcludes = Type,
    AliasExcludes = Alias,

    ModuleMember = Variable | Function | Class | Interface | Enum | Module | TypeAlias | Alias,

    ExportHasLocal = Function | Class | Enum | ValueModule,

    BlockScoped = BlockScopedVariable | Class | Enum,

    PropertyOrAccessor = Property | Accessor,

    ClassMember = Method | Accessor | Property,

    /** @internal */
    ExportSupportsDefaultModifier = Class | Function | Interface,

    /** @internal */
    ExportDoesNotSupportDefaultModifier = ~ExportSupportsDefaultModifier,

    /** @internal */
    // The set of things we consider semantically classifiable.  Used to speed up the LS during
    // classification.
    Classifiable = Class | Enum | TypeAlias | Interface | TypeParameter | Module | Alias,

    /** @internal */
    LateBindingContainer = Class | Interface | TypeLiteral | ObjectLiteral | Function,
}

Symbol がどういう属性をもってるかが格納されてそう。

ところでこのチルダ記法始めてみた。

    ConstEnumExcludes = (Value | Type) & ~ConstEnum, // const enums merge only with const enums

enum 同士のマージってこと? というか enum の初期化子って、こんな型レベル演算できていいの...

SymbolTable

/** SymbolTable based on ES6 Map interface. */
export type SymbolTable = Map<__String, Symbol>;

なんらかの symbol map

Declaration

export interface Declaration extends Node {
    _declarationBrand: any;
    /** @internal */ symbol: Symbol;                       // Symbol declared by node (initialized by binding)
    /** @internal */ localSymbol?: Symbol;                 // Local symbol declared by node (initialized by binding only for exported nodes)
}

Node の一種で、 symbol と localSymbol を持ってる。 localSymbol のコメント見るに export {a as b} from "..." のやつで、基本は symbol 参照だろう。

たぶんこれは実体がある型というよりは継承される abstract なやつで、例えば ClassDeclaration がこれを継承している

export interface ClassDeclaration extends ClassLikeDeclarationBase, DeclarationStatement {
    readonly kind: SyntaxKind.ClassDeclaration;
    readonly modifiers?: NodeArray<ModifierLike>;
    /** May be undefined in `export default class { ... }`. */
    readonly name?: Identifier;
}

//...

export interface DeclarationStatement extends NamedDeclaration, Statement {
    readonly name?: Identifier | StringLiteral | NumericLiteral;
}

//...

export interface NamedDeclaration extends Declaration {
    readonly name?: DeclarationName;
}

NamedDeclaration で Statement で ClassLikeDeclarationBase なもの。
Declaration を持つノードは定義元を追えるんだろう。

ついでに Identifier も見た。

export interface Identifier extends PrimaryExpression, Declaration, JSDocContainer, FlowContainer {
    readonly kind: SyntaxKind.Identifier;
    /**
     * Prefer to use `id.unescapedText`. (Note: This is available only in services, not internally to the TypeScript compiler.)
     * Text of identifier, but if the identifier begins with two underscores, this will begin with three.
     */
    readonly escapedText: __String;
}

なるほど FlowContainer がここに出てくるのか

mizchimizchi

ついでに ApparentType 概念よくみるので実装をみる

    /**
     * For a type parameter, return the base constraint of the type parameter. For the string, number,
     * boolean, and symbol primitive types, return the corresponding object types. Otherwise return the
     * type itself.
     */
    function getApparentType(type: Type): Type {
        const t = !(type.flags & TypeFlags.Instantiable) ? type : getBaseConstraintOfType(type) || unknownType;
        return getObjectFlags(t) & ObjectFlags.Mapped ? getApparentTypeOfMappedType(t as MappedType) :
            t.flags & TypeFlags.Intersection ? getApparentTypeOfIntersectionType(t as IntersectionType) :
            t.flags & TypeFlags.StringLike ? globalStringType :
            t.flags & TypeFlags.NumberLike ? globalNumberType :
            t.flags & TypeFlags.BigIntLike ? getGlobalBigIntType() :
            t.flags & TypeFlags.BooleanLike ? globalBooleanType :
            t.flags & TypeFlags.ESSymbolLike ? getGlobalESSymbolType() :
            t.flags & TypeFlags.NonPrimitive ? emptyObjectType :
            t.flags & TypeFlags.Index ? keyofConstraintType :
            t.flags & TypeFlags.Unknown && !strictNullChecks ? emptyObjectType :
            t;
    }

型引数を解決した Type 型か。Instance化可能な型で(たぶんできない型が never と unknown とか) getBaseConstraintOfType(type) の結果が ObjectFlags.Mapped でなければ、ビットマスクでさらに絞り込んでるっぽい。ObjectMapped な型は MappedType として解決する。これは Conditional Mapped Type の内部実装かな。

getBaseConstraintOfType を見る

    function getBaseConstraintOfType(type: Type): Type | undefined {
        if (type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) || isGenericTupleType(type)) {
            const constraint = getResolvedBaseConstraint(type as InstantiableType | UnionOrIntersectionType);
            return constraint !== noConstraintType && constraint !== circularConstraintType ? constraint : undefined;
        }
        return type.flags & TypeFlags.Index ? keyofConstraintType : undefined;
    }

まず GenericTupleType かを見て、 getResolvedBaseConstraint を設定する。

これは多分 [a as A, b as B, c as C] のような配列があったとき、基底の型が A & B & C になるやつかな。

そうでない場合は、flags が TypeFlag.Index ならば keyofConstraintType になる。Obj[keyof Obj] みたいなやつか? 自信がない。

    function getApparentTypeOfMappedType(type: MappedType) {
        return type.resolvedApparentType || (type.resolvedApparentType = getResolvedApparentTypeOfMappedType(type));
    }

    function getResolvedApparentTypeOfMappedType(type: MappedType) {
        const typeVariable = getHomomorphicTypeVariable(type);
        if (typeVariable && !type.declaration.nameType) {
            const constraint = getConstraintOfTypeParameter(typeVariable);
            if (constraint && everyType(constraint, isArrayOrTupleType)) {
                return instantiateType(type, prependTypeMapping(typeVariable, constraint, type.mapper));
            }
        }
        return type;
    }

getHomomorphicTypeVariable で typeVariable を得て、 名前がついてない型だったら さっき読んだ getConstraintOfTypeParameter して、その結果の constraint が全部が ArrayOrTuple だったら、 instantiateType() で型を解決する。(その前に prependTypeMapping とかいうことをする)

instantiateType はきっと type X<T> = T に対して X<InstanceType> するときだとイメージ付くが、getHomomorphicTypeVariable が何してるか全然わからないな。

    function getHomomorphicTypeVariable(type: MappedType) {
        const constraintType = getConstraintTypeFromMappedType(type);
        if (constraintType.flags & TypeFlags.Index) {
            const typeVariable = getActualTypeVariable((constraintType as IndexType).type);
            if (typeVariable.flags & TypeFlags.TypeParameter) {
                return typeVariable as TypeParameter;
            }
        }
        return undefined;
    }

TypeFlags.Index ってなんだよって思って定義をみたら keyof のことらしい

    Index           = 1 << 22,  // keyof T

すべてのキーに対して getActualTypeVariable して(再帰) その typeVariables が typeParameters をもっているかみてる?

ここまでふわっとスルーしてきたが homomorphic で辞書を引いたら準同型のことだった。型引数を解決した準同型の型を得ているというニュアンスだろうか。

https://ja.wikipedia.org/wiki/準同型

特に、準同型写像 f: A → B が与えられたとき、その像 f(A) は B の部分代数系となる。このとき一般には、像 f(A) はもとの代数系 A からある程度 "つぶれている" ため、像 f(A) から直接にもとの代数系 A の様子を知ることは完全にはできないのであるが、この潰れ具合は準同型の核と呼ばれる同値関係によって推し量ることができ、それによってもとの代数系 A を復元することができる。一方、準同型 f が単射であれば A は B にその構造まで込めて埋め込まれる。

わかりやすい。ApparentType からは型引数を解決する前の構造を推し量れないが、結果的に同じ構造を指してる、という理解をした。(でもこれどの段階の展開を指してるのかよくわかってない)

次は instantiateType をみる

    function instantiateType(type: Type, mapper: TypeMapper | undefined): Type;
    function instantiateType(type: Type | undefined, mapper: TypeMapper | undefined): Type | undefined;
    function instantiateType(type: Type | undefined, mapper: TypeMapper | undefined): Type | undefined {
        return type && mapper ? instantiateTypeWithAlias(type, mapper, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined) : type;
    }

    function instantiateTypeWithAlias(type: Type, mapper: TypeMapper, aliasSymbol: Symbol | undefined, aliasTypeArguments: readonly Type[] | undefined): Type {
        if (!couldContainTypeVariables(type)) {
            return type;
        }
        if (instantiationDepth === 100 || instantiationCount >= 5000000) {
            // We have reached 100 recursive type instantiations, or 5M type instantiations caused by the same statement
            // or expression. There is a very high likelyhood we're dealing with a combination of infinite generic types
            // that perpetually generate new type identities, so we stop the recursion here by yielding the error type.
            tracing?.instant(tracing.Phase.CheckTypes, "instantiateType_DepthLimit", { typeId: type.id, instantiationDepth, instantiationCount });
            error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite);
            return errorType;
        }
        totalInstantiationCount++;
        instantiationCount++;
        instantiationDepth++;
        const result = instantiateTypeWorker(type, mapper, aliasSymbol, aliasTypeArguments);
        instantiationDepth--;
        return result;
    }

100回までしか再帰できない!

instantiateTypeWorker が本番か。

mizchimizchi

ここまでのあらすじ

TypeScriptの型推論の大まかな構造がわかってきた。TypeFlags で取りうる状態全部をビットフラグで表現して、union による型の合成は型レベルの計算でも本当に ビット演算のA | B で intersection が A & B なんだ。構造を持つ MappedType が絡む場合はもっと複雑だけど基本的にメンバー同士でそれやるだけのはず。

mizchimizchi

instantiateTypeWorker の続き

TypeParameter のとき、 typedMapper と一緒に getMappedType を呼ぶ。 typed mapper の定義

/** @internal */
export type TypeMapper =
    | { kind: TypeMapKind.Simple, source: Type, target: Type }
    | { kind: TypeMapKind.Array, sources: readonly Type[], targets: readonly Type[] | undefined }
    | { kind: TypeMapKind.Deferred, sources: readonly Type[], targets: (() => Type)[] }
    | { kind: TypeMapKind.Function, func: (t: Type) => Type, debugInfo?: () => string }
    | { kind: TypeMapKind.Composite | TypeMapKind.Merged, mapper1: TypeMapper, mapper2: TypeMapper };

これがどう作られるか。初期化経路が色々あって複雑だが、例えばこれが普通の mapper 生成か?

    function createTypeMapper(sources: readonly TypeParameter[], targets: readonly Type[] | undefined): TypeMapper {
        return sources.length === 1 ? makeUnaryTypeMapper(sources[0], targets ? targets[0] : anyType) : makeArrayTypeMapper(sources, targets);
    }

unary だけ見てみる

    function makeUnaryTypeMapper(source: Type, target: Type): TypeMapper {
        return Debug.attachDebugPrototypeIfDebug({ kind: TypeMapKind.Simple, source, target });
    }

なんか debugger に教えながら返してる?なにかのトレースのためだろうか。

instantiateTypeWorker の続き。もし Object だったら ObjectFlags をみる。参照かどうかでさらに分岐。

        if (flags & TypeFlags.Object) {
            const objectFlags = (type as ObjectType).objectFlags;
            if (objectFlags & (ObjectFlags.Reference | ObjectFlags.Anonymous | ObjectFlags.Mapped)) {
                if (objectFlags & ObjectFlags.Reference && !(type as TypeReference).node) {
                    const resolvedTypeArguments = (type as TypeReference).resolvedTypeArguments;
                    const newTypeArguments = instantiateTypes(resolvedTypeArguments, mapper);
                    return newTypeArguments !== resolvedTypeArguments ? createNormalizedTypeReference((type as TypeReference).target, newTypeArguments) : type;
                }
                if (objectFlags & ObjectFlags.ReverseMapped) {
                    return instantiateReverseMappedType(type as ReverseMappedType, mapper);
                }
                return getObjectTypeInstantiation(type as TypeReference | AnonymousType | MappedType, mapper, aliasSymbol, aliasTypeArguments);
            }
            return type;
        }

getObjectTypeInstantiation で、さらに実体を解決する?

ReverseMappedType がわからない。反変だろうか。

ここで TypeReference と ReverseMappedType の型を確認したほうがよさそう。

/**
 * Type references (ObjectFlags.Reference). When a class or interface has type parameters or
 * a "this" type, references to the class or interface are made using type references. The
 * typeArguments property specifies the types to substitute for the type parameters of the
 * class or interface and optionally includes an extra element that specifies the type to
 * substitute for "this" in the resulting instantiation. When no extra argument is present,
 * the type reference itself is substituted for "this". The typeArguments property is undefined
 * if the class or interface has no type parameters and the reference isn't specifying an
 * explicit "this" argument.
 */
export interface TypeReference extends ObjectType {
    target: GenericType;    // Type reference target
    node?: TypeReferenceNode | ArrayTypeNode | TupleTypeNode;
    /** @internal */
    mapper?: TypeMapper;
    /** @internal */
    resolvedTypeArguments?: readonly Type[];  // Resolved type reference type arguments
    /** @internal */
    literalType?: TypeReference;  // Clone of type with ObjectFlags.ArrayLiteral set
    /** @internal */
    cachedEquivalentBaseType?: Type; // Only set on references to class or interfaces with a single base type and no augmentations
}

コメントの deepl

  • 型参照(ObjectFlags.Reference)。クラスやインターフェイスが型パラメータを持つ場合、または
  • クラスやインターフェイスに "this "型がある場合、そのクラスやインターフェイスへの参照は、型参照を使用して行われます。このとき
  • typeArgumentsプロパティは、クラスまたはインターフェイスの型パラメーターを代用する型を指定します。
  • typeArgumentsプロパティは、クラスまたはインタフェースのタイプパラメータに代わるタイプを指定し、オプションとして、"this "に代わるタイプを指定する追加要素を含む。
  • typeArgumentsプロパティは,クラスやインタフェースの型パラメーターを代用する型を指定する。余分な引数が存在しない場合、
  • 型参照そのものが "this "に置換される。typeArgumentsプロパティは未定義である。
  • クラスまたはインターフェイスに型パラメータがなく、参照が明示的な "this "引数を指定していない場合、typeArgumentsプロパティは未定義です。
  • 明示的な "this "引数を指定していない場合、typeArgumentsプロパティは未定義です。

T<A>T シンボルの型は、即座に解決できないから参照として表現される感じかな。

                    const resolvedTypeArguments = (type as TypeReference).resolvedTypeArguments;
                    const newTypeArguments = instantiateTypes(resolvedTypeArguments, mapper);
                    return newTypeArguments !== resolvedTypeArguments ? createNormalizedTypeReference((type as TypeReference).target, newTypeArguments) : type;

Type 型の .resolvedTypeArguments から解決された型を拾って(どこで解決されたんだろう), さらに instantiateTypes で mapper と一緒に解決して(ここで何を解決したんだ?)、それが前の型と違ったらなんらかの normalize をする。(わからん)

飛ばす。 UnionOrIntersection の場合

        if (flags & TypeFlags.UnionOrIntersection) {
            const origin = type.flags & TypeFlags.Union ? (type as UnionType).origin : undefined;
            const types = origin && origin.flags & TypeFlags.UnionOrIntersection ? (origin as UnionOrIntersectionType).types : (type as UnionOrIntersectionType).types;
            const newTypes = instantiateTypes(types, mapper);
            if (newTypes === types && aliasSymbol === type.aliasSymbol) {
                return type;
            }
            const newAliasSymbol = aliasSymbol || type.aliasSymbol;
            const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper);
            return flags & TypeFlags.Intersection || origin && origin.flags & TypeFlags.Intersection ?
                getIntersectionType(newTypes, newAliasSymbol, newAliasTypeArguments) :
                getUnionType(newTypes, UnionReduction.Literal, newAliasSymbol, newAliasTypeArguments);
        }

union 型なら origin というプロパティがあって、その origin.types のフラグが UnionOrIntersectionType なら、その types を取る。そうでないなら type.types を直接使う。どういう意味だろう。

export interface UnionType extends UnionOrIntersectionType {
    /** @internal */
    resolvedReducedType?: Type;
    /** @internal */
    regularType?: UnionType;
    /** @internal */
    origin?: Type;  // Denormalized union, intersection, or index type in which union originates
    /** @internal */
    keyPropertyName?: __String;  // Property with unique unit type that exists in every object/intersection in union type
    /** @internal */
    constituentMap?: Map<TypeId, Type>;  // Constituents keyed by unit type discriminants
    /** @internal */
    arrayFallbackSignatures?: readonly Signature[]; // Special remapped signature list for unions of arrays
}

UnionOrIntersectionType の定義

export interface UnionOrIntersectionType extends Type {
    types: Type[];                    // Constituent types
    /** @internal */
    objectFlags: ObjectFlags;
    /** @internal */
    propertyCache?: SymbolTable;       // Cache of resolved properties
    /** @internal */
    propertyCacheWithoutObjectFunctionPropertyAugment?: SymbolTable; // Cache of resolved properties that does not augment function or object type properties
    /** @internal */
    resolvedProperties: Symbol[];
    /** @internal */
    resolvedIndexType: IndexType;
    /** @internal */
    resolvedStringIndexType: IndexType;
    /** @internal */
    resolvedBaseConstraint: Type;
}

instantiateTypes は instantiateType の複数版だと思っていたが違うのだろうか。

    function instantiateTypes(types: readonly Type[], mapper: TypeMapper): readonly Type[];
    function instantiateTypes(types: readonly Type[] | undefined, mapper: TypeMapper): readonly Type[] | undefined;
    function instantiateTypes(types: readonly Type[] | undefined, mapper: TypeMapper): readonly Type[] | undefined {
        return instantiateList<Type>(types, mapper, instantiateType);
    }

// ...

    function instantiateList<T>(items: readonly T[], mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[];
    function instantiateList<T>(items: readonly T[] | undefined, mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[] | undefined;
    function instantiateList<T>(items: readonly T[] | undefined, mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[] | undefined {
        if (items && items.length) {
            for (let i = 0; i < items.length; i++) {
                const item = items[i];
                const mapped = instantiator(item, mapper);
                if (item !== mapped) {
                    const result = i === 0 ? [] : items.slice(0, i);
                    result.push(mapped);
                    for (i++; i < items.length; i++) {
                        result.push(instantiator(items[i], mapper));
                    }
                    return result;
                }
            }
        }
        return items;
    }

const mapped = instantiator(item, mapper); で mapped でないなら items[i] を instantiator で作って push する。 type の型構造自体にオブジェクトプロパティーをflatten した型構造を含んでいる? わからんので飛ばす。

instantiator をみる。引き渡される一つがこのシンボル解決。

    function instantiateSymbol(symbol: Symbol, mapper: TypeMapper): Symbol {
        const links = getSymbolLinks(symbol);
        if (links.type && !couldContainTypeVariables(links.type)) {
            // If the type of the symbol is already resolved, and if that type could not possibly
            // be affected by instantiation, simply return the symbol itself.
            return symbol;
        }
        if (getCheckFlags(symbol) & CheckFlags.Instantiated) {
            // If symbol being instantiated is itself a instantiation, fetch the original target and combine the
            // type mappers. This ensures that original type identities are properly preserved and that aliases
            // always reference a non-aliases.
            symbol = links.target!;
            mapper = combineTypeMappers(links.mapper, mapper);
        }
        // Keep the flags from the symbol we're instantiating.  Mark that is instantiated, and
        // also transient so that we can just store data on it directly.
        const result = createSymbol(symbol.flags, symbol.escapedName, CheckFlags.Instantiated | getCheckFlags(symbol) & (CheckFlags.Readonly | CheckFlags.Late | CheckFlags.OptionalParameter | CheckFlags.RestParameter));
        result.declarations = symbol.declarations;
        result.parent = symbol.parent;
        result.links.target = symbol;
        result.links.mapper = mapper;
        if (symbol.valueDeclaration) {
            result.valueDeclaration = symbol.valueDeclaration;
        }
        if (links.nameType) {
            result.links.nameType = links.nameType;
        }
        return result;
    }

シンボルが Instantiated なら(型引数を含まないなら?) mapper = combineTypeMappers(links.mapper, mapper) と mapper を合成している。

新しく createSymbol してフラグを引き継ぎ、色々とフラグを立てる。その他のシンボルのパラメータを引き継ぐ。これによって、valueDeclaration も引き継いでるので、定義元ASTを追えるようになってる。

mizchimizchi

instantiateTypeWorker に戻る。

        if (flags & TypeFlags.Index) {
            return getIndexType(instantiateType((type as IndexType).type, mapper));
        }
        if (flags & TypeFlags.TemplateLiteral) {
            return getTemplateLiteralType((type as TemplateLiteralType).texts, instantiateTypes((type as TemplateLiteralType).types, mapper));
        }
        if (flags & TypeFlags.StringMapping) {
            return getStringMappingType((type as StringMappingType).symbol, instantiateType((type as StringMappingType).type, mapper));
        }
        if (flags & TypeFlags.IndexedAccess) {
            const newAliasSymbol = aliasSymbol || type.aliasSymbol;
            const newAliasTypeArguments = aliasSymbol ? aliasTypeArguments : instantiateTypes(type.aliasTypeArguments, mapper);
            return getIndexedAccessType(instantiateType((type as IndexedAccessType).objectType, mapper), instantiateType((type as IndexedAccessType).indexType, mapper), (type as IndexedAccessType).accessFlags, /*accessNode*/ undefined, newAliasSymbol, newAliasTypeArguments);
        }
        if (flags & TypeFlags.Conditional) {
            return getConditionalTypeInstantiation(type as ConditionalType, combineTypeMappers((type as ConditionalType).mapper, mapper), aliasSymbol, aliasTypeArguments);
        }
        if (flags & TypeFlags.Substitution) {
            const newBaseType = instantiateType((type as SubstitutionType).baseType, mapper);
            const newConstraint = instantiateType((type as SubstitutionType).constraint, mapper);
            // A substitution type originates in the true branch of a conditional type and can be resolved
            // to just the base type in the same cases as the conditional type resolves to its true branch
            // (because the base type is then known to satisfy the constraint).
            if (newBaseType.flags & TypeFlags.TypeVariable && isGenericType(newConstraint)) {
                return getSubstitutionType(newBaseType, newConstraint);
            }
            if (newConstraint.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(newBaseType), getRestrictiveInstantiation(newConstraint))) {
                return newBaseType;
            }
            return newBaseType.flags & TypeFlags.TypeVariable ? getSubstitutionType(newBaseType, newConstraint) : getIntersectionType([newConstraint, newBaseType]);
        }
        return type;

消耗してきたので飛ばすが、いろいろな LiteralType ごとの初期化が行われていそう...

mizchimizchi

たぶんここまでコントロールフローの部分と型引数の解決を読んだ。全然わかってないけど、グッと戻って checkSourceElement から再開する。

再帰下降してるのはわかってるので、なんか汎用的なやつをみたくなった。Block { ... } でも見てみる。

    function checkBlock(node: Block) {
        // Grammar checking for SyntaxKind.Block
        if (node.kind === SyntaxKind.Block) {
            checkGrammarStatementInAmbientContext(node);
        }
        if (isFunctionOrModuleBlock(node)) {
            const saveFlowAnalysisDisabled = flowAnalysisDisabled;
            forEach(node.statements, checkSourceElement);
            flowAnalysisDisabled = saveFlowAnalysisDisabled;
        }
        else {
            forEach(node.statements, checkSourceElement);
        }
        if (node.locals) {
            registerForUnusedIdentifiersCheck(node);
        }
    }

再帰下降しつつ local変数の解析をしているっぽい。

expression はどこで出てくるんだろう。 checkExpression で検索してみつけた。

    function checkExpression(node: Expression | QualifiedName, checkMode?: CheckMode, forceTuple?: boolean): Type {
        tracing?.push(tracing.Phase.Check, "checkExpression", { kind: node.kind, pos: node.pos, end: node.end, path: (node as TracingNode).tracingPath });
        const saveCurrentNode = currentNode;
        currentNode = node;
        instantiationCount = 0;
        const uninstantiatedType = checkExpressionWorker(node, checkMode, forceTuple);
        const type = instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode);
        if (isConstEnumObjectType(type)) {
            checkConstEnumAccess(node, type);
        }
        currentNode = saveCurrentNode;
        tracing?.pop();
        return type;
    }

やっぱ ~Worker ってみると重い処理を覚悟するよね

    function checkExpressionWorker(node: Expression | QualifiedName, checkMode: CheckMode | undefined, forceTuple?: boolean): Type {
        const kind = node.kind;
        if (cancellationToken) {
            // Only bother checking on a few construct kinds.  We don't want to be excessively
            // hitting the cancellation token on every node we check.
            switch (kind) {
                case SyntaxKind.ClassExpression:
                case SyntaxKind.FunctionExpression:
                case SyntaxKind.ArrowFunction:
                    cancellationToken.throwIfCancellationRequested();
            }
        }
        switch (kind) {
            case SyntaxKind.Identifier:
                return checkIdentifier(node as Identifier, checkMode);
            case SyntaxKind.PrivateIdentifier:
                return checkPrivateIdentifierExpression(node as PrivateIdentifier);
            case SyntaxKind.ThisKeyword:
                return checkThisExpression(node);

node ごとの処理。比較的単純そうな checkIdentifier をみてみる。

全然軽くなかった。起源にして頂点みたいなノリか(伝われ)

    function checkIdentifier(node: Identifier, checkMode: CheckMode | undefined): Type {
        if (isThisInTypeQuery(node)) {
            return checkThisExpression(node);
        }

        const symbol = getResolvedSymbol(node);
        if (symbol === unknownSymbol) {
            return errorType;
        }

        // As noted in ECMAScript 6 language spec, arrow functions never have an arguments objects.
        // Although in down-level emit of arrow function, we emit it using function expression which means that
        // arguments objects will be bound to the inner object; emitting arrow function natively in ES6, arguments objects
        // will be bound to non-arrow function that contain this arrow function. This results in inconsistent behavior.
        // To avoid that we will give an error to users if they use arguments objects in arrow function so that they
        // can explicitly bound arguments objects
        if (symbol === argumentsSymbol) {
            if (isInPropertyInitializerOrClassStaticBlock(node)) {

ちょくちょく TypeQuery って概念を定期的に見かけるので、型を確認する

export interface TypeQueryNode extends NodeWithTypeArguments {
    readonly kind: SyntaxKind.TypeQuery;
    readonly exprName: EntityName;
}
//...
export interface NodeWithTypeArguments extends TypeNode {
    readonly typeArguments?: NodeArray<TypeNode>;
}

型引数付きのシンボルかな T<A> みたいな。

isThisInTypeQuery はなんだろう。

/** @internal */
export function isThisInTypeQuery(node: Node): boolean {
    if (!isThisIdentifier(node)) {
        return false;
    }

    while (isQualifiedName(node.parent) && node.parent.left === node) {
        node = node.parent;
    }

    return node.parent.kind === SyntaxKind.TypeQuery;
}

自分が this かつ親が QualifiedName で親の左辺が自分の場合...?

export interface QualifiedName extends Node, FlowContainer {
    readonly kind: SyntaxKind.QualifiedName;
    readonly left: EntityName;
    readonly right: Identifier;
}

こういうやつか?

const f = function(){
  this
}

いや、違いそうだな...飛ばそう

getResolvedSymbol を読む。

      const symbol = getResolvedSymbol(node);

// ...
    function getResolvedSymbol(node: Identifier): Symbol {
        const links = getNodeLinks(node);
        if (!links.resolvedSymbol) {
            links.resolvedSymbol = !nodeIsMissing(node) &&
                resolveName(
                    node,
                    node.escapedText,
                    SymbolFlags.Value | SymbolFlags.ExportValue,
                    getCannotFindNameDiagnosticForName(node),
                    node,
                    !isWriteOnlyAccess(node),
                    /*excludeGlobals*/ false) || unknownSymbol;
        }
        return links.resolvedSymbol;
    }

identifier から Symbol を解決する。たしかに変数空間と型のシンボルの空間は別って認識持たないと駄目だな。

node.links.resolvedSymbol が未初期化の場合、 resolveName して初期化

    /**
     * Resolve a given name for a given meaning at a given location. An error is reported if the name was not found and
     * the nameNotFoundMessage argument is not undefined. Returns the resolved symbol, or undefined if no symbol with
     * the given name can be found.
     *
     * @param nameNotFoundMessage If defined, we will report errors found during resolve.
     * @param isUse If true, this will count towards --noUnusedLocals / --noUnusedParameters.
     */
    function resolveName(
        location: Node | undefined,
        name: __String,
        meaning: SymbolFlags,
        nameNotFoundMessage: DiagnosticMessage | undefined,
        nameArg: __String | Identifier | undefined,
        isUse: boolean,
        excludeGlobals = false,
        getSpellingSuggestions = true): Symbol | undefined {
        return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSpellingSuggestions, getSymbol);
    }

    function resolveNameHelper(
        location: Node | undefined,
        name: __String,
        meaning: SymbolFlags,
        nameNotFoundMessage: DiagnosticMessage | undefined,
        nameArg: __String | Identifier | undefined,
        isUse: boolean,
        excludeGlobals: boolean,
        getSpellingSuggestions: boolean,
        lookup: typeof getSymbol): Symbol | undefined {
        const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location
        let result: Symbol | undefined;
        let lastLocation: Node | undefined;
//...

ここは Worker じゃなくて Helper だ。何の違いだろう。resolveNameHelper の実装は結構長い。
getSymbol から先に見たほうがいいかな。

    function getSymbol(symbols: SymbolTable, name: __String, meaning: SymbolFlags): Symbol | undefined {
        if (meaning) {
            const symbol = getMergedSymbol(symbols.get(name));
            if (symbol) {
                Debug.assert((getCheckFlags(symbol) & CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here.");
                if (symbol.flags & meaning) {
                    return symbol;
                }
                if (symbol.flags & SymbolFlags.Alias) {
                    const targetFlags = getAllSymbolFlags(symbol);
                    // `targetFlags` will be `SymbolFlags.All` if an error occurred in alias resolution; this avoids cascading errors
                    if (targetFlags & meaning) {
                        return symbol;
                    }
                }
            }
        }
        // return undefined if we can't find a symbol.
    }

これは Compiler API で見覚えある! checker.getSymbolsInScope(...) で渡すインターフェースに近い。SymbolTable から meaning の修理で絞り込んで Symbol 一覧を得るやつ。

    function getMergedSymbol(symbol: Symbol): Symbol;
    function getMergedSymbol(symbol: Symbol | undefined): Symbol | undefined;
    function getMergedSymbol(symbol: Symbol | undefined): Symbol | undefined {
        let merged: Symbol;
        return symbol && symbol.mergeId && (merged = mergedSymbols[symbol.mergeId]) ? merged : symbol;
    }

// ...
    var mergedSymbols: Symbol[] = [];

checker のスコープから mergeId を更新しつつ symbol を返す。

続き。

                if (symbol.flags & meaning) {
                    return symbol;
                }
                if (symbol.flags & SymbolFlags.Alias) {
                    const targetFlags = getAllSymbolFlags(symbol);
                    // `targetFlags` will be `SymbolFlags.All` if an error occurred in alias resolution; this avoids cascading errors
                    if (targetFlags & meaning) {
                        return symbol;
                    }
                }

symbol.flags が meaning を満たせば symbol を返す。Alias の場合、 Alias のシンボルを解決して、同じく meaning を満たすか確認する。getSymbol 終わり。ここは難しくないな。

mizchimizchi

resolveNameHelper に戻る。

ローカル変数が多い。

        const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location
        let result: Symbol | undefined;
        let lastLocation: Node | undefined;
        let lastSelfReferenceLocation: Declaration | undefined;
        let propertyWithInvalidInitializer: PropertyDeclaration | undefined;
        let associatedDeclarationForContainingInitializerOrBindingName: ParameterDeclaration | BindingElement | undefined;
        let withinDeferredContext = false;
        const errorLocation = location;
        let grandparent: Node;
        let isInExternalModule = false;

あー identifier 簡単そうでいいじゃんって読み始めたけど、実際は何の型の一部の identifier なのかで推論結果が分岐するのか。それは複雑だ。

珍しい、ラベル構文だ

        loop: while (location) {
            if (name === "const" && isConstAssertion(location)) {

識別子として const の禁止。それはそう。

これもある種のAST下るための再帰下降になってる。

            if (isModuleDeclaration(location) && lastLocation && location.name === lastLocation) {
                // If this is the name of a namespace, skip the parent since it will have is own locals that could
                // conflict.
                lastLocation = location;
                location = location.parent;
            }
            // Locals of a source file are not in scope (because they get merged into the global symbol table)
            if (canHaveLocals(location) && location.locals && !isGlobalSourceFile(location)) {
                if (result = lookup(location.locals, name, meaning)) {
                    let useResult = true;
                    if (isFunctionLike(location) && lastLocation && lastLocation !== (location as FunctionLikeDeclaration).body) {
                        // symbol lookup restrictions for function-like declarations
                        // - Type parameters of a function are in scope in the entire function declaration, including the parameter
                        //   list and return type. However, local types are only in scope in the function body.
                        // - parameters are only in the scope of function body
                        // This restriction does not apply to JSDoc comment types because they are parented
                        // at a higher level than type parameters would normally be
                        if (meaning & result.flags & SymbolFlags.Type && lastLocation.kind !== SyntaxKind.JSDoc) {

GlobalScope の場合は、ローカル変数ではなくグローバル変数になる。

canHaveLocals() を見る。

/** @internal */
export function canHaveLocals(node: Node): node is HasLocals {
    switch (node.kind) {
        case SyntaxKind.ArrowFunction:
        case SyntaxKind.Block:
        case SyntaxKind.CallSignature:
        case SyntaxKind.CaseBlock:
        case SyntaxKind.CatchClause:
        case SyntaxKind.ClassStaticBlockDeclaration:
        case SyntaxKind.ConditionalType:
        case SyntaxKind.Constructor:
        case SyntaxKind.ConstructorType:
        case SyntaxKind.ConstructSignature:
        case SyntaxKind.ForStatement:
        case SyntaxKind.ForInStatement:
        case SyntaxKind.ForOfStatement:
        case SyntaxKind.FunctionDeclaration:
        case SyntaxKind.FunctionExpression:
        case SyntaxKind.FunctionType:
        case SyntaxKind.GetAccessor:
        case SyntaxKind.IndexSignature:
        case SyntaxKind.JSDocCallbackTag:
        case SyntaxKind.JSDocEnumTag:
        case SyntaxKind.JSDocFunctionType:
        case SyntaxKind.JSDocSignature:
        case SyntaxKind.JSDocTypedefTag:
        case SyntaxKind.MappedType:
        case SyntaxKind.MethodDeclaration:
        case SyntaxKind.MethodSignature:
        case SyntaxKind.ModuleDeclaration:
        case SyntaxKind.SetAccessor:
        case SyntaxKind.SourceFile:
        case SyntaxKind.TypeAliasDeclaration:
            return true;
        default:
            return false;
    }
}

if (result = lookup(location.locals, name, meaning)) {

ローカル変数から lookup する。 lookup は typeof getSymbol でそのまま getSymbol である感じか。

                        if (meaning & result.flags & SymbolFlags.Variable) {
                            // expression inside parameter will lookup as normal variable scope when targeting es2015+
                            if (useOuterVariableScopeInParameter(result, location, lastLocation)) {
                                useResult = false;
                            }
                            else if (result.flags & SymbolFlags.FunctionScopedVariable) {
                                // parameters are visible only inside function body, parameter list and return type
                                // technically for parameter list case here we might mix parameters and variables declared in function,
                                // however it is detected separately when checking initializers of parameters
                                // to make sure that they reference no variables declared after them.
                                useResult =
                                    lastLocation.kind === SyntaxKind.Parameter ||
                                    (
                                        lastLocation === (location as FunctionLikeDeclaration).type &&
                                        !!findAncestor(result.valueDeclaration, isParameter)
                                    );
                            }
                        }

SymbolFlags.Varible

findAncestor(result.valueDeclaration, isParameter) で親方向の Paramter を探索

/**
 * Iterates through the parent chain of a node and performs the callback on each parent until the callback
 * returns a truthy value, then returns that value.
 * If no such value is found, it applies the callback until the parent pointer is undefined or the callback returns "quit"
 * At that point findAncestor returns undefined.
 */
export function findAncestor<T extends Node>(node: Node | undefined, callback: (element: Node) => element is T): T | undefined;
export function findAncestor(node: Node | undefined, callback: (element: Node) => boolean | "quit"): Node | undefined;
export function findAncestor(node: Node | undefined, callback: (element: Node) => boolean | "quit"): Node | undefined {
    while (node) {
        const result = callback(node);
        if (result === "quit") {
            return undefined;
        }
        else if (result) {
            return node;
        }
        node = node.parent;
    }
    return undefined;
}

///...

// TODO(rbuckton): Rename to 'isParameterDeclaration'
export function isParameter(node: Node): node is ParameterDeclaration {
    return node.kind === SyntaxKind.Parameter;
}
mizchimizchi

関数スコープなら function f(param: number) { param; } で param を見つけてるやつかな。

                    else if (location.kind === SyntaxKind.ConditionalType) {
                        // A type parameter declared using 'infer T' in a conditional type is visible only in
                        // the true branch of the conditional type.
                        useResult = lastLocation === (location as ConditionalTypeNode).trueType;
                    }

                    if (useResult) {
                        break loop;
                    }
                    else {
                        result = undefined;
                    }

気持ちがわかってきた。 AST の node.parent をただ辿るんじゃなくて location になりうる特定の node だけがこのループの親になりうる感じ。

遅延されたコンテキストかどうか

            withinDeferredContext = withinDeferredContext || getIsDeferredContext(location, lastLocation);
            switch (location.kind) {
                case SyntaxKind.SourceFile:
                    if (!isExternalOrCommonJsModule(location as SourceFile)) break;
                    isInExternalModule = true;
                    // falls through

SourceFile までたどり切ったら isInExternalModule = true にする

                case SyntaxKind.ModuleDeclaration:
                    const moduleExports = getSymbolOfDeclaration(location as SourceFile | ModuleDeclaration)?.exports || emptySymbols;
                    if (location.kind === SyntaxKind.SourceFile || (isModuleDeclaration(location) && location.flags & NodeFlags.Ambient && !isGlobalScopeAugmentation(location))) {

                        // It's an external module. First see if the module has an export default and if the local
                        // name of that export default matches.
                        if (result = moduleExports.get(InternalSymbolName.Default)) {
                            const localSymbol = getLocalSymbolForExportDefault(result);
                            if (localSymbol && (result.flags & meaning) && localSymbol.escapedName === name) {
                                break loop;
                            }
                            result = undefined;
                        }

ModuleDeclaration まで辿ったら location.exports を見に行く。 Ambient かつ GlobalScope でないなら、

default が LocalSymbol として名前がついてたらそれを解決する。

                        // Because of module/namespace merging, a module's exports are in scope,
                        // yet we never want to treat an export specifier as putting a member in scope.
                        // Therefore, if the name we find is purely an export specifier, it is not actually considered in scope.
                        // Two things to note about this:
                        //     1. We have to check this without calling getSymbol. The problem with calling getSymbol
                        //        on an export specifier is that it might find the export specifier itself, and try to
                        //        resolve it as an alias. This will cause the checker to consider the export specifier
                        //        a circular alias reference when it might not be.
                        //     2. We check === SymbolFlags.Alias in order to check that the symbol is *purely*
                        //        an alias. If we used &, we'd be throwing out symbols that have non alias aspects,
                        //        which is not the desired behavior.
                        const moduleExport = moduleExports.get(name);
                        if (moduleExport &&
                            moduleExport.flags === SymbolFlags.Alias &&
                            (getDeclarationOfKind(moduleExport, SyntaxKind.ExportSpecifier) || getDeclarationOfKind(moduleExport, SyntaxKind.NamespaceExport))) {
                            break;
                        }
                   // モジュールと名前空間の統合により、モジュールのエクスポートはスコープに入ります、
                   // しかし、エクスポート指定子をメンバをスコープに入れるものとして扱うことは決してしたくありません。
                   // したがって、見つけた名前が純粋にエクスポート指定子である場合、実際にはスコープ内にあるとは考えられません。
                   // これについては、2つの注意点があります:
                   // 1. getSymbolを呼び出さずにこれをチェックする必要があります。輸出指定子に対して getSymbol を呼び出すと、次のような問題があります。
                  // を呼び出すと、エクスポート指定子そのものを見つけ、それをエイリアスとして解決しようとする可能性があることです。
                   // それをエイリアスとして解決しようとする可能性があることです。このため、チェッカーはエクスポート指定子をエイリアス参照とみなします。
                   // このため、チェッカーはエクスポート指定子を循環エイリアス参照とみなし、そうでない可能性もあります。
                   // 2.シンボルがエイリアスであることを確認するために、=== SymbolFlags.Alias をチェックします。
                   // エイリアスであることを確認します。もし&を使用すると、エイリアスでない側面を持つシンボルを捨ててしまうことになります、
                   // これは望ましい動作ではありません。

import default alias を lookup

                    // ES6 exports are also visible locally (except for 'default'), but commonjs exports are not (except typedefs)
                    if (name !== InternalSymbolName.Default && (result = lookup(moduleExports, name, meaning & SymbolFlags.ModuleMember))) {
                        if (isSourceFile(location) && location.commonJsModuleIndicator && !result.declarations?.some(isJSDocTypeAlias)) {
                            result = undefined;
                        }
                        else {
                            break loop;
                        }
                    }

だいたいわかった。各 ASTパターンに応じて lookup する場所が変わる。これは直感的に挙動がわかる。

もし result が空で nameNotFoundMessage なら、それは遅延解決する

        if (!result) {
            if (nameNotFoundMessage) {
                addLazyDiagnostic(() => {
                    if (!errorLocation ||
                        errorLocation.parent.kind !== SyntaxKind.JSDocLink &&
                        !checkAndReportErrorForMissingPrefix(errorLocation, name, nameArg!) && // TODO: GH#18217
                        !checkAndReportErrorForInvalidInitializer() &&
                        !checkAndReportErrorForExtendingInterface(errorLocation) &&

もし名前が見つからなかったら似ている名前を提案する。

                        // then spelling suggestions
                        if (!suggestedLib && getSpellingSuggestions && suggestionCount < maximumSuggestionCount) {
                            suggestion = getSuggestedSymbolForNonexistentSymbol(originalLocation, name, meaning);
                            const isGlobalScopeAugmentationDeclaration = suggestion?.valueDeclaration && isAmbientModule(suggestion.valueDeclaration) && isGlobalScopeAugmentation(suggestion.valueDeclaration);
                            if (isGlobalScopeAugmentationDeclaration) {
                                suggestion = undefined;
                            }
                            if (suggestion) {
                                const suggestionName = symbolToString(suggestion);
                                const isUncheckedJS = isUncheckedJSSuggestion(originalLocation, suggestion, /*excludeClasses*/ false);
                                const message = meaning === SymbolFlags.Namespace || nameArg && typeof nameArg !== "string" && nodeIsSynthesized(nameArg) ? Diagnostics.Cannot_find_namespace_0_Did_you_mean_1
                                    : isUncheckedJS ? Diagnostics.Could_not_find_name_0_Did_you_mean_1
                                    : Diagnostics.Cannot_find_name_0_Did_you_mean_1;
                                const diagnostic = createError(errorLocation, message, diagnosticName(nameArg!), suggestionName);
                                addErrorOrSuggestion(!isUncheckedJS, diagnostic);
                                if (suggestion.valueDeclaration) {
                                    addRelatedInfo(
                                        diagnostic,
                                        createDiagnosticForNode(suggestion.valueDeclaration, Diagnostics._0_is_declared_here, suggestionName)
                                    );
                                }
                            }
                        }
                        // And then fall back to unspecified "not found"
                        if (!suggestion && !suggestedLib && nameArg) {
                            error(errorLocation, nameNotFoundMessage, diagnosticName(nameArg));
                        }
                        suggestionCount++;

もし見つからない遅延しても見つからなかった時、追加のエラーを報告する。

        // Perform extra checks only if error reporting was requested
        if (nameNotFoundMessage) {
            addLazyDiagnostic(() => {
                // Only check for block-scoped variable if we have an error location and are looking for the
                // name with variable meaning
                //      For example,
                //          declare module foo {
                //              interface bar {}
                //          }
                //      const foo/*1*/: foo/*2*/.bar;
                // The foo at /*1*/ and /*2*/ will share same symbol with two meanings:
                // block-scoped variable and namespace module. However, only when we
                // try to resolve name in /*1*/ which is used in variable position,
                // we want to check for block-scoped
                if (errorLocation &&
                    (meaning & SymbolFlags.BlockScopedVariable ||
                     ((meaning & SymbolFlags.Class || meaning & SymbolFlags.Enum) && (meaning & SymbolFlags.Value) === SymbolFlags.Value))) {
                    const exportOrLocalSymbol = getExportSymbolOfValueSymbolIfExported(result!);
                    if (exportOrLocalSymbol.flags & SymbolFlags.BlockScopedVariable || exportOrLocalSymbol.flags & SymbolFlags.Class || exportOrLocalSymbol.flags & SymbolFlags.Enum) {
                        checkResolvedBlockScopedVariable(exportOrLocalSymbol, errorLocation);
                    }
                }

                // If we're in an external module, we can't reference value symbols created from UMD export declarations
                if (result && isInExternalModule && (meaning & SymbolFlags.Value) === SymbolFlags.Value && !(originalLocation!.flags & NodeFlags.JSDoc)) {
                    const merged = getMergedSymbol(result);
                    if (length(merged.declarations) && every(merged.declarations, d => isNamespaceExportDeclaration(d) || isSourceFile(d) && !!d.symbol.globalExports)) {
                        errorOrSuggestion(!compilerOptions.allowUmdGlobalAccess, errorLocation!, Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead, unescapeLeadingUnderscores(name));
                    }
                }

                // If we're in a parameter initializer or binding name, we can't reference the values of the parameter whose initializer we're within or parameters to the right
                if (result && associatedDeclarationForContainingInitializerOrBindingName && !withinDeferredContext && (meaning & SymbolFlags.Value) === SymbolFlags.Value) {
                    const candidate = getMergedSymbol(getLateBoundSymbol(result));
                    const root = (getRootDeclaration(associatedDeclarationForContainingInitializerOrBindingName) as ParameterDeclaration);
                    // A parameter initializer or binding pattern initializer within a parameter cannot refer to itself
                    if (candidate === getSymbolOfDeclaration(associatedDeclarationForContainingInitializerOrBindingName)) {
                        error(errorLocation, Diagnostics.Parameter_0_cannot_reference_itself, declarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.name));
                    }
                    // And it cannot refer to any declarations which come after it
                    else if (candidate.valueDeclaration && candidate.valueDeclaration.pos > associatedDeclarationForContainingInitializerOrBindingName.pos && root.parent.locals && lookup(root.parent.locals, candidate.escapedName, meaning) === candidate) {
                        error(errorLocation, Diagnostics.Parameter_0_cannot_reference_identifier_1_declared_after_it, declarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.name), declarationNameToString(errorLocation as Identifier));
                    }
                }
                if (result && errorLocation && meaning & SymbolFlags.Value && result.flags & SymbolFlags.Alias && !(result.flags & SymbolFlags.Value) && !isValidTypeOnlyAliasUseSite(errorLocation)) {
                    const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(result, SymbolFlags.Value);
                    if (typeOnlyDeclaration) {
                        const message = typeOnlyDeclaration.kind === SyntaxKind.ExportSpecifier || typeOnlyDeclaration.kind === SyntaxKind.ExportDeclaration || typeOnlyDeclaration.kind === SyntaxKind.NamespaceExport
                            ? Diagnostics._0_cannot_be_used_as_a_value_because_it_was_exported_using_export_type
                            : Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type;
                        const unescapedName = unescapeLeadingUnderscores(name);
                        addTypeOnlyDeclarationRelatedInfo(
                            error(errorLocation, message, unescapedName),
                            typeOnlyDeclaration,
                            unescapedName);
                    }
                }
            });
        }
        return result;