Skip to content

Commit c0b39c6

Browse files
jakebaileysandersnsheetalkamat
authored
Skip parsing JSDoc when not needed (#52921)
Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Co-authored-by: Sheetal Nandi <shkamat@microsoft.com>
1 parent 21b8892 commit c0b39c6

15 files changed

+312
-24
lines changed

src/compiler/parser.ts

+30-14
Original file line numberDiff line numberDiff line change
@@ -1324,7 +1324,10 @@ function setExternalModuleIndicator(sourceFile: SourceFile) {
13241324
sourceFile.externalModuleIndicator = isFileProbablyExternalModule(sourceFile);
13251325
}
13261326

1327-
export function createSourceFile(fileName: string, sourceText: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, setParentNodes = false, scriptKind?: ScriptKind): SourceFile {
1327+
export function createSourceFile(fileName: string, sourceText: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, setParentNodes?: boolean, scriptKind?: ScriptKind): SourceFile;
1328+
/** @internal */
1329+
export function createSourceFile(fileName: string, sourceText: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, setParentNodes?: boolean, scriptKind?: ScriptKind, skipNonSemanticJSDoc?: boolean): SourceFile;
1330+
export function createSourceFile(fileName: string, sourceText: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, setParentNodes = false, scriptKind?: ScriptKind, skipNonSemanticJSDoc?: boolean): SourceFile {
13281331
tracing?.push(tracing.Phase.Parse, "createSourceFile", { path: fileName }, /*separateBeginAndEnd*/ true);
13291332
performance.mark("beforeParse");
13301333
let result: SourceFile;
@@ -1336,14 +1339,14 @@ export function createSourceFile(fileName: string, sourceText: string, languageV
13361339
impliedNodeFormat: format,
13371340
} = typeof languageVersionOrOptions === "object" ? languageVersionOrOptions : ({ languageVersion: languageVersionOrOptions } as CreateSourceFileOptions);
13381341
if (languageVersion === ScriptTarget.JSON) {
1339-
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ScriptKind.JSON, noop);
1342+
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ScriptKind.JSON, noop, skipNonSemanticJSDoc);
13401343
}
13411344
else {
13421345
const setIndicator = format === undefined ? overrideSetExternalModuleIndicator : (file: SourceFile) => {
13431346
file.impliedNodeFormat = format;
13441347
return (overrideSetExternalModuleIndicator || setExternalModuleIndicator)(file);
13451348
};
1346-
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind, setIndicator);
1349+
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind, setIndicator, skipNonSemanticJSDoc);
13471350
}
13481351
perfLogger?.logStopParseSourceFile();
13491352

@@ -1575,7 +1578,16 @@ namespace Parser {
15751578
var parseErrorBeforeNextFinishedNode = false;
15761579
/* eslint-enable no-var */
15771580

1578-
export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor | undefined, setParentNodes = false, scriptKind?: ScriptKind, setExternalModuleIndicatorOverride?: (file: SourceFile) => void): SourceFile {
1581+
export function parseSourceFile(
1582+
fileName: string,
1583+
sourceText: string,
1584+
languageVersion: ScriptTarget,
1585+
syntaxCursor: IncrementalParser.SyntaxCursor | undefined,
1586+
setParentNodes = false,
1587+
scriptKind?: ScriptKind,
1588+
setExternalModuleIndicatorOverride?: (file: SourceFile) => void,
1589+
skipNonSemanticJSDoc?: boolean,
1590+
): SourceFile {
15791591
scriptKind = ensureScriptKind(fileName, scriptKind);
15801592
if (scriptKind === ScriptKind.JSON) {
15811593
const result = parseJsonText(fileName, sourceText, languageVersion, syntaxCursor, setParentNodes);
@@ -1589,9 +1601,10 @@ namespace Parser {
15891601
return result;
15901602
}
15911603

1592-
initializeState(fileName, sourceText, languageVersion, syntaxCursor, scriptKind);
1604+
skipNonSemanticJSDoc = !!skipNonSemanticJSDoc && scriptKind !== ScriptKind.JS && scriptKind !== ScriptKind.JSX;
1605+
initializeState(fileName, sourceText, languageVersion, syntaxCursor, scriptKind, skipNonSemanticJSDoc);
15931606

1594-
const result = parseSourceFileWorker(languageVersion, setParentNodes, scriptKind, setExternalModuleIndicatorOverride || setExternalModuleIndicator);
1607+
const result = parseSourceFileWorker(languageVersion, setParentNodes, scriptKind, setExternalModuleIndicatorOverride || setExternalModuleIndicator, skipNonSemanticJSDoc);
15951608

15961609
clearState();
15971610

@@ -1600,7 +1613,7 @@ namespace Parser {
16001613

16011614
export function parseIsolatedEntityName(content: string, languageVersion: ScriptTarget): EntityName | undefined {
16021615
// Choice of `isDeclarationFile` should be arbitrary
1603-
initializeState("", content, languageVersion, /*syntaxCursor*/ undefined, ScriptKind.JS);
1616+
initializeState("", content, languageVersion, /*syntaxCursor*/ undefined, ScriptKind.JS, /*skipNonSemanticJSDoc*/ false);
16041617
// Prime the scanner.
16051618
nextToken();
16061619
const entityName = parseEntityName(/*allowReservedWords*/ true);
@@ -1610,7 +1623,7 @@ namespace Parser {
16101623
}
16111624

16121625
export function parseJsonText(fileName: string, sourceText: string, languageVersion: ScriptTarget = ScriptTarget.ES2015, syntaxCursor?: IncrementalParser.SyntaxCursor, setParentNodes = false): JsonSourceFile {
1613-
initializeState(fileName, sourceText, languageVersion, syntaxCursor, ScriptKind.JSON);
1626+
initializeState(fileName, sourceText, languageVersion, syntaxCursor, ScriptKind.JSON, /*skipNonSemanticJSDoc*/ false);
16141627
sourceFlags = contextFlags;
16151628

16161629
// Prime the scanner.
@@ -1698,7 +1711,7 @@ namespace Parser {
16981711
return result;
16991712
}
17001713

1701-
function initializeState(_fileName: string, _sourceText: string, _languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor | undefined, _scriptKind: ScriptKind) {
1714+
function initializeState(_fileName: string, _sourceText: string, _languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor | undefined, _scriptKind: ScriptKind, _skipNonSemanticJSDoc: boolean) {
17021715
NodeConstructor = objectAllocator.getNodeConstructor();
17031716
TokenConstructor = objectAllocator.getTokenConstructor();
17041717
IdentifierConstructor = objectAllocator.getIdentifierConstructor();
@@ -1739,13 +1752,15 @@ namespace Parser {
17391752
scanner.setOnError(scanError);
17401753
scanner.setScriptTarget(languageVersion);
17411754
scanner.setLanguageVariant(languageVariant);
1755+
scanner.setSkipNonSemanticJSDoc(_skipNonSemanticJSDoc);
17421756
}
17431757

17441758
function clearState() {
17451759
// Clear out the text the scanner is pointing at, so it doesn't keep anything alive unnecessarily.
17461760
scanner.clearCommentDirectives();
17471761
scanner.setText("");
17481762
scanner.setOnError(undefined);
1763+
scanner.setSkipNonSemanticJSDoc(false);
17491764

17501765
// Clear any data. We don't want to accidentally hold onto it for too long.
17511766
sourceText = undefined!;
@@ -1762,7 +1777,7 @@ namespace Parser {
17621777
topLevel = true;
17631778
}
17641779

1765-
function parseSourceFileWorker(languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind, setExternalModuleIndicator: (file: SourceFile) => void): SourceFile {
1780+
function parseSourceFileWorker(languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind, setExternalModuleIndicator: (file: SourceFile) => void, skipNonSemanticJSDoc: boolean): SourceFile {
17661781
const isDeclarationFile = isDeclarationFileName(fileName);
17671782
if (isDeclarationFile) {
17681783
contextFlags |= NodeFlags.Ambient;
@@ -1789,6 +1804,7 @@ namespace Parser {
17891804
sourceFile.identifierCount = identifierCount;
17901805
sourceFile.identifiers = identifiers;
17911806
sourceFile.parseDiagnostics = attachFileToDiagnostics(parseDiagnostics, sourceFile);
1807+
sourceFile.skipNonSemanticJSDoc = skipNonSemanticJSDoc;
17921808
if (jsDocDiagnostics) {
17931809
sourceFile.jsDocDiagnostics = attachFileToDiagnostics(jsDocDiagnostics, sourceFile);
17941810
}
@@ -8670,7 +8686,7 @@ namespace Parser {
86708686

86718687
export namespace JSDocParser {
86728688
export function parseJSDocTypeExpressionForTests(content: string, start: number | undefined, length: number | undefined): { jsDocTypeExpression: JSDocTypeExpression; diagnostics: Diagnostic[]; } | undefined {
8673-
initializeState("file.js", content, ScriptTarget.Latest, /*syntaxCursor*/ undefined, ScriptKind.JS);
8689+
initializeState("file.js", content, ScriptTarget.Latest, /*syntaxCursor*/ undefined, ScriptKind.JS, /*skipNonSemanticJSDoc*/ false);
86748690
scanner.setText(content, start, length);
86758691
currentToken = scanner.scan();
86768692
const jsDocTypeExpression = parseJSDocTypeExpression();
@@ -8720,7 +8736,7 @@ namespace Parser {
87208736
}
87218737

87228738
export function parseIsolatedJSDocComment(content: string, start: number | undefined, length: number | undefined): { jsDoc: JSDoc; diagnostics: Diagnostic[]; } | undefined {
8723-
initializeState("", content, ScriptTarget.Latest, /*syntaxCursor*/ undefined, ScriptKind.JS);
8739+
initializeState("", content, ScriptTarget.Latest, /*syntaxCursor*/ undefined, ScriptKind.JS, /*skipNonSemanticJSDoc*/ false);
87248740
const jsDoc = doInsideOfContext(NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length));
87258741

87268742
const sourceFile = { languageVariant: LanguageVariant.Standard, text: content } as SourceFile;
@@ -9788,7 +9804,7 @@ namespace IncrementalParser {
97889804
if (sourceFile.statements.length === 0) {
97899805
// If we don't have any statements in the current source file, then there's no real
97909806
// way to incrementally parse. So just do a full parse instead.
9791-
return Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, /*syntaxCursor*/ undefined, /*setParentNodes*/ true, sourceFile.scriptKind, sourceFile.setExternalModuleIndicator);
9807+
return Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, /*syntaxCursor*/ undefined, /*setParentNodes*/ true, sourceFile.scriptKind, sourceFile.setExternalModuleIndicator, sourceFile.skipNonSemanticJSDoc);
97929808
}
97939809

97949810
// Make sure we're not trying to incrementally update a source file more than once. Once
@@ -9851,7 +9867,7 @@ namespace IncrementalParser {
98519867
// inconsistent tree. Setting the parents on the new tree should be very fast. We
98529868
// will immediately bail out of walking any subtrees when we can see that their parents
98539869
// are already correct.
9854-
const result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /*setParentNodes*/ true, sourceFile.scriptKind, sourceFile.setExternalModuleIndicator);
9870+
const result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /*setParentNodes*/ true, sourceFile.scriptKind, sourceFile.setExternalModuleIndicator, sourceFile.skipNonSemanticJSDoc);
98559871
result.commentDirectives = getNewCommentDirectives(
98569872
sourceFile.commentDirectives,
98579873
result.commentDirectives,

src/compiler/program.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ export function createGetSourceFile(
402402
readFile: ProgramHost<any>["readFile"],
403403
getCompilerOptions: () => CompilerOptions,
404404
setParentNodes: boolean | undefined,
405+
skipNonSemanticJSDocParsing: boolean | undefined,
405406
): CompilerHost["getSourceFile"] {
406407
return (fileName, languageVersionOrOptions, onError) => {
407408
let text: string | undefined;
@@ -417,7 +418,7 @@ export function createGetSourceFile(
417418
}
418419
text = "";
419420
}
420-
return text !== undefined ? createSourceFile(fileName, text, languageVersionOrOptions, setParentNodes) : undefined;
421+
return text !== undefined ? createSourceFile(fileName, text, languageVersionOrOptions, setParentNodes, /*scriptKind*/ undefined, skipNonSemanticJSDocParsing) : undefined;
421422
};
422423
}
423424

@@ -455,7 +456,12 @@ export function createWriteFileMeasuringIO(
455456
}
456457

457458
/** @internal */
458-
export function createCompilerHostWorker(options: CompilerOptions, setParentNodes?: boolean, system: System = sys): CompilerHost {
459+
export function createCompilerHostWorker(
460+
options: CompilerOptions,
461+
setParentNodes?: boolean,
462+
skipNonSemanticJSDocParsing?: boolean,
463+
system: System = sys,
464+
): CompilerHost {
459465
const existingDirectories = new Map<string, boolean>();
460466
const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames);
461467
function directoryExists(directoryPath: string): boolean {
@@ -476,7 +482,7 @@ export function createCompilerHostWorker(options: CompilerOptions, setParentNode
476482
const newLine = getNewLineCharacter(options);
477483
const realpath = system.realpath && ((path: string) => system.realpath!(path));
478484
const compilerHost: CompilerHost = {
479-
getSourceFile: createGetSourceFile(fileName => compilerHost.readFile(fileName), () => options, setParentNodes),
485+
getSourceFile: createGetSourceFile(fileName => compilerHost.readFile(fileName), () => options, setParentNodes, skipNonSemanticJSDocParsing),
480486
getDefaultLibLocation,
481487
getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)),
482488
writeFile: createWriteFileMeasuringIO(

src/compiler/scanner.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export interface Scanner {
114114
// callback returns something truthy, then the scanner state is not rolled back. The result
115115
// of invoking the callback is returned from this function.
116116
tryScan<T>(callback: () => T): T;
117+
/** @internal */
118+
setSkipNonSemanticJSDoc(skip: boolean): void;
117119
}
118120

119121
/** @internal */
@@ -343,6 +345,11 @@ const commentDirectiveRegExSingleLine = /^\/\/\/?\s*@(ts-expect-error|ts-ignore)
343345
*/
344346
const commentDirectiveRegExMultiLine = /^(?:\/|\*)*\s*@(ts-expect-error|ts-ignore)/;
345347

348+
/**
349+
* Test for whether a comment contains a JSDoc tag needed by the checker when run in tsc.
350+
*/
351+
const semanticJSDocTagRegEx = /@(?:see|link)/i;
352+
346353
function lookupInUnicodeMap(code: number, map: readonly number[]): boolean {
347354
// Bail out quickly if it couldn't possibly be in the map.
348355
if (code < map[0]) {
@@ -1001,6 +1008,8 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
10011008
var commentDirectives: CommentDirective[] | undefined;
10021009
var inJSDocType = 0;
10031010

1011+
var skipNonSemanticJSDoc = false;
1012+
10041013
setText(text, start, length);
10051014

10061015
var scanner: Scanner = {
@@ -1052,6 +1061,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
10521061
tryScan,
10531062
lookAhead,
10541063
scanRange,
1064+
setSkipNonSemanticJSDoc,
10551065
};
10561066
/* eslint-enable no-var */
10571067

@@ -1971,9 +1981,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
19711981
// Multi-line comment
19721982
if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) {
19731983
pos += 2;
1974-
if (text.charCodeAt(pos) === CharacterCodes.asterisk && text.charCodeAt(pos + 1) !== CharacterCodes.slash) {
1975-
tokenFlags |= TokenFlags.PrecedingJSDocComment;
1976-
}
1984+
const isJSDoc = text.charCodeAt(pos) === CharacterCodes.asterisk && text.charCodeAt(pos + 1) !== CharacterCodes.slash;
19771985

19781986
let commentClosed = false;
19791987
let lastLineStart = tokenStart;
@@ -1994,6 +2002,10 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
19942002
}
19952003
}
19962004

2005+
if (isJSDoc && (!skipNonSemanticJSDoc || semanticJSDocTagRegEx.test(text.slice(fullStartPos, pos)))) {
2006+
tokenFlags |= TokenFlags.PrecedingJSDocComment;
2007+
}
2008+
19972009
commentDirectives = appendIfCommentDirective(commentDirectives, text.slice(lastLineStart, pos), commentDirectiveRegExMultiLine, lastLineStart);
19982010

19992011
if (!commentClosed) {
@@ -2775,6 +2787,10 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
27752787
languageVariant = variant;
27762788
}
27772789

2790+
function setSkipNonSemanticJSDoc(skip: boolean) {
2791+
skipNonSemanticJSDoc = skip;
2792+
}
2793+
27782794
function resetTokenState(position: number) {
27792795
Debug.assert(position >= 0);
27802796
pos = position;

src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4297,6 +4297,8 @@ export interface SourceFile extends Declaration, LocalsContainer {
42974297

42984298
/** @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit;
42994299
/** @internal */ endFlowNode?: FlowNode;
4300+
4301+
/** @internal */ skipNonSemanticJSDoc?: boolean;
43004302
}
43014303

43024304
/** @internal */

src/compiler/watch.ts

+1
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ export function createCompilerHostFromProgramHost(host: ProgramHost<any>, getCom
746746
(fileName, encoding) => !encoding ? compilerHost.readFile(fileName) : host.readFile(fileName, encoding),
747747
getCompilerOptions,
748748
/*setParentNodes*/ undefined,
749+
host.skipNonSemanticJSDocParsing,
749750
),
750751
getDefaultLibLocation: maybeBind(host, host.getDefaultLibLocation),
751752
getDefaultLibFileName: options => host.getDefaultLibFileName(options),

src/compiler/watchPublic.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,11 @@ export function readBuilderProgram(compilerOptions: CompilerOptions, host: ReadB
118118
return createBuilderProgramUsingProgramBuildInfo(buildInfo, buildInfoPath, host);
119119
}
120120

121-
export function createIncrementalCompilerHost(options: CompilerOptions, system = sys): CompilerHost {
122-
const host = createCompilerHostWorker(options, /*setParentNodes*/ undefined, system);
121+
export function createIncrementalCompilerHost(options: CompilerOptions, system?: System): CompilerHost;
122+
/** @internal */
123+
export function createIncrementalCompilerHost(options: CompilerOptions, system?: System, skipNonSemanticJSDocParsing?: boolean): CompilerHost;
124+
export function createIncrementalCompilerHost(options: CompilerOptions, system = sys, skipNonSemanticJSDocParsing?: boolean): CompilerHost {
125+
const host = createCompilerHostWorker(options, /*setParentNodes*/ undefined, skipNonSemanticJSDocParsing, system);
123126
host.createHash = maybeBind(system, system.createHash);
124127
host.storeFilesChangingSignatureDuringEmit = system.storeFilesChangingSignatureDuringEmit;
125128
setGetSourceFileAsHashVersioned(host);
@@ -254,6 +257,13 @@ export interface ProgramHost<T extends BuilderProgram> {
254257
* Returns the module resolution cache used by a provided `resolveModuleNames` implementation so that any non-name module resolution operations (eg, package.json lookup) can reuse it
255258
*/
256259
getModuleResolutionCache?(): ModuleResolutionCache | undefined;
260+
261+
/**
262+
* True if it's safe for the parser to skip parsing non-semantic JSDoc tags.
263+
*
264+
* @internal
265+
*/
266+
skipNonSemanticJSDocParsing?: boolean;
257267
}
258268
/**
259269
* Internal interface used to wire emit through same host

0 commit comments

Comments
 (0)