From 4a1e81be197f303405e3b9bcab5d46c8d9714ae5 Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Sun, 19 May 2024 19:49:33 +0900 Subject: [PATCH 1/8] change mjs/mts files to always be parsed by ESM --- packages/typescript-estree/src/parser.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index d724d81e379d..d15494710980 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -1,5 +1,6 @@ import debug from 'debug'; -import type * as ts from 'typescript'; +import path from 'path'; +import * as ts from 'typescript'; import { astConverter } from './ast-converter'; import { convertError } from './convert'; @@ -125,11 +126,28 @@ function parse( return ast; } +function isESM( + options: T | undefined, +): boolean { + const extension = path + .extname(options?.filePath ?? '') + .toLowerCase() as ts.Extension; + + return extension === ts.Extension.Mjs || extension === ts.Extension.Mts; +} + function parseWithNodeMapsInternal( code: ts.SourceFile | string, options: T | undefined, shouldPreserveNodeMaps: boolean, ): ParseWithNodeMapsResult { + /** + * Ensure that `mjs`/`mts` files are always treated as ESM + */ + if (typeof code === 'string' && isESM(options)) { + code += '\n' + 'export {}'; + } + /** * Reset the parse configuration */ From e98727bc475aa4ad6a1b185fa2566c134dafb7cc Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Wed, 22 May 2024 13:04:34 +0900 Subject: [PATCH 2/8] revert wrong approach --- packages/typescript-estree/src/parser.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index d15494710980..d724d81e379d 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -1,6 +1,5 @@ import debug from 'debug'; -import path from 'path'; -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import { astConverter } from './ast-converter'; import { convertError } from './convert'; @@ -126,28 +125,11 @@ function parse( return ast; } -function isESM( - options: T | undefined, -): boolean { - const extension = path - .extname(options?.filePath ?? '') - .toLowerCase() as ts.Extension; - - return extension === ts.Extension.Mjs || extension === ts.Extension.Mts; -} - function parseWithNodeMapsInternal( code: ts.SourceFile | string, options: T | undefined, shouldPreserveNodeMaps: boolean, ): ParseWithNodeMapsResult { - /** - * Ensure that `mjs`/`mts` files are always treated as ESM - */ - if (typeof code === 'string' && isESM(options)) { - code += '\n' + 'export {}'; - } - /** * Reset the parse configuration */ From 0027099d81673eadd465bfd52f82c32d163b560d Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Wed, 22 May 2024 13:25:50 +0900 Subject: [PATCH 3/8] change mjs/mts files to always be parsed as ESM (using setExternalModuleIndicator) --- .../src/create-program/createSourceFile.ts | 1 + .../src/parseSettings/createParseSettings.ts | 21 +++++++++++++------ .../src/parseSettings/index.ts | 8 +++++++ .../typescript-estree/typings/typescript.d.ts | 2 +- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createSourceFile.ts b/packages/typescript-estree/src/create-program/createSourceFile.ts index bb5bc9d7b8fc..096264c5844f 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 b5d52ce569b8..4835fefab95d 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,4 +1,5 @@ import debug from 'debug'; +import path from 'path'; import * as ts from 'typescript'; import type { ProjectServiceSettings } from '../create-program/createProjectService'; @@ -46,6 +47,13 @@ export function createParseSettings( ? options.tsconfigRootDir : process.cwd(); const passedLoggerFn = typeof options.loggerFn === 'function'; + const filePath = ensureAbsolutePath( + typeof options.filePath === 'string' && options.filePath !== '' + ? options.filePath + : getFileName(options.jsx), + tsconfigRootDir, + ); + const extension = path.extname(filePath).toLowerCase() as ts.Extension; const jsDocParsingMode = ((): ts.JSDocParsingMode => { switch (options.jsDocParsingMode) { case 'all': @@ -96,12 +104,13 @@ export function createParseSettings( options.extraFileExtensions.every(ext => typeof ext === 'string') ? options.extraFileExtensions : [], - filePath: ensureAbsolutePath( - typeof options.filePath === 'string' && options.filePath !== '' - ? options.filePath - : getFileName(options.jsx), - tsconfigRootDir, - ), + filePath: filePath, + setExternalModuleIndicator: + extension === ts.Extension.Mjs || extension === ts.Extension.Mts + ? (file): void => { + file.externalModuleIndicator = true; + } + : undefined, jsDocParsingMode, jsx: options.jsx === true, loc: options.loc === true, diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 3511e3be718d..c2a16d4805a5 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -93,6 +93,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/typings/typescript.d.ts b/packages/typescript-estree/typings/typescript.d.ts index de19fdb5a447..bf67bca069a2 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[]; } From 7172c055c3622b75c9e0bf3e0a7b0a104f40fbe3 Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Mon, 16 Sep 2024 09:38:15 +0900 Subject: [PATCH 4/8] changed `sourceType` options to be imported from a config file --- packages/parser/src/parser.ts | 1 + .../src/parseSettings/createParseSettings.ts | 4 +++- packages/typescript-estree/src/parser-options.ts | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 6aa369134a07..f1aafb5b88b8 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -105,6 +105,7 @@ function parseForESLint( const parserOptions: TSESTreeOptions = {}; Object.assign(parserOptions, options, { + sourceType: options.sourceType, jsx: validateBoolean(options.ecmaFeatures.jsx), /** * Override errorOnTypeScriptSyntacticAndSemanticIssues and set it to false to prevent use from user config diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 4835fefab95d..84da5e753a23 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -106,7 +106,9 @@ export function createParseSettings( : [], filePath: filePath, setExternalModuleIndicator: - extension === ts.Extension.Mjs || extension === ts.Extension.Mts + options.sourceType === 'module' || + (options.sourceType === undefined && extension === ts.Extension.Mjs) || + (options.sourceType === undefined && extension === ts.Extension.Mts) ? (file): void => { file.externalModuleIndicator = true; } diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 8211f40f5446..02383bdefb14 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -2,6 +2,7 @@ import type { CacheDurationSeconds, DebugLevel, JSDocParsingMode, + SourceType, } from '@typescript-eslint/types'; import type * as ts from 'typescript'; @@ -12,6 +13,12 @@ import type { TSESTree, TSESTreeToTSNode, TSNode, TSToken } from './ts-estree'; ////////////////////////////////////////////////////////// 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. From b34e53f70db2d71adc187da7e1a5356b957bb52f Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Mon, 16 Sep 2024 14:03:48 +0900 Subject: [PATCH 5/8] write test code (`typescript-estree/tests/lib/parse.test.ts`) --- .../typescript-estree/tests/lib/parse.test.ts | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index f973358d85a1..f6e6d8af30b1 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -340,6 +340,135 @@ describe('parseAndGenerateServices', () => { }); }); + describe('ESM parsing', () => { + describe('TLA(Top Level Await)', () => { + const config: TSESTreeOptions = { + EXPERIMENTAL_useProjectService: 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}`, + project: './tsconfig.json', + }); + 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_EXPERIMENTAL_TSSERVER !== 'true') { describe('invalid file error messages', () => { const PROJECT_DIR = resolve(FIXTURES_DIR, '../invalidFileErrors'); From d369ec8846cae1d5ac4bcff5d0bc257347077560 Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Mon, 16 Sep 2024 17:01:36 +0900 Subject: [PATCH 6/8] change the test to simply not set the `project` property on the `parseAndGeneratedServices` test --- packages/typescript-estree/tests/lib/parse.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index f6e6d8af30b1..0f8d892f3b29 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -392,7 +392,6 @@ describe('parseAndGenerateServices', () => { ...config, sourceType, filePath: `file${ext}`, - project: './tsconfig.json', }); const expressionType = ( result.ast.body[0] as parser.TSESTree.ExpressionStatement From fa9d25b10e909be7b9eae54cdd680e5ef77d3824 Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Mon, 30 Sep 2024 23:44:58 +0900 Subject: [PATCH 7/8] doc: update `TypesScript_ESTree`'s `ParseOptions` --- docs/packages/TypeScript_ESTree.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index eb6a49c8a1bd..b5d22e3a08c5 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -31,6 +31,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. From 3bdee134ea04664524c9e27bd5730664b060ffb5 Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Wed, 2 Oct 2024 17:01:50 +0900 Subject: [PATCH 8/8] fix eslint error --- .../src/parseSettings/createParseSettings.ts | 5 +++-- packages/typescript-estree/src/parser-options.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 971d87f7346d..35783e0acbe5 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,5 +1,6 @@ +import path from 'node:path'; + import debug from 'debug'; -import path from 'path'; import * as ts from 'typescript'; import type { ProjectServiceSettings } from '../create-program/createProjectService'; @@ -90,7 +91,7 @@ export function createParseSettings( tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string') ? tsestreeOptions.extraFileExtensions : [], - filePath: filePath, + filePath, setExternalModuleIndicator: tsestreeOptions.sourceType === 'module' || (tsestreeOptions.sourceType === undefined && diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 832868286d0b..44af134611c2 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -2,8 +2,8 @@ import type { CacheDurationSeconds, DebugLevel, JSDocParsingMode, - SourceType, ProjectServiceOptions, + SourceType, } from '@typescript-eslint/types'; import type * as ts from 'typescript';