Skip to content

Commit d1fa945

Browse files
authored
Add moduleDetection compiler flag to allow for changing how modules are parsed (microsoft#47495)
* Add moduleDetection compiler flag to allow for changing how modules are parsed The default setting is 'auto', where JSX containing files under react-jsx and react-jsxdev are always parsed as modules, and esm-format files under module: node12+ are always parsed as modules, in addition to the 'legacy' detection mode's conditions for other files. (Declaration files are exempt from these new conditions) The 'legacy' mode preserves TS's behavior prior to the introduction of this flag - a file is parsed as a module if it contains an import, export, or import.meta expression. In addition, there is a 'force' mode that forces all non-declaration files to be parsed as modules. (Declaration files are still only modules if they contain a top-level import or export.) This technically breaks the parser API, but it's kinda-sorta backwards compatible so long as you don't need the functionality associated with more recent compiler flags. * Fix post-merge lint * Rename function * Update default value documentation * PR feedback * Fix lint and typo
1 parent 0271738 commit d1fa945

File tree

144 files changed

+2534
-203
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+2534
-203
lines changed

src/compiler/commandLineParser.ts

+12
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,18 @@ namespace ts {
12411241
category: Diagnostics.Editor_Support,
12421242

12431243
},
1244+
{
1245+
name: "moduleDetection",
1246+
type: new Map(getEntries({
1247+
auto: ModuleDetectionKind.Auto,
1248+
legacy: ModuleDetectionKind.Legacy,
1249+
force: ModuleDetectionKind.Force,
1250+
})),
1251+
affectsModuleResolution: true,
1252+
description: Diagnostics.Control_what_method_is_used_to_detect_module_format_JS_files,
1253+
category: Diagnostics.Language_and_Environment,
1254+
defaultValueDescription: Diagnostics.auto_Colon_Treat_files_with_imports_exports_import_meta_jsx_with_jsx_Colon_react_jsx_or_esm_format_with_module_Colon_node12_as_modules,
1255+
}
12441256
];
12451257

12461258
/* @internal */

src/compiler/core.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -2156,14 +2156,16 @@ namespace ts {
21562156
return (arg: T) => f(arg) && g(arg);
21572157
}
21582158

2159-
export function or<T extends unknown[]>(...fs: ((...args: T) => boolean)[]): (...args: T) => boolean {
2159+
export function or<T extends unknown[], U>(...fs: ((...args: T) => U)[]): (...args: T) => U {
21602160
return (...args) => {
2161+
let lastResult: U;
21612162
for (const f of fs) {
2162-
if (f(...args)) {
2163-
return true;
2163+
lastResult = f(...args);
2164+
if (lastResult) {
2165+
return lastResult;
21642166
}
21652167
}
2166-
return false;
2168+
return lastResult!;
21672169
};
21682170
}
21692171

src/compiler/diagnosticMessages.json

+8
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,14 @@
14491449
"category": "Error",
14501450
"code": 1474
14511451
},
1452+
"Control what method is used to detect module-format JS files.": {
1453+
"category": "Message",
1454+
"code": 1475
1455+
},
1456+
"\"auto\": Treat files with imports, exports, import.meta, jsx (with jsx: react-jsx), or esm format (with module: node12+) as modules.": {
1457+
"category": "Message",
1458+
"code": 1476
1459+
},
14521460

14531461
"The types of '{0}' are incompatible between these types.": {
14541462
"category": "Error",

src/compiler/parser.ts

+100-57
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,41 @@ namespace ts {
6060
text.charCodeAt(start + 3) !== CharacterCodes.slash;
6161
}
6262

63+
/*@internal*/
64+
export function isFileProbablyExternalModule(sourceFile: SourceFile) {
65+
// Try to use the first top-level import/export when available, then
66+
// fall back to looking for an 'import.meta' somewhere in the tree if necessary.
67+
return forEach(sourceFile.statements, isAnExternalModuleIndicatorNode) ||
68+
getImportMetaIfNecessary(sourceFile);
69+
}
70+
71+
function isAnExternalModuleIndicatorNode(node: Node) {
72+
return hasModifierOfKind(node, SyntaxKind.ExportKeyword)
73+
|| isImportEqualsDeclaration(node) && isExternalModuleReference(node.moduleReference)
74+
|| isImportDeclaration(node)
75+
|| isExportAssignment(node)
76+
|| isExportDeclaration(node) ? node : undefined;
77+
}
78+
79+
function getImportMetaIfNecessary(sourceFile: SourceFile) {
80+
return sourceFile.flags & NodeFlags.PossiblyContainsImportMeta ?
81+
walkTreeForImportMeta(sourceFile) :
82+
undefined;
83+
}
84+
85+
function walkTreeForImportMeta(node: Node): Node | undefined {
86+
return isImportMeta(node) ? node : forEachChild(node, walkTreeForImportMeta);
87+
}
88+
89+
/** Do not use hasModifier inside the parser; it relies on parent pointers. Use this instead. */
90+
function hasModifierOfKind(node: Node, kind: SyntaxKind) {
91+
return some(node.modifiers, m => m.kind === kind);
92+
}
93+
94+
function isImportMeta(node: Node): boolean {
95+
return isMetaProperty(node) && node.keywordToken === SyntaxKind.ImportKeyword && node.name.escapedText === "meta";
96+
}
97+
6398
/**
6499
* Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes
65100
* stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; otherwise,
@@ -642,17 +677,46 @@ namespace ts {
642677
}
643678
}
644679

645-
export function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes = false, scriptKind?: ScriptKind): SourceFile {
680+
export interface CreateSourceFileOptions {
681+
languageVersion: ScriptTarget;
682+
/**
683+
* Controls the format the file is detected as - this can be derived from only the path
684+
* and files on disk, but needs to be done with a module resolution cache in scope to be performant.
685+
* This is usually `undefined` for compilations that do not have `moduleResolution` values of `node12` or `nodenext`.
686+
*/
687+
impliedNodeFormat?: ModuleKind.ESNext | ModuleKind.CommonJS;
688+
/**
689+
* Controls how module-y-ness is set for the given file. Usually the result of calling
690+
* `getSetExternalModuleIndicator` on a valid `CompilerOptions` object. If not present, the default
691+
* check specified by `isFileProbablyExternalModule` will be used to set the field.
692+
*/
693+
setExternalModuleIndicator?: (file: SourceFile) => void;
694+
}
695+
696+
function setExternalModuleIndicator(sourceFile: SourceFile) {
697+
sourceFile.externalModuleIndicator = isFileProbablyExternalModule(sourceFile);
698+
}
699+
700+
export function createSourceFile(fileName: string, sourceText: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, setParentNodes = false, scriptKind?: ScriptKind): SourceFile {
646701
tracing?.push(tracing.Phase.Parse, "createSourceFile", { path: fileName }, /*separateBeginAndEnd*/ true);
647702
performance.mark("beforeParse");
648703
let result: SourceFile;
649704

650705
perfLogger.logStartParseSourceFile(fileName);
706+
const {
707+
languageVersion,
708+
setExternalModuleIndicator: overrideSetExternalModuleIndicator,
709+
impliedNodeFormat: format
710+
} = typeof languageVersionOrOptions === "object" ? languageVersionOrOptions : ({ languageVersion: languageVersionOrOptions } as CreateSourceFileOptions);
651711
if (languageVersion === ScriptTarget.JSON) {
652-
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ScriptKind.JSON);
712+
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ScriptKind.JSON, noop);
653713
}
654714
else {
655-
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind);
715+
const setIndicator = format === undefined ? overrideSetExternalModuleIndicator : (file: SourceFile) => {
716+
file.impliedNodeFormat = format;
717+
return (overrideSetExternalModuleIndicator || setExternalModuleIndicator)(file);
718+
};
719+
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind, setIndicator);
656720
}
657721
perfLogger.logStopParseSourceFile();
658722

@@ -851,7 +915,7 @@ namespace ts {
851915
// attached to the EOF token.
852916
let parseErrorBeforeNextFinishedNode = false;
853917

854-
export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor | undefined, setParentNodes = false, scriptKind?: ScriptKind): SourceFile {
918+
export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor | undefined, setParentNodes = false, scriptKind?: ScriptKind, setExternalModuleIndicatorOverride?: (file: SourceFile) => void): SourceFile {
855919
scriptKind = ensureScriptKind(fileName, scriptKind);
856920
if (scriptKind === ScriptKind.JSON) {
857921
const result = parseJsonText(fileName, sourceText, languageVersion, syntaxCursor, setParentNodes);
@@ -867,7 +931,7 @@ namespace ts {
867931

868932
initializeState(fileName, sourceText, languageVersion, syntaxCursor, scriptKind);
869933

870-
const result = parseSourceFileWorker(languageVersion, setParentNodes, scriptKind);
934+
const result = parseSourceFileWorker(languageVersion, setParentNodes, scriptKind, setExternalModuleIndicatorOverride || setExternalModuleIndicator);
871935

872936
clearState();
873937

@@ -955,7 +1019,7 @@ namespace ts {
9551019
}
9561020

9571021
// Set source file so that errors will be reported with this file name
958-
const sourceFile = createSourceFile(fileName, ScriptTarget.ES2015, ScriptKind.JSON, /*isDeclaration*/ false, statements, endOfFileToken, sourceFlags);
1022+
const sourceFile = createSourceFile(fileName, ScriptTarget.ES2015, ScriptKind.JSON, /*isDeclaration*/ false, statements, endOfFileToken, sourceFlags, noop);
9591023

9601024
if (setParentNodes) {
9611025
fixupParentReferences(sourceFile);
@@ -1039,7 +1103,7 @@ namespace ts {
10391103
topLevel = true;
10401104
}
10411105

1042-
function parseSourceFileWorker(languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile {
1106+
function parseSourceFileWorker(languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind, setExternalModuleIndicator: (file: SourceFile) => void): SourceFile {
10431107
const isDeclarationFile = isDeclarationFileName(fileName);
10441108
if (isDeclarationFile) {
10451109
contextFlags |= NodeFlags.Ambient;
@@ -1054,7 +1118,7 @@ namespace ts {
10541118
Debug.assert(token() === SyntaxKind.EndOfFileToken);
10551119
const endOfFileToken = addJSDocComment(parseTokenNode<EndOfFileToken>());
10561120

1057-
const sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile, statements, endOfFileToken, sourceFlags);
1121+
const sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile, statements, endOfFileToken, sourceFlags, setExternalModuleIndicator);
10581122

10591123
// A member of ReadonlyArray<T> isn't assignable to a member of T[] (and prevents a direct cast) - but this is where we set up those members so they can be readonly in the future
10601124
processCommentPragmas(sourceFile as {} as PragmaContext, sourceText);
@@ -1213,28 +1277,42 @@ namespace ts {
12131277
setParentRecursive(rootNode, /*incremental*/ true);
12141278
}
12151279

1216-
function createSourceFile(fileName: string, languageVersion: ScriptTarget, scriptKind: ScriptKind, isDeclarationFile: boolean, statements: readonly Statement[], endOfFileToken: EndOfFileToken, flags: NodeFlags): SourceFile {
1280+
function createSourceFile(
1281+
fileName: string,
1282+
languageVersion: ScriptTarget,
1283+
scriptKind: ScriptKind,
1284+
isDeclarationFile: boolean,
1285+
statements: readonly Statement[],
1286+
endOfFileToken: EndOfFileToken,
1287+
flags: NodeFlags,
1288+
setExternalModuleIndicator: (sourceFile: SourceFile) => void): SourceFile {
12171289
// code from createNode is inlined here so createNode won't have to deal with special case of creating source files
12181290
// this is quite rare comparing to other nodes and createNode should be as fast as possible
12191291
let sourceFile = factory.createSourceFile(statements, endOfFileToken, flags);
12201292
setTextRangePosWidth(sourceFile, 0, sourceText.length);
1221-
setExternalModuleIndicator(sourceFile);
1293+
setFields(sourceFile);
12221294

12231295
// If we parsed this as an external module, it may contain top-level await
12241296
if (!isDeclarationFile && isExternalModule(sourceFile) && sourceFile.transformFlags & TransformFlags.ContainsPossibleTopLevelAwait) {
12251297
sourceFile = reparseTopLevelAwait(sourceFile);
1298+
setFields(sourceFile);
12261299
}
12271300

1228-
sourceFile.text = sourceText;
1229-
sourceFile.bindDiagnostics = [];
1230-
sourceFile.bindSuggestionDiagnostics = undefined;
1231-
sourceFile.languageVersion = languageVersion;
1232-
sourceFile.fileName = fileName;
1233-
sourceFile.languageVariant = getLanguageVariant(scriptKind);
1234-
sourceFile.isDeclarationFile = isDeclarationFile;
1235-
sourceFile.scriptKind = scriptKind;
1236-
12371301
return sourceFile;
1302+
1303+
function setFields(sourceFile: SourceFile) {
1304+
sourceFile.text = sourceText;
1305+
sourceFile.bindDiagnostics = [];
1306+
sourceFile.bindSuggestionDiagnostics = undefined;
1307+
sourceFile.languageVersion = languageVersion;
1308+
sourceFile.fileName = fileName;
1309+
sourceFile.languageVariant = getLanguageVariant(scriptKind);
1310+
sourceFile.isDeclarationFile = isDeclarationFile;
1311+
sourceFile.scriptKind = scriptKind;
1312+
1313+
setExternalModuleIndicator(sourceFile);
1314+
sourceFile.setExternalModuleIndicator = setExternalModuleIndicator;
1315+
}
12381316
}
12391317

12401318
function setContextFlag(val: boolean, flag: NodeFlags) {
@@ -7575,41 +7653,6 @@ namespace ts {
75757653
return withJSDoc(finishNode(node, pos), hasJSDoc);
75767654
}
75777655

7578-
function setExternalModuleIndicator(sourceFile: SourceFile) {
7579-
// Try to use the first top-level import/export when available, then
7580-
// fall back to looking for an 'import.meta' somewhere in the tree if necessary.
7581-
sourceFile.externalModuleIndicator =
7582-
forEach(sourceFile.statements, isAnExternalModuleIndicatorNode) ||
7583-
getImportMetaIfNecessary(sourceFile);
7584-
}
7585-
7586-
function isAnExternalModuleIndicatorNode(node: Node) {
7587-
return hasModifierOfKind(node, SyntaxKind.ExportKeyword)
7588-
|| isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)
7589-
|| isImportDeclaration(node)
7590-
|| isExportAssignment(node)
7591-
|| isExportDeclaration(node) ? node : undefined;
7592-
}
7593-
7594-
function getImportMetaIfNecessary(sourceFile: SourceFile) {
7595-
return sourceFile.flags & NodeFlags.PossiblyContainsImportMeta ?
7596-
walkTreeForExternalModuleIndicators(sourceFile) :
7597-
undefined;
7598-
}
7599-
7600-
function walkTreeForExternalModuleIndicators(node: Node): Node | undefined {
7601-
return isImportMeta(node) ? node : forEachChild(node, walkTreeForExternalModuleIndicators);
7602-
}
7603-
7604-
/** Do not use hasModifier inside the parser; it relies on parent pointers. Use this instead. */
7605-
function hasModifierOfKind(node: Node, kind: SyntaxKind) {
7606-
return some(node.modifiers, m => m.kind === kind);
7607-
}
7608-
7609-
function isImportMeta(node: Node): boolean {
7610-
return isMetaProperty(node) && node.keywordToken === SyntaxKind.ImportKeyword && node.name.escapedText === "meta";
7611-
}
7612-
76137656
const enum ParsingContext {
76147657
SourceElements, // Elements in source file
76157658
BlockStatements, // Statements in block
@@ -7652,7 +7695,7 @@ namespace ts {
76527695
currentToken = scanner.scan();
76537696
const jsDocTypeExpression = parseJSDocTypeExpression();
76547697

7655-
const sourceFile = createSourceFile("file.js", ScriptTarget.Latest, ScriptKind.JS, /*isDeclarationFile*/ false, [], factory.createToken(SyntaxKind.EndOfFileToken), NodeFlags.None);
7698+
const sourceFile = createSourceFile("file.js", ScriptTarget.Latest, ScriptKind.JS, /*isDeclarationFile*/ false, [], factory.createToken(SyntaxKind.EndOfFileToken), NodeFlags.None, noop);
76567699
const diagnostics = attachFileToDiagnostics(parseDiagnostics, sourceFile);
76577700
if (jsDocDiagnostics) {
76587701
sourceFile.jsDocDiagnostics = attachFileToDiagnostics(jsDocDiagnostics, sourceFile);
@@ -8698,7 +8741,7 @@ namespace ts {
86988741
if (sourceFile.statements.length === 0) {
86998742
// If we don't have any statements in the current source file, then there's no real
87008743
// way to incrementally parse. So just do a full parse instead.
8701-
return Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, /*syntaxCursor*/ undefined, /*setParentNodes*/ true, sourceFile.scriptKind);
8744+
return Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, /*syntaxCursor*/ undefined, /*setParentNodes*/ true, sourceFile.scriptKind, sourceFile.setExternalModuleIndicator);
87028745
}
87038746

87048747
// Make sure we're not trying to incrementally update a source file more than once. Once
@@ -8762,7 +8805,7 @@ namespace ts {
87628805
// inconsistent tree. Setting the parents on the new tree should be very fast. We
87638806
// will immediately bail out of walking any subtrees when we can see that their parents
87648807
// are already correct.
8765-
const result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /*setParentNodes*/ true, sourceFile.scriptKind);
8808+
const result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /*setParentNodes*/ true, sourceFile.scriptKind, sourceFile.setExternalModuleIndicator);
87668809
result.commentDirectives = getNewCommentDirectives(
87678810
sourceFile.commentDirectives,
87688811
result.commentDirectives,

0 commit comments

Comments
 (0)