1. 概览

我们在上一篇章中主要讲了 关于 绑定器相关的之后 在上面的篇章讲完之后 相当于 我们完成了如下的流程

  1. 源代码 -> 扫描器 -> token -> 解析器 -> AST ->绑定器 -> Symbol(符号)

接下来 主要讲一下 typescript 代码报错功能的实现 主要是依赖检查器实现。

1.1 程序对检查器的使用

检查器是由程序初始化 下面是调用栈示意

  1. program.getTypeChecker ->
  2. ts.createTypeChecker(检查器中)->
  3. initializeTypeChecker(检查器中) ->
  4. for each SourceFile `ts.bindSourceFile`(绑定器中)
  5. // 接着
  6. for each SourceFile `ts.mergeSymbolTable`(检查器中)

根据上面的调用栈 可以看出 在 initializeTypeChecker 的时候会调用 绑定器的 bindSourceFile 以及 检查器本身的 mergeSymbolTable

2. 初始化 类型检查

2.1 initializeTypeChecker 验证调用栈的正确与否

  1. function initializeTypeChecker() {
  2. // Bind all source files and propagate errors
  3. for (const file of host.getSourceFiles()) {
  4. bindSourceFile(file, compilerOptions);
  5. }
  6. // Initialize global symbol table
  7. let augmentations: LiteralExpression[][];
  8. for (const file of host.getSourceFiles()) {
  9. if (!isExternalOrCommonJsModule(file)) {
  10. mergeSymbolTable(globals, file.locals);
  11. }
  12. if (file.patternAmbientModules && file.patternAmbientModules.length) {
  13. patternAmbientModules = concatenate(patternAmbientModules, file.patternAmbientModules);
  14. }
  15. if (file.moduleAugmentations.length) {
  16. (augmentations || (augmentations = [])).push(file.moduleAugmentations);
  17. }
  18. if (file.symbol && file.symbol.globalExports) {
  19. // Merge in UMD exports with first-in-wins semantics (see #9771)
  20. const source = file.symbol.globalExports;
  21. source.forEach((sourceSymbol, id) => {
  22. if (!globals.has(id)) {
  23. globals.set(id, sourceSymbol);
  24. }
  25. });
  26. }
  27. }
  28. }

查看检查器中的源码, 我们确实验证了上述调用栈的过程。先调用 bindSourceFile 再调用了 mergeSymbolTable 。

2.2 mergeSymbolTable

在上一篇章中 我们对绑定器 也就是 bindSourceFile 进行了一个分析 最后给每个节点都创建了一个符号, 将每个节点连接成一个相关的类型系统。

  1. function mergeSymbolTable(target: SymbolTable, source: SymbolTable) {
  2. source.forEach((sourceSymbol, id) => {
  3. let targetSymbol = target.get(id);
  4. if (!targetSymbol) {
  5. target.set(id, sourceSymbol);
  6. }
  7. else {
  8. if (!(targetSymbol.flags & SymbolFlags.Transient)) {
  9. targetSymbol = cloneSymbol(targetSymbol);
  10. target.set(id, targetSymbol);
  11. }
  12. mergeSymbol(targetSymbol, sourceSymbol);
  13. }
  14. });
  15. }

看上面 mergeSymbolTable 的代码 我们不难发现 mergeSymbolTable 主要做的事情 就是 将所有的global 符号合并到 let globals: SymbolTable = {} 符号表中。往后的类型检查都 统一在global上校验即可。

  1. function mergeSymbol(target: Symbol, source: Symbol) {
  2. if (!(target.flags & getExcludedSymbolFlags(source.flags))) {
  3. if (source.flags & SymbolFlags.ValueModule && target.flags & SymbolFlags.ValueModule && target.constEnumOnlyModule && !source.constEnumOnlyModule) {
  4. // reset flag when merging instantiated module into value module that has only const enums
  5. target.constEnumOnlyModule = false;
  6. }
  7. target.flags |= source.flags;
  8. if (source.valueDeclaration &&
  9. (!target.valueDeclaration ||
  10. (target.valueDeclaration.kind === SyntaxKind.ModuleDeclaration && source.valueDeclaration.kind !== SyntaxKind.ModuleDeclaration))) {
  11. // other kinds of value declarations take precedence over modules
  12. target.valueDeclaration = source.valueDeclaration;
  13. }
  14. addRange(target.declarations, source.declarations);
  15. if (source.members) {
  16. if (!target.members) target.members = createMap<Symbol>();
  17. mergeSymbolTable(target.members, source.members);
  18. }
  19. if (source.exports) {
  20. if (!target.exports) target.exports = createMap<Symbol>();
  21. mergeSymbolTable(target.exports, source.exports);
  22. }
  23. recordMergedSymbol(target, source);
  24. }
  25. else if (target.flags & SymbolFlags.NamespaceModule) {
  26. error(getNameOfDeclaration(source.declarations[0]), Diagnostics.Cannot_augment_module_0_with_value_exports_because_it_resolves_to_a_non_module_entity, symbolToString(target));
  27. }
  28. else {
  29. const message = target.flags & SymbolFlags.BlockScopedVariable || source.flags & SymbolFlags.BlockScopedVariable
  30. ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 : Diagnostics.Duplicate_identifier_0;
  31. forEach(source.declarations, node => {
  32. error(getNameOfDeclaration(node) || node, message, symbolToString(source));
  33. });
  34. forEach(target.declarations, node => {
  35. error(getNameOfDeclaration(node) || node, message, symbolToString(source));
  36. });
  37. }
  38. }

3. 类型检查

真正的类型检查会在调用 getDiagnostics 时才会发生。 该函数被调用时 (比如由 Program.emit 请求),检查器返回一个 EmitResolver(由程序调用检查器的 getEmitResolver 函数得到)。 EmitResolver 是 createTypeChecker 的一个本地函数的集合。

下面是 该过程直到 checkSourceFile 的调用栈 ( checkSourceFile 是 createTypeChecker 的一个本地函数 )

  1. program.emit ->
  2. emitWorker (program local) ->
  3. createTypeChecker.getEmitResolver ->
  4. // 第一次调用下面的几个 createTypeChecker 的本地函数
  5. call getDiagnostics ->
  6. getDiagnosticsWorker ->
  7. checkSourceFile
  8. // 接着
  9. return resolver
  10. // 通过对本地函数 createResolver() 的调用,resolver 已在 createTypeChecker 中初始化。

接下里 从 getDiagnostics 开始做代码分析

3.1 getDiagnostics

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

3.2 getDiagnosticsWorker

  1. function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {
  2. throwIfNonDiagnosticsProducing();
  3. if (sourceFile) {
  4. // Some global diagnostics are deferred until they are needed and
  5. // may not be reported in the firt call to getGlobalDiagnostics.
  6. // We should catch these changes and report them.
  7. const previousGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
  8. const previousGlobalDiagnosticsSize = previousGlobalDiagnostics.length;
  9. checkSourceFile(sourceFile);
  10. const semanticDiagnostics = diagnostics.getDiagnostics(sourceFile.fileName);
  11. const currentGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
  12. if (currentGlobalDiagnostics !== previousGlobalDiagnostics) {
  13. // If the arrays are not the same reference, new diagnostics were added.
  14. const deferredGlobalDiagnostics = relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, compareDiagnostics);
  15. return concatenate(deferredGlobalDiagnostics, semanticDiagnostics);
  16. }
  17. else if (previousGlobalDiagnosticsSize === 0 && currentGlobalDiagnostics.length > 0) {
  18. // If the arrays are the same reference, but the length has changed, a single
  19. // new diagnostic was added as DiagnosticCollection attempts to reuse the
  20. // same array.
  21. return concatenate(currentGlobalDiagnostics, semanticDiagnostics);
  22. }
  23. return semanticDiagnostics;
  24. }
  25. // Global diagnostics are always added when a file is not provided to
  26. // getDiagnostics
  27. forEach(host.getSourceFiles(), checkSourceFile);
  28. return diagnostics.getDiagnostics();
  29. }

把所有不相干的东西都去掉, 我们发现了一个小递归, 如果sourceFile存在那么进行checkSourceFile 操作, 否则就进入diagnostics.getDiagnostics() 再来一遍。

3.3 checkSourceFile

  1. function checkSourceFile(node: SourceFile) {
  2. performance.mark("beforeCheck");
  3. checkSourceFileWorker(node);
  4. performance.mark("afterCheck");
  5. performance.measure("Check", "beforeCheck", "afterCheck");
  6. }

3.4 checkSourceFileWorker

  1. // Fully type check a source file and collect the relevant diagnostics.
  2. function checkSourceFileWorker(node: SourceFile) {
  3. const links = getNodeLinks(node);
  4. if (!(links.flags & NodeCheckFlags.TypeChecked)) {
  5. // If skipLibCheck is enabled, skip type checking if file is a declaration file.
  6. // If skipDefaultLibCheck is enabled, skip type checking if file contains a
  7. // '/// <reference no-default-lib="true"/>' directive.
  8. if (compilerOptions.skipLibCheck && node.isDeclarationFile || compilerOptions.skipDefaultLibCheck && node.hasNoDefaultLib) {
  9. return;
  10. }
  11. // Grammar checking
  12. checkGrammarSourceFile(node);
  13. potentialThisCollisions.length = 0;
  14. potentialNewTargetCollisions.length = 0;
  15. deferredNodes = [];
  16. deferredUnusedIdentifierNodes = produceDiagnostics && noUnusedIdentifiers ? [] : undefined;
  17. forEach(node.statements, checkSourceElement);
  18. checkDeferredNodes();
  19. if (isExternalModule(node)) {
  20. registerForUnusedIdentifiersCheck(node);
  21. }
  22. if (!node.isDeclarationFile) {
  23. checkUnusedIdentifiers();
  24. }
  25. deferredNodes = undefined;
  26. deferredUnusedIdentifierNodes = undefined;
  27. if (isExternalOrCommonJsModule(node)) {
  28. checkExternalModuleExports(node);
  29. }
  30. if (potentialThisCollisions.length) {
  31. forEach(potentialThisCollisions, checkIfThisIsCapturedInEnclosingScope);
  32. potentialThisCollisions.length = 0;
  33. }
  34. if (potentialNewTargetCollisions.length) {
  35. forEach(potentialNewTargetCollisions, checkIfNewTargetIsCapturedInEnclosingScope);
  36. potentialNewTargetCollisions.length = 0;
  37. }
  38. links.flags |= NodeCheckFlags.TypeChecked;
  39. }
  40. }

仔细阅读以上代码, 我们发现在checkSourceFileWorker函数内有各种各样的check操作比如: checkGrammarSourceFile 、 checkDeferredNodes、 registerForUnusedIdentifiersCheck … 这不就是我们苦苦探寻的么?我们随便调一个继续深究下去.

3.5 checkGrammarSourceFile

  1. function checkGrammarSourceFile(node: SourceFile): boolean {
  2. return isInAmbientContext(node) && checkGrammarTopLevelElementsForRequiredDeclareModifier(node);
  3. }

3.6 checkGrammarTopLevelElementsForRequiredDeclareModifier

  1. function checkGrammarTopLevelElementsForRequiredDeclareModifier(file: SourceFile): boolean {
  2. for (const decl of file.statements) {
  3. if (isDeclaration(decl) || decl.kind === SyntaxKind.VariableStatement) {
  4. if (checkGrammarTopLevelElementForRequiredDeclareModifier(decl)) {
  5. return true;
  6. }
  7. }
  8. }
  9. }

3.7 checkGrammarTopLevelElementForRequiredDeclareModifier

  1. function checkGrammarTopLevelElementForRequiredDeclareModifier(node: Node): boolean {
  2. // A declare modifier is required for any top level .d.ts declaration except export=, export default, export as namespace
  3. // interfaces and imports categories:
  4. //
  5. // DeclarationElement:
  6. // ExportAssignment
  7. // export_opt InterfaceDeclaration
  8. // export_opt TypeAliasDeclaration
  9. // export_opt ImportDeclaration
  10. // export_opt ExternalImportDeclaration
  11. // export_opt AmbientDeclaration
  12. //
  13. // TODO: The spec needs to be amended to reflect this grammar.
  14. if (node.kind === SyntaxKind.InterfaceDeclaration ||
  15. node.kind === SyntaxKind.TypeAliasDeclaration ||
  16. node.kind === SyntaxKind.ImportDeclaration ||
  17. node.kind === SyntaxKind.ImportEqualsDeclaration ||
  18. node.kind === SyntaxKind.ExportDeclaration ||
  19. node.kind === SyntaxKind.ExportAssignment ||
  20. node.kind === SyntaxKind.NamespaceExportDeclaration ||
  21. getModifierFlags(node) & (ModifierFlags.Ambient | ModifierFlags.Export | ModifierFlags.Default)) {
  22. return false;
  23. }
  24. return grammarErrorOnFirstToken(node, Diagnostics.A_declare_modifier_is_required_for_a_top_level_declaration_in_a_d_ts_file);
  25. }

3.8 grammarErrorOnFirstToken

  1. function grammarErrorOnFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean {
  2. const sourceFile = getSourceFileOfNode(node);
  3. if (!hasParseDiagnostics(sourceFile)) {
  4. const span = getSpanOfTokenAtPosition(sourceFile, node.pos);
  5. diagnostics.add(createFileDiagnostic(sourceFile, span.start, span.length, message, arg0, arg1, arg2));
  6. return true;
  7. }
  8. }

3.9 createFileDiagnostic

  1. export function createFileDiagnostic(file: SourceFile, start: number, length: number, message: DiagnosticMessage, ...args: (string | number)[]): Diagnostic;
  2. export function createFileDiagnostic(file: SourceFile, start: number, length: number, message: DiagnosticMessage): Diagnostic {
  3. const end = start + length;
  4. Debug.assert(start >= 0, "start must be non-negative, is " + start);
  5. Debug.assert(length >= 0, "length must be non-negative, is " + length);
  6. if (file) {
  7. Debug.assert(start <= file.text.length, `start must be within the bounds of the file. ${start} > ${file.text.length}`);
  8. Debug.assert(end <= file.text.length, `end must be the bounds of the file. ${end} > ${file.text.length}`);
  9. }
  10. let text = getLocaleSpecificMessage(message);
  11. if (arguments.length > 4) {
  12. text = formatStringFromArgs(text, arguments, 4);
  13. }
  14. return {
  15. file,
  16. start,
  17. length,
  18. messageText: text,
  19. category: message.category,
  20. code: message.code,
  21. };
  22. }

终于结束了, 我们发现最后的类型校验都会通过Debug.assert 函数给抛出来。

4. 流程图

image.png