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 errors
for (const file of host.getSourceFiles()) {
bindSourceFile(file, compilerOptions);
}
// Initialize global symbol table
let 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 enums
target.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 modules
target.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
// getDiagnostics
forEach(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 checking
checkGrammarSourceFile(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 函数给抛出来。