1. 概览
我们在上一篇章中主要讲了 关于 绑定器相关的之后 在上面的篇章讲完之后 相当于 我们完成了如下的流程
源代码 -> 扫描器 -> token流 -> 解析器 -> AST ->绑定器 -> Symbol(符号)
接下来 主要讲一下 typescript 代码报错功能的实现 主要是依赖检查器实现。
1.1 程序对检查器的使用
检查器是由程序初始化 下面是调用栈示意
program.getTypeChecker ->ts.createTypeChecker(检查器中)->initializeTypeChecker(检查器中) ->for each SourceFile `ts.bindSourceFile`(绑定器中)// 接着for each SourceFile `ts.mergeSymbolTable`(检查器中)
根据上面的调用栈 可以看出 在 initializeTypeChecker 的时候会调用 绑定器的 bindSourceFile 以及 检查器本身的 mergeSymbolTable
2. 初始化 类型检查
2.1 initializeTypeChecker 验证调用栈的正确与否
function initializeTypeChecker() {// Bind all source files and propagate errorsfor (const file of host.getSourceFiles()) {bindSourceFile(file, compilerOptions);}// Initialize global symbol tablelet augmentations: LiteralExpression[][];for (const file of host.getSourceFiles()) {if (!isExternalOrCommonJsModule(file)) {mergeSymbolTable(globals, file.locals);}if (file.patternAmbientModules && file.patternAmbientModules.length) {patternAmbientModules = concatenate(patternAmbientModules, file.patternAmbientModules);}if (file.moduleAugmentations.length) {(augmentations || (augmentations = [])).push(file.moduleAugmentations);}if (file.symbol && file.symbol.globalExports) {// Merge in UMD exports with first-in-wins semantics (see #9771)const source = file.symbol.globalExports;source.forEach((sourceSymbol, id) => {if (!globals.has(id)) {globals.set(id, sourceSymbol);}});}}}
查看检查器中的源码, 我们确实验证了上述调用栈的过程。先调用 bindSourceFile 再调用了 mergeSymbolTable 。
2.2 mergeSymbolTable
在上一篇章中 我们对绑定器 也就是 bindSourceFile 进行了一个分析 最后给每个节点都创建了一个符号, 将每个节点连接成一个相关的类型系统。
function mergeSymbolTable(target: SymbolTable, source: SymbolTable) {source.forEach((sourceSymbol, id) => {let targetSymbol = target.get(id);if (!targetSymbol) {target.set(id, sourceSymbol);}else {if (!(targetSymbol.flags & SymbolFlags.Transient)) {targetSymbol = cloneSymbol(targetSymbol);target.set(id, targetSymbol);}mergeSymbol(targetSymbol, sourceSymbol);}});}
看上面 mergeSymbolTable 的代码 我们不难发现 mergeSymbolTable 主要做的事情 就是 将所有的global 符号合并到 let globals: SymbolTable = {} 符号表中。往后的类型检查都 统一在global上校验即可。
function mergeSymbol(target: Symbol, source: Symbol) {if (!(target.flags & getExcludedSymbolFlags(source.flags))) {if (source.flags & SymbolFlags.ValueModule && target.flags & SymbolFlags.ValueModule && target.constEnumOnlyModule && !source.constEnumOnlyModule) {// reset flag when merging instantiated module into value module that has only const enumstarget.constEnumOnlyModule = false;}target.flags |= source.flags;if (source.valueDeclaration &&(!target.valueDeclaration ||(target.valueDeclaration.kind === SyntaxKind.ModuleDeclaration && source.valueDeclaration.kind !== SyntaxKind.ModuleDeclaration))) {// other kinds of value declarations take precedence over modulestarget.valueDeclaration = source.valueDeclaration;}addRange(target.declarations, source.declarations);if (source.members) {if (!target.members) target.members = createMap<Symbol>();mergeSymbolTable(target.members, source.members);}if (source.exports) {if (!target.exports) target.exports = createMap<Symbol>();mergeSymbolTable(target.exports, source.exports);}recordMergedSymbol(target, source);}else if (target.flags & SymbolFlags.NamespaceModule) {error(getNameOfDeclaration(source.declarations[0]), Diagnostics.Cannot_augment_module_0_with_value_exports_because_it_resolves_to_a_non_module_entity, symbolToString(target));}else {const message = target.flags & SymbolFlags.BlockScopedVariable || source.flags & SymbolFlags.BlockScopedVariable? Diagnostics.Cannot_redeclare_block_scoped_variable_0 : Diagnostics.Duplicate_identifier_0;forEach(source.declarations, node => {error(getNameOfDeclaration(node) || node, message, symbolToString(source));});forEach(target.declarations, node => {error(getNameOfDeclaration(node) || node, message, symbolToString(source));});}}
3. 类型检查
真正的类型检查会在调用 getDiagnostics 时才会发生。 该函数被调用时 (比如由 Program.emit 请求),检查器返回一个 EmitResolver(由程序调用检查器的 getEmitResolver 函数得到)。 EmitResolver 是 createTypeChecker 的一个本地函数的集合。
下面是 该过程直到 checkSourceFile 的调用栈 ( checkSourceFile 是 createTypeChecker 的一个本地函数 )
program.emit ->emitWorker (program local) ->createTypeChecker.getEmitResolver ->// 第一次调用下面的几个 createTypeChecker 的本地函数call getDiagnostics ->getDiagnosticsWorker ->checkSourceFile// 接着return resolver// 通过对本地函数 createResolver() 的调用,resolver 已在 createTypeChecker 中初始化。
接下里 从 getDiagnostics 开始做代码分析
3.1 getDiagnostics
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;}}
3.2 getDiagnosticsWorker
function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {throwIfNonDiagnosticsProducing();if (sourceFile) {// Some global diagnostics are deferred until they are needed and// may not be reported in the firt call to getGlobalDiagnostics.// We should catch these changes and report them.const previousGlobalDiagnostics = diagnostics.getGlobalDiagnostics();const previousGlobalDiagnosticsSize = previousGlobalDiagnostics.length;checkSourceFile(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// getDiagnosticsforEach(host.getSourceFiles(), checkSourceFile);return diagnostics.getDiagnostics();}
把所有不相干的东西都去掉, 我们发现了一个小递归, 如果sourceFile存在那么进行checkSourceFile 操作, 否则就进入diagnostics.getDiagnostics() 再来一遍。
3.3 checkSourceFile
function checkSourceFile(node: SourceFile) {performance.mark("beforeCheck");checkSourceFileWorker(node);performance.mark("afterCheck");performance.measure("Check", "beforeCheck", "afterCheck");}
3.4 checkSourceFileWorker
// 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 skipLibCheck is enabled, skip type checking if file is a declaration file.// If skipDefaultLibCheck is enabled, skip type checking if file contains a// '/// <reference no-default-lib="true"/>' directive.if (compilerOptions.skipLibCheck && node.isDeclarationFile || compilerOptions.skipDefaultLibCheck && node.hasNoDefaultLib) {return;}// Grammar checkingcheckGrammarSourceFile(node);potentialThisCollisions.length = 0;potentialNewTargetCollisions.length = 0;deferredNodes = [];deferredUnusedIdentifierNodes = produceDiagnostics && noUnusedIdentifiers ? [] : undefined;forEach(node.statements, checkSourceElement);checkDeferredNodes();if (isExternalModule(node)) {registerForUnusedIdentifiersCheck(node);}if (!node.isDeclarationFile) {checkUnusedIdentifiers();}deferredNodes = undefined;deferredUnusedIdentifierNodes = undefined;if (isExternalOrCommonJsModule(node)) {checkExternalModuleExports(node);}if (potentialThisCollisions.length) {forEach(potentialThisCollisions, checkIfThisIsCapturedInEnclosingScope);potentialThisCollisions.length = 0;}if (potentialNewTargetCollisions.length) {forEach(potentialNewTargetCollisions, checkIfNewTargetIsCapturedInEnclosingScope);potentialNewTargetCollisions.length = 0;}links.flags |= NodeCheckFlags.TypeChecked;}}
仔细阅读以上代码, 我们发现在checkSourceFileWorker函数内有各种各样的check操作比如: checkGrammarSourceFile 、 checkDeferredNodes、 registerForUnusedIdentifiersCheck … 这不就是我们苦苦探寻的么?我们随便调一个继续深究下去.
3.5 checkGrammarSourceFile
function checkGrammarSourceFile(node: SourceFile): boolean {return isInAmbientContext(node) && checkGrammarTopLevelElementsForRequiredDeclareModifier(node);}
3.6 checkGrammarTopLevelElementsForRequiredDeclareModifier
function checkGrammarTopLevelElementsForRequiredDeclareModifier(file: SourceFile): boolean {for (const decl of file.statements) {if (isDeclaration(decl) || decl.kind === SyntaxKind.VariableStatement) {if (checkGrammarTopLevelElementForRequiredDeclareModifier(decl)) {return true;}}}}
3.7 checkGrammarTopLevelElementForRequiredDeclareModifier
function checkGrammarTopLevelElementForRequiredDeclareModifier(node: Node): boolean {// A declare modifier is required for any top level .d.ts declaration except export=, export default, export as namespace// interfaces and imports categories://// DeclarationElement:// ExportAssignment// export_opt InterfaceDeclaration// export_opt TypeAliasDeclaration// export_opt ImportDeclaration// export_opt ExternalImportDeclaration// export_opt AmbientDeclaration//// TODO: The spec needs to be amended to reflect this grammar.if (node.kind === SyntaxKind.InterfaceDeclaration ||node.kind === SyntaxKind.TypeAliasDeclaration ||node.kind === SyntaxKind.ImportDeclaration ||node.kind === SyntaxKind.ImportEqualsDeclaration ||node.kind === SyntaxKind.ExportDeclaration ||node.kind === SyntaxKind.ExportAssignment ||node.kind === SyntaxKind.NamespaceExportDeclaration ||getModifierFlags(node) & (ModifierFlags.Ambient | ModifierFlags.Export | ModifierFlags.Default)) {return false;}return grammarErrorOnFirstToken(node, Diagnostics.A_declare_modifier_is_required_for_a_top_level_declaration_in_a_d_ts_file);}
3.8 grammarErrorOnFirstToken
function grammarErrorOnFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean {const sourceFile = getSourceFileOfNode(node);if (!hasParseDiagnostics(sourceFile)) {const span = getSpanOfTokenAtPosition(sourceFile, node.pos);diagnostics.add(createFileDiagnostic(sourceFile, span.start, span.length, message, arg0, arg1, arg2));return true;}}
3.9 createFileDiagnostic
export function createFileDiagnostic(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ...args: (string | number)[]): Diagnostic;export function createFileDiagnostic(file: SourceFile, start: number, length: number, message: DiagnosticMessage): Diagnostic {const end = start + length;Debug.assert(start >= 0, "start must be non-negative, is " + start);Debug.assert(length >= 0, "length must be non-negative, is " + length);if (file) {Debug.assert(start <= file.text.length, `start must be within the bounds of the file. ${start} > ${file.text.length}`);Debug.assert(end <= file.text.length, `end must be the bounds of the file. ${end} > ${file.text.length}`);}let text = getLocaleSpecificMessage(message);if (arguments.length > 4) {text = formatStringFromArgs(text, arguments, 4);}return {file,start,length,messageText: text,category: message.category,code: message.code,};}
终于结束了, 我们发现最后的类型校验都会通过Debug.assert 函数给抛出来。
4. 流程图

