diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index d865ebb717d..c00bd325b6a 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -33,6 +33,12 @@ Parses the given string of code with the options provided and returns an ESTree- ```ts interface ParseOptions { + /** + * Specify the `sourceType`. + * For more details, see https://github.com/typescript-eslint/typescript-eslint/pull/9121 + */ + sourceType?: SourceType; + /** * Prevents the parser from throwing an error if it receives an invalid AST from TypeScript. * This case only usually occurs when attempting to lint invalid code. diff --git a/packages/typescript-estree/src/create-program/createSourceFile.ts b/packages/typescript-estree/src/create-program/createSourceFile.ts index bb5bc9d7b8f..096264c5844 100644 --- a/packages/typescript-estree/src/create-program/createSourceFile.ts +++ b/packages/typescript-estree/src/create-program/createSourceFile.ts @@ -23,6 +23,7 @@ function createSourceFile(parseSettings: ParseSettings): ts.SourceFile { { languageVersion: ts.ScriptTarget.Latest, jsDocParsingMode: parseSettings.jsDocParsingMode, + setExternalModuleIndicator: parseSettings.setExternalModuleIndicator, }, /* setParentNodes */ true, getScriptKind(parseSettings.filePath, parseSettings.jsx), diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 2bae667104f..35783e0acbe 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,3 +1,5 @@ +import path from 'node:path'; + import debug from 'debug'; import * as ts from 'typescript'; @@ -46,6 +48,14 @@ export function createParseSettings( ? tsestreeOptions.tsconfigRootDir : process.cwd(); const passedLoggerFn = typeof tsestreeOptions.loggerFn === 'function'; + const filePath = ensureAbsolutePath( + typeof tsestreeOptions.filePath === 'string' && + tsestreeOptions.filePath !== '' + ? tsestreeOptions.filePath + : getFileName(tsestreeOptions.jsx), + tsconfigRootDir, + ); + const extension = path.extname(filePath).toLowerCase() as ts.Extension; const jsDocParsingMode = ((): ts.JSDocParsingMode => { switch (tsestreeOptions.jsDocParsingMode) { case 'all': @@ -81,13 +91,17 @@ export function createParseSettings( tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string') ? tsestreeOptions.extraFileExtensions : [], - filePath: ensureAbsolutePath( - typeof tsestreeOptions.filePath === 'string' && - tsestreeOptions.filePath !== '' - ? tsestreeOptions.filePath - : getFileName(tsestreeOptions.jsx), - tsconfigRootDir, - ), + filePath, + setExternalModuleIndicator: + tsestreeOptions.sourceType === 'module' || + (tsestreeOptions.sourceType === undefined && + extension === ts.Extension.Mjs) || + (tsestreeOptions.sourceType === undefined && + extension === ts.Extension.Mts) + ? (file): void => { + file.externalModuleIndicator = true; + } + : undefined, jsDocParsingMode, jsx: tsestreeOptions.jsx === true, loc: tsestreeOptions.loc === true, diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 41a21c04494..2e9b341d3dc 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -72,6 +72,14 @@ export interface MutableParseSettings { */ filePath: string; + /** + * Sets the external module indicator on the source file. + * Used by Typescript to determine if a sourceFile is an external module. + * + * needed to always parsing `mjs`/`mts` files as ESM + */ + setExternalModuleIndicator?: (file: ts.SourceFile) => void; + /** * JSDoc parsing style to pass through to TypeScript */ diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 55f4d3b05eb..44af134611c 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -3,6 +3,7 @@ import type { DebugLevel, JSDocParsingMode, ProjectServiceOptions, + SourceType, } from '@typescript-eslint/types'; import type * as ts from 'typescript'; @@ -15,6 +16,12 @@ export type { ProjectServiceOptions } from '@typescript-eslint/types'; ////////////////////////////////////////////////////////// interface ParseOptions { + /** + * Specify the `sourceType`. + * For more details, see https://github.com/typescript-eslint/typescript-eslint/pull/9121 + */ + sourceType?: SourceType; + /** * Prevents the parser from throwing an error if it receives an invalid AST from TypeScript. * This case only usually occurs when attempting to lint invalid code. diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index dc750b9f9b6..fe9883ffe5a 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -342,6 +342,134 @@ describe('parseAndGenerateServices', () => { }); }); + describe('ESM parsing', () => { + describe('TLA(Top Level Await)', () => { + const config: TSESTreeOptions = { + projectService: false, + comment: true, + tokens: true, + range: true, + loc: true, + }; + const code = 'await(1)'; + + const testParse = ({ + sourceType, + ext, + shouldAllowTLA = false, + }: { + sourceType?: 'module' | 'script'; + ext: '.js' | '.ts' | '.mjs' | '.mts'; + shouldAllowTLA?: boolean; + }): void => { + const ast = parser.parse(code, { + ...config, + sourceType, + filePath: `file${ext}`, + }); + const expressionType = ( + ast.body[0] as parser.TSESTree.ExpressionStatement + ).expression.type; + + it(`parse(): should ${ + shouldAllowTLA ? 'allow' : 'not allow' + } TLA for ${ext} file with sourceType = ${sourceType}`, () => { + expect(expressionType).toBe( + shouldAllowTLA + ? parser.AST_NODE_TYPES.AwaitExpression + : parser.AST_NODE_TYPES.CallExpression, + ); + }); + }; + const testParseAndGenerateServices = ({ + sourceType, + ext, + shouldAllowTLA = false, + }: { + sourceType?: 'module' | 'script'; + ext: '.js' | '.ts' | '.mjs' | '.mts'; + shouldAllowTLA?: boolean; + }): void => { + const result = parser.parseAndGenerateServices(code, { + ...config, + sourceType, + filePath: `file${ext}`, + }); + const expressionType = ( + result.ast.body[0] as parser.TSESTree.ExpressionStatement + ).expression.type; + + it(`parseAndGenerateServices(): should ${ + shouldAllowTLA ? 'allow' : 'not allow' + } TLA for ${ext} file with sourceType = ${sourceType}`, () => { + expect(expressionType).toBe( + shouldAllowTLA + ? parser.AST_NODE_TYPES.AwaitExpression + : parser.AST_NODE_TYPES.CallExpression, + ); + }); + }; + + testParse({ ext: '.js' }); + testParse({ ext: '.ts' }); + testParse({ ext: '.mjs', shouldAllowTLA: true }); + testParse({ ext: '.mts', shouldAllowTLA: true }); + + testParse({ sourceType: 'module', ext: '.js', shouldAllowTLA: true }); + testParse({ sourceType: 'module', ext: '.ts', shouldAllowTLA: true }); + testParse({ sourceType: 'module', ext: '.mjs', shouldAllowTLA: true }); + testParse({ sourceType: 'module', ext: '.mts', shouldAllowTLA: true }); + + testParse({ sourceType: 'script', ext: '.js' }); + testParse({ sourceType: 'script', ext: '.ts' }); + testParse({ sourceType: 'script', ext: '.mjs' }); + testParse({ sourceType: 'script', ext: '.mts' }); + + testParseAndGenerateServices({ ext: '.js' }); + testParseAndGenerateServices({ ext: '.ts' }); + testParseAndGenerateServices({ ext: '.mjs', shouldAllowTLA: true }); + testParseAndGenerateServices({ ext: '.mts', shouldAllowTLA: true }); + + testParseAndGenerateServices({ + sourceType: 'module', + ext: '.js', + shouldAllowTLA: true, + }); + testParseAndGenerateServices({ + sourceType: 'module', + ext: '.ts', + shouldAllowTLA: true, + }); + testParseAndGenerateServices({ + sourceType: 'module', + ext: '.mjs', + shouldAllowTLA: true, + }); + testParseAndGenerateServices({ + sourceType: 'module', + ext: '.mts', + shouldAllowTLA: true, + }); + + testParseAndGenerateServices({ + sourceType: 'script', + ext: '.js', + }); + testParseAndGenerateServices({ + sourceType: 'script', + ext: '.ts', + }); + testParseAndGenerateServices({ + sourceType: 'script', + ext: '.mjs', + }); + testParseAndGenerateServices({ + sourceType: 'script', + ext: '.mts', + }); + }); + }); + if (process.env.TYPESCRIPT_ESLINT_PROJECT_SERVICE !== 'true') { describe('invalid file error messages', () => { const PROJECT_DIR = resolve(FIXTURES_DIR, '../invalidFileErrors'); diff --git a/packages/typescript-estree/typings/typescript.d.ts b/packages/typescript-estree/typings/typescript.d.ts index 8d370d1ea69..039c9a06259 100644 --- a/packages/typescript-estree/typings/typescript.d.ts +++ b/packages/typescript-estree/typings/typescript.d.ts @@ -3,7 +3,7 @@ import 'typescript'; // these additions are marked as internal to typescript declare module 'typescript' { interface SourceFile { - externalModuleIndicator?: Node; + externalModuleIndicator?: Node | true; parseDiagnostics: DiagnosticWithLocation[]; }