Skip to content

Commit

Permalink
Region-based semantic diagnostics (#57842)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabritto authored Jun 13, 2024
1 parent b258429 commit 4857546
Show file tree
Hide file tree
Showing 161 changed files with 4,872 additions and 927 deletions.
82 changes: 68 additions & 14 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47502,6 +47502,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function checkSourceElementWorker(node: Node): void {
if (getNodeCheckFlags(node) & NodeCheckFlags.PartiallyTypeChecked) {
return;
}

if (canHaveJSDoc(node)) {
forEach(node.jsDoc, ({ comment, tags }) => {
checkJSDocCommentWorker(comment);
Expand Down Expand Up @@ -47530,6 +47534,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
errorOrSuggestion(compilerOptions.allowUnreachableCode === false, node, Diagnostics.Unreachable_code_detected);
}

// If editing this, keep `isSourceElement` in utilities up to date.
switch (kind) {
case SyntaxKind.TypeParameter:
return checkTypeParameter(node as TypeParameterDeclaration);
Expand Down Expand Up @@ -47894,12 +47899,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
tracing?.pop();
}

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");
function checkSourceFile(node: SourceFile, nodesToCheck: Node[] | undefined) {
tracing?.push(tracing.Phase.Check, nodesToCheck ? "checkSourceFileNodes" : "checkSourceFile", { path: node.path }, /*separateBeginAndEnd*/ true);
const beforeMark = nodesToCheck ? "beforeCheckNodes" : "beforeCheck";
const afterMark = nodesToCheck ? "afterCheckNodes" : "afterCheck";
performance.mark(beforeMark);
nodesToCheck ? checkSourceFileNodesWorker(node, nodesToCheck) : checkSourceFileWorker(node);
performance.mark(afterMark);
performance.measure("Check", beforeMark, afterMark);
tracing?.pop();
}

Expand Down Expand Up @@ -47938,6 +47945,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
clear(potentialReflectCollisions);
clear(potentialUnusedRenamedBindingElementsInTypes);

if (links.flags & NodeCheckFlags.PartiallyTypeChecked) {
potentialThisCollisions = links.potentialThisCollisions!;
potentialNewTargetCollisions = links.potentialNewTargetCollisions!;
potentialWeakMapSetCollisions = links.potentialWeakMapSetCollisions!;
potentialReflectCollisions = links.potentialReflectCollisions!;
potentialUnusedRenamedBindingElementsInTypes = links.potentialUnusedRenamedBindingElementsInTypes!;
}

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

Expand Down Expand Up @@ -47989,13 +48004,49 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function getDiagnostics(sourceFile: SourceFile, ct: CancellationToken): Diagnostic[] {
function checkSourceFileNodesWorker(file: SourceFile, nodes: readonly Node[]) {
const links = getNodeLinks(file);
if (!(links.flags & NodeCheckFlags.TypeChecked)) {
if (skipTypeChecking(file, compilerOptions, host)) {
return;
}

// Grammar checking
checkGrammarSourceFile(file);

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

forEach(nodes, checkSourceElement);

checkDeferredNodes(file);

(links.potentialThisCollisions || (links.potentialThisCollisions = [])).push(...potentialThisCollisions);
(links.potentialNewTargetCollisions || (links.potentialNewTargetCollisions = [])).push(...potentialNewTargetCollisions);
(links.potentialWeakMapSetCollisions || (links.potentialWeakMapSetCollisions = [])).push(...potentialWeakMapSetCollisions);
(links.potentialReflectCollisions || (links.potentialReflectCollisions = [])).push(...potentialReflectCollisions);
(links.potentialUnusedRenamedBindingElementsInTypes || (links.potentialUnusedRenamedBindingElementsInTypes = [])).push(
...potentialUnusedRenamedBindingElementsInTypes,
);

links.flags |= NodeCheckFlags.PartiallyTypeChecked;
for (const node of nodes) {
const nodeLinks = getNodeLinks(node);
nodeLinks.flags |= NodeCheckFlags.PartiallyTypeChecked;
}
}
}

function getDiagnostics(sourceFile: SourceFile, ct: CancellationToken, nodesToCheck?: Node[]): 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);
return getDiagnosticsWorker(sourceFile, nodesToCheck);
}
finally {
cancellationToken = undefined;
Expand All @@ -48010,7 +48061,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
deferredDiagnosticsCallbacks = [];
}

function checkSourceFileWithEagerDiagnostics(sourceFile: SourceFile) {
function checkSourceFileWithEagerDiagnostics(sourceFile: SourceFile, nodesToCheck?: Node[]) {
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)
Expand All @@ -48019,11 +48070,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// thus much more likely retaining the same union ordering as before we had lazy diagnostics)
const oldAddLazyDiagnostics = addLazyDiagnostic;
addLazyDiagnostic = cb => cb();
checkSourceFile(sourceFile);
checkSourceFile(sourceFile, nodesToCheck);
addLazyDiagnostic = oldAddLazyDiagnostics;
}

function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {
function getDiagnosticsWorker(sourceFile: SourceFile, nodesToCheck: Node[] | undefined): Diagnostic[] {
if (sourceFile) {
ensurePendingDiagnosticWorkComplete();
// Some global diagnostics are deferred until they are needed and
Expand All @@ -48032,9 +48083,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const previousGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
const previousGlobalDiagnosticsSize = previousGlobalDiagnostics.length;

checkSourceFileWithEagerDiagnostics(sourceFile);

checkSourceFileWithEagerDiagnostics(sourceFile, nodesToCheck);
const semanticDiagnostics = diagnostics.getDiagnostics(sourceFile.fileName);
if (nodesToCheck) {
// No need to get global diagnostics.
return semanticDiagnostics;
}
const currentGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
if (currentGlobalDiagnostics !== previousGlobalDiagnostics) {
// If the arrays are not the same reference, new diagnostics were added.
Expand All @@ -48053,7 +48107,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

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

Expand Down
57 changes: 42 additions & 15 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2961,8 +2961,12 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
return getDiagnosticsHelper(sourceFile, getSyntacticDiagnosticsForFile, cancellationToken);
}

function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] {
return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken);
function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken, nodesToCheck?: Node[]): readonly Diagnostic[] {
return getDiagnosticsHelper(
sourceFile,
(sourceFile, cancellationToken) => getSemanticDiagnosticsForFile(sourceFile, cancellationToken, nodesToCheck),
cancellationToken,
);
}

function getCachedSemanticDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] | undefined {
Expand All @@ -2972,7 +2976,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
}

function getBindAndCheckDiagnostics(sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] {
return getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken);
return getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken, /*nodesToCheck*/ undefined);
}

function getProgramDiagnostics(sourceFile: SourceFile): readonly Diagnostic[] {
Expand Down Expand Up @@ -3026,18 +3030,33 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
}
}

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

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

function getBindAndCheckDiagnosticsForFileNoCache(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] {
function getBindAndCheckDiagnosticsForFileNoCache(
sourceFile: SourceFile,
cancellationToken: CancellationToken | undefined,
nodesToCheck?: Node[],
): readonly Diagnostic[] {
return runWithCancellationToken(() => {
if (skipTypeChecking(sourceFile, options, program)) {
return emptyArray;
Expand All @@ -3048,32 +3067,40 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
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 isCheckJs = isJs && isCheckJsEnabledForFile(sourceFile, options);

// 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
let bindDiagnostics = sourceFile.bindDiagnostics;
let checkDiagnostics = typeChecker.getDiagnostics(sourceFile, cancellationToken);
let checkDiagnostics = typeChecker.getDiagnostics(sourceFile, cancellationToken, nodesToCheck);
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, !isPlainJs, bindDiagnostics, checkDiagnostics, isCheckJs ? sourceFile.jsDocDiagnostics : undefined);
return getMergedBindAndCheckDiagnostics(
sourceFile,
!isPlainJs,
!!nodesToCheck,
bindDiagnostics,
checkDiagnostics,
isCheckJs ? sourceFile.jsDocDiagnostics : undefined,
);
});
}

function getMergedBindAndCheckDiagnostics(sourceFile: SourceFile, includeBindAndCheckDiagnostics: boolean, ...allDiagnostics: (readonly Diagnostic[] | undefined)[]) {
function getMergedBindAndCheckDiagnostics(sourceFile: SourceFile, includeBindAndCheckDiagnostics: boolean, partialCheck: boolean, ...allDiagnostics: (readonly Diagnostic[] | undefined)[]) {
const flatDiagnostics = flatten(allDiagnostics);
if (!includeBindAndCheckDiagnostics || !sourceFile.commentDirectives?.length) {
return flatDiagnostics;
}

const { diagnostics, directives } = getDiagnosticsWithPrecedingDirectives(sourceFile, sourceFile.commentDirectives, flatDiagnostics);

// When doing a partial check, we can't be sure a directive is unused.
if (partialCheck) {
return diagnostics;
}

for (const errorExpectation of directives.getUnusedExpectations()) {
diagnostics.push(createDiagnosticForRange(sourceFile, errorExpectation.range, Diagnostics.Unused_ts_expect_error_directive));
}
Expand Down
11 changes: 10 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4716,6 +4716,9 @@ export interface Program extends ScriptReferenceHost {
getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[];
/** The first time this is called, it will return global diagnostics (no location). */
getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[];
/** @internal */
getSemanticDiagnostics(sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined, nodesToCheck: Node[]): readonly Diagnostic[];

getDeclarationDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[];
getConfigFileParsingDiagnostics(): readonly Diagnostic[];
/** @internal */ getSuggestionDiagnostics(sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[];
Expand Down Expand Up @@ -5264,7 +5267,7 @@ export interface TypeChecker {
/** @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 */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken, nodesToCheck?: Node[]): Diagnostic[];
/** @internal */ getGlobalDiagnostics(): Diagnostic[];
/** @internal */ getEmitResolver(sourceFile?: SourceFile, cancellationToken?: CancellationToken, forceDts?: boolean): EmitResolver;
/** @internal */ requiresAddingImplicitUndefined(parameter: ParameterDeclaration | JSDocParameterTag): boolean;
Expand Down Expand Up @@ -6118,6 +6121,7 @@ export const enum NodeCheckFlags {
ContainsClassWithPrivateIdentifiers = 1 << 20, // Marked on all block-scoped containers containing a class with private identifiers.
ContainsSuperPropertyInStaticInitializer = 1 << 21, // Marked on all block-scoped containers containing a static initializer with 'super.x' or 'super[x]'.
InCheckIdentifier = 1 << 22,
PartiallyTypeChecked = 1 << 23, // Node has been partially type checked

/** These flags are LazyNodeCheckFlags and can be calculated lazily by `hasNodeCheckFlag` */
LazyFlags = SuperInstance
Expand Down Expand Up @@ -6177,6 +6181,11 @@ export interface NodeLinks {
parameterInitializerContainsUndefined?: boolean; // True if this is a parameter declaration whose type annotation contains "undefined".
fakeScopeForSignatureDeclaration?: "params" | "typeParams"; // If present, this is a fake scope injected into an enclosing declaration chain.
assertionExpressionType?: Type; // Cached type of the expression of a type assertion
potentialThisCollisions?: Node[];
potentialNewTargetCollisions?: Node[];
potentialWeakMapSetCollisions?: Node[];
potentialReflectCollisions?: Node[];
potentialUnusedRenamedBindingElementsInTypes?: BindingElement[];
externalHelpersModule?: Symbol; // Resolved symbol for the external helpers module
}

Expand Down
Loading

0 comments on commit 4857546

Please sign in to comment.