From 69aa3c984fb28b92a34987014a8cba154cf9262c Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Fri, 7 Jun 2024 20:03:33 -0600 Subject: [PATCH 01/19] refactor(typescript-estree): refactors default project config and adds initial tests --- .../create-program/createProjectService.ts | 40 +++---- .../src/create-program/getParsedConfigFile.ts | 56 ++++++++++ .../src/create-program/useProvidedPrograms.ts | 49 ++------- .../project-a/src/index.ts | 0 .../project-a/tsconfig.json | 5 + .../project-a/tsconfig.src.json | 0 .../project-b/src/index.ts | 0 .../project-b/tsconfig.json | 5 + .../project-b/tsconfig.src.json | 9 ++ .../projectServicesComplex/tsconfig.base.json | 9 ++ .../projectServicesComplex/tsconfig.json | 8 ++ .../tsconfig.overridden.json | 3 + .../tsconfig.overrides.json | 5 + .../tests/lib/createProjectService.test.ts | 100 ++++++++++++++---- 14 files changed, 204 insertions(+), 85 deletions(-) create mode 100644 packages/typescript-estree/src/create-program/getParsedConfigFile.ts create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/src/index.ts create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.json create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/src/index.ts create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.json create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.src.json create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.base.json create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.json create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overridden.json create mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overrides.json diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 9f0f44dcec68..0c1831e2231f 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ +import fs from 'node:fs'; import os from 'node:os'; +import path from 'node:path'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { ProjectServiceOptions } from '../parser-options'; import { validateDefaultProjectForFilesGlob } from './validateDefaultProjectForFilesGlob'; +import { CORE_COMPILER_OPTIONS } from './shared'; +import { getParsedConfigFile } from './getParsedConfigFile'; const DEFAULT_PROJECT_MATCHED_FILES_THRESHOLD = 8; @@ -69,39 +73,25 @@ export function createProjectService( }); if (options.defaultProject) { - let configRead; - try { - configRead = tsserver.readConfigFile( + const configFile = getParsedConfigFile( options.defaultProject, - system.readFile, + path.dirname(options.defaultProject), + ); + service.setCompilerOptionsForInferredProjects( + // NOTE: The inferred projects APIs are not intended for source files when a tsconfig exists. + // There is no API that generates InferredProjectCompilerOptions suggesting it is meant for + // hard coded options passed in to be type checked. The original readConfigFile.config is + // type any and all available config parsing methods generate a CompilerOptions type. Hard + // casting as a work around. + // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 + configFile.options as any, // providing CompilerOptions while expecting InferredProjectCompilerOptions ); } catch (error) { throw new Error( `Could not parse default project '${options.defaultProject}': ${(error as Error).message}`, ); } - - if (configRead.error) { - throw new Error( - `Could not read default project '${options.defaultProject}': ${tsserver.formatDiagnostic( - configRead.error, - { - getCurrentDirectory: system.getCurrentDirectory, - getCanonicalFileName: fileName => fileName, - getNewLine: () => os.EOL, - }, - )}`, - ); - } - - service.setCompilerOptionsForInferredProjects( - ( - configRead.config as { - compilerOptions: ts.server.protocol.InferredProjectCompilerOptions; - } - ).compilerOptions, - ); } return { diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts new file mode 100644 index 000000000000..e6997f41009f --- /dev/null +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -0,0 +1,56 @@ +import * as ts from 'typescript'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { CORE_COMPILER_OPTIONS } from './shared'; + +/** + * Utility offered by parser to help consumers read a config file. + * + * @param configFilePath the path to the tsconfig.json file, relative to `projectDirectory` + * @param projectDirectory the project directory to use as the CWD, defaults to `process.cwd()` + */ +function getParsedConfigFile( + configFilePath: string, + projectDirectory?: string, +): ts.ParsedCommandLine { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (ts.sys === undefined) { + throw new Error( + '`createProgramFromConfigFile` is only supported in a Node-like environment.', + ); + } + + const parsed = ts.getParsedCommandLineOfConfigFile( + configFilePath, + CORE_COMPILER_OPTIONS, + { + onUnRecoverableConfigFileDiagnostic: diag => { + throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined. + }, + fileExists: fs.existsSync, + getCurrentDirectory: () => + (projectDirectory && path.resolve(projectDirectory)) || process.cwd(), + readDirectory: ts.sys.readDirectory, + readFile: file => fs.readFileSync(file, 'utf-8'), + useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, + }, + ); + // parsed is not undefined, since we throw on failure. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = parsed!; + if (result.errors.length) { + throw new Error(formatDiagnostics(result.errors)); + } + return result; +} + +function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { + return ts.formatDiagnostics(diagnostics, { + getCanonicalFileName: f => f, + getCurrentDirectory: process.cwd, + getNewLine: () => '\n', + }); +} + +export { getParsedConfigFile }; diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index cae07bda5fa1..67249355322f 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -1,10 +1,10 @@ import debug from 'debug'; -import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; import type { ASTAndDefiniteProgram } from './shared'; -import { CORE_COMPILER_OPTIONS, getAstFromProgram } from './shared'; +import { getAstFromProgram } from './shared'; +import { getParsedConfigFile } from './getParsedConfigFile'; const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); @@ -49,51 +49,16 @@ function useProvidedPrograms( /** * Utility offered by parser to help consumers construct their own program instance. * - * @param configFile the path to the tsconfig.json file, relative to `projectDirectory` + * @param configFilePath the path to the tsconfig.json file, relative to `projectDirectory` * @param projectDirectory the project directory to use as the CWD, defaults to `process.cwd()` */ function createProgramFromConfigFile( - configFile: string, + configFilePath: string, projectDirectory?: string, ): ts.Program { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (ts.sys === undefined) { - throw new Error( - '`createProgramFromConfigFile` is only supported in a Node-like environment.', - ); - } - - const parsed = ts.getParsedCommandLineOfConfigFile( - configFile, - CORE_COMPILER_OPTIONS, - { - onUnRecoverableConfigFileDiagnostic: diag => { - throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined. - }, - fileExists: fs.existsSync, - getCurrentDirectory: () => - (projectDirectory && path.resolve(projectDirectory)) || process.cwd(), - readDirectory: ts.sys.readDirectory, - readFile: file => fs.readFileSync(file, 'utf-8'), - useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, - }, - ); - // parsed is not undefined, since we throw on failure. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const result = parsed!; - if (result.errors.length) { - throw new Error(formatDiagnostics(result.errors)); - } - const host = ts.createCompilerHost(result.options, true); - return ts.createProgram(result.fileNames, result.options, host); -} - -function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { - return ts.formatDiagnostics(diagnostics, { - getCanonicalFileName: f => f, - getCurrentDirectory: process.cwd, - getNewLine: () => '\n', - }); + const configFile = getParsedConfigFile(configFilePath, projectDirectory); + const host = ts.createCompilerHost(configFile.options, true); + return ts.createProgram(configFile.fileNames, configFile.options, host); } export { useProvidedPrograms, createProgramFromConfigFile }; diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/src/index.ts b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/src/index.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.json new file mode 100644 index 000000000000..144eca86d81d --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": [], + "references": [{ "path": "./tsconfig.src.json" }] +} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/src/index.ts b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/src/index.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.json new file mode 100644 index 000000000000..144eca86d81d --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": [], + "references": [{ "path": "./tsconfig.src.json" }] +} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.src.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.src.json new file mode 100644 index 000000000000..35eec1467e18 --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.src.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["./src"], + "compilerOptions": { + "paths": { + "@": ["./project-b/src/index.ts"] + } + } +} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.base.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.base.json new file mode 100644 index 000000000000..1558f0441589 --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.base.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "paths": { + "@/package-a": ["./project-a/src/index.ts"], + "@/package-b": ["./project-b/src/index.ts"] + } + } +} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.json new file mode 100644 index 000000000000..f5573dac6eda --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "include": [], + "references": [ + { "path": "./packages/package-a" }, + { "path": "./packages/package-b" } + ] +} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overridden.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overridden.json new file mode 100644 index 000000000000..588708a2b74e --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overridden.json @@ -0,0 +1,3 @@ +{ + "extends": ["./tsconfig.base.json", "./tsconfig.overrides.json"] +} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overrides.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overrides.json new file mode 100644 index 000000000000..8ad9c98014c7 --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overrides.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strict": false + } +} diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 1159e8625f41..28a12da24e42 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -1,22 +1,32 @@ import * as ts from 'typescript'; +import fs from 'node:fs'; +import { join } from 'node:path'; +import { CORE_COMPILER_OPTIONS } from '../../src/create-program/shared'; import { createProjectService } from '../../src/create-program/createProjectService'; -const mockReadConfigFile = jest.fn(); +const FIXTURES_DIR = join(__dirname, '../fixtures/projectServicesComplex'); + +const mockGetParsedCommandLineOfConfigFile = jest.fn(); const mockSetCompilerOptionsForInferredProjects = jest.fn(); -jest.mock('typescript/lib/tsserverlibrary', () => ({ - ...jest.requireActual('typescript/lib/tsserverlibrary'), - readConfigFile: mockReadConfigFile, - server: { - ProjectService: class { - setCompilerOptionsForInferredProjects = - mockSetCompilerOptionsForInferredProjects; +const useMock = () => + jest.mock('typescript/lib/tsserverlibrary', () => ({ + ...jest.requireActual('typescript/lib/tsserverlibrary'), + getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, + server: { + ProjectService: class { + setCompilerOptionsForInferredProjects = + mockSetCompilerOptionsForInferredProjects; + }, }, - }, -})); + })); describe('createProjectService', () => { + beforeEach(() => { + jest.resetModules(); + }); + it('sets allowDefaultProject when options.allowDefaultProject is defined', () => { const allowDefaultProject = ['./*.js']; const settings = createProjectService({ allowDefaultProject }, undefined); @@ -31,7 +41,8 @@ describe('createProjectService', () => { }); it('throws an error when options.defaultProject is set and readConfigFile returns an error', () => { - mockReadConfigFile.mockReturnValue({ + useMock(); + mockGetParsedCommandLineOfConfigFile.mockReturnValue({ error: { category: ts.DiagnosticCategory.Error, code: 1234, @@ -58,7 +69,8 @@ describe('createProjectService', () => { }); it('throws an error when options.defaultProject is set and readConfigFile throws an error', () => { - mockReadConfigFile.mockImplementation(() => { + useMock(); + mockGetParsedCommandLineOfConfigFile.mockImplementation(() => { throw new Error('Oh no!'); }); @@ -73,20 +85,72 @@ describe('createProjectService', () => { ).toThrow("Could not parse default project './tsconfig.json': Oh no!"); }); - it('uses the default projects compiler options when options.defaultProject is set and readConfigFile succeeds', () => { - const compilerOptions = { strict: true }; - mockReadConfigFile.mockReturnValue({ config: { compilerOptions } }); + it('uses the default projects compiler options when options.defaultProject is set and getParsedCommandLineOfConfigFile succeeds', () => { + const base = JSON.parse( + fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), + ); + const compilerOptions = base?.compilerOptions; + + const { service } = createProjectService( + { + defaultProject: join(FIXTURES_DIR, 'tsconfig.base.json'), + }, + undefined, + ); + + expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( + // Looser assertion since config parser tacks on some meta data to track references to other files. + // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L2888 + // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L3125 + expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), + ); + }); + + it('uses the default projects compiler options when options.defaultProject is set with a single extends', () => { + const base = JSON.parse( + fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), + ); + const compilerOptions = base?.compilerOptions; + + const { service } = createProjectService( + { + defaultProject: join(FIXTURES_DIR, 'tsconfig.json'), + }, + undefined, + ); + + expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( + // Looser assertion since config parser tacks on some meta data to track references to other files. + // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L2888 + // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L3125 + expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), + ); + }); + + it('uses the default projects compiler options when options.defaultProject is set with multiple extends', () => { + const base = JSON.parse( + fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), + ); + const overrides = JSON.parse( + fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.overrides.json'), 'utf8'), + ); + const compilerOptions = { + ...base?.compilerOptions, + ...overrides?.compilerOptions, + }; const { service } = createProjectService( { - allowDefaultProject: ['file.js'], - defaultProject: './tsconfig.json', + defaultProject: join(FIXTURES_DIR, 'tsconfig.overridden.json'), }, undefined, ); expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - compilerOptions, + // Looser assertion since config parser tacks on some meta data to track references to other files. + // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L2888 + // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L3125 + expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), ); }); }); From d32c1e5eedd3ad33e85be24daaf7ec285ffc956a Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 8 Jun 2024 14:08:34 -0600 Subject: [PATCH 02/19] test(typescript-estree): fix original failing unit tests --- .../create-program/createProjectService.ts | 8 +- .../src/create-program/getParsedConfigFile.ts | 33 +++++--- .../src/create-program/useProvidedPrograms.ts | 2 +- .../tests/lib/createProjectService.test.ts | 75 +++++++++++-------- 4 files changed, 69 insertions(+), 49 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 0c1831e2231f..a33bb2c7435a 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -1,14 +1,11 @@ /* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ -import fs from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { ProjectServiceOptions } from '../parser-options'; -import { validateDefaultProjectForFilesGlob } from './validateDefaultProjectForFilesGlob'; -import { CORE_COMPILER_OPTIONS } from './shared'; import { getParsedConfigFile } from './getParsedConfigFile'; +import { validateDefaultProjectForFilesGlob } from './validateDefaultProjectForFilesGlob'; const DEFAULT_PROJECT_MATCHED_FILES_THRESHOLD = 8; @@ -35,6 +32,7 @@ export function createProjectService( // We import this lazily to avoid its cost for users who don't use the service // TODO: Once we drop support for TS<5.3 we can import from "typescript" directly + // NOTE: Jest mock's rely on this behavior. // eslint-disable-next-line @typescript-eslint/no-require-imports const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts; @@ -71,7 +69,6 @@ export function createProjectService( session: undefined, jsDocParsingMode, }); - if (options.defaultProject) { try { const configFile = getParsedConfigFile( @@ -85,6 +82,7 @@ export function createProjectService( // type any and all available config parsing methods generate a CompilerOptions type. Hard // casting as a work around. // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any configFile.options as any, // providing CompilerOptions while expecting InferredProjectCompilerOptions ); } catch (error) { diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts index e6997f41009f..912b5319b4c0 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -1,6 +1,6 @@ -import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; +import type * as ts from 'typescript/lib/tsserverlibrary'; import { CORE_COMPILER_OPTIONS } from './shared'; @@ -14,14 +14,20 @@ function getParsedConfigFile( configFilePath: string, projectDirectory?: string, ): ts.ParsedCommandLine { + // We import this lazily to avoid its cost for users who don't use the service + // TODO: Once we drop support for TS<5.3 we can import from "typescript" directly + // NOTE: Jest mock's rely on this behavior. + // eslint-disable-next-line @typescript-eslint/no-require-imports + const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (ts.sys === undefined) { + if (tsserver.sys === undefined) { throw new Error( '`createProgramFromConfigFile` is only supported in a Node-like environment.', ); } - const parsed = ts.getParsedCommandLineOfConfigFile( + const parsed = tsserver.getParsedCommandLineOfConfigFile( configFilePath, CORE_COMPILER_OPTIONS, { @@ -31,26 +37,29 @@ function getParsedConfigFile( fileExists: fs.existsSync, getCurrentDirectory: () => (projectDirectory && path.resolve(projectDirectory)) || process.cwd(), - readDirectory: ts.sys.readDirectory, + readDirectory: tsserver.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf-8'), - useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, + useCaseSensitiveFileNames: tsserver.sys.useCaseSensitiveFileNames, }, ); + // parsed is not undefined, since we throw on failure. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const result = parsed!; if (result.errors.length) { throw new Error(formatDiagnostics(result.errors)); } + return result; -} -function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { - return ts.formatDiagnostics(diagnostics, { - getCanonicalFileName: f => f, - getCurrentDirectory: process.cwd, - getNewLine: () => '\n', - }); + // scoped to parent function to use lazy typescript import + function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { + return tsserver.formatDiagnostics(diagnostics, { + getCanonicalFileName: f => f, + getCurrentDirectory: process.cwd, + getNewLine: () => '\n', + }); + } } export { getParsedConfigFile }; diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index 67249355322f..00b20fc47e23 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -2,9 +2,9 @@ import debug from 'debug'; import * as path from 'path'; import * as ts from 'typescript'; +import { getParsedConfigFile } from './getParsedConfigFile'; import type { ASTAndDefiniteProgram } from './shared'; import { getAstFromProgram } from './shared'; -import { getParsedConfigFile } from './getParsedConfigFile'; const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 28a12da24e42..3600c24585c9 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -1,30 +1,34 @@ -import * as ts from 'typescript'; import fs from 'node:fs'; import { join } from 'node:path'; -import { CORE_COMPILER_OPTIONS } from '../../src/create-program/shared'; +import * as ts from 'typescript'; + import { createProjectService } from '../../src/create-program/createProjectService'; +import { CORE_COMPILER_OPTIONS } from '../../src/create-program/shared'; const FIXTURES_DIR = join(__dirname, '../fixtures/projectServicesComplex'); const mockGetParsedCommandLineOfConfigFile = jest.fn(); const mockSetCompilerOptionsForInferredProjects = jest.fn(); -const useMock = () => - jest.mock('typescript/lib/tsserverlibrary', () => ({ - ...jest.requireActual('typescript/lib/tsserverlibrary'), - getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, - server: { - ProjectService: class { - setCompilerOptionsForInferredProjects = - mockSetCompilerOptionsForInferredProjects; - }, +const origGetParsedCommandLineOfConfigFile = + ts.getParsedCommandLineOfConfigFile; + +jest.mock('typescript/lib/tsserverlibrary', () => ({ + ...jest.requireActual('typescript/lib/tsserverlibrary'), + getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, + server: { + ProjectService: class { + setCompilerOptionsForInferredProjects = + mockSetCompilerOptionsForInferredProjects; }, - })); + }, +})); describe('createProjectService', () => { - beforeEach(() => { - jest.resetModules(); + // not strictly needed but removes the dependency on tests running in a specific order + afterEach(() => { + jest.resetAllMocks(); }); it('sets allowDefaultProject when options.allowDefaultProject is defined', () => { @@ -41,18 +45,19 @@ describe('createProjectService', () => { }); it('throws an error when options.defaultProject is set and readConfigFile returns an error', () => { - useMock(); mockGetParsedCommandLineOfConfigFile.mockReturnValue({ - error: { - category: ts.DiagnosticCategory.Error, - code: 1234, - file: ts.createSourceFile('./tsconfig.json', '', { - languageVersion: ts.ScriptTarget.Latest, - }), - start: 0, - length: 0, - messageText: 'Oh no!', - } satisfies ts.Diagnostic, + errors: [ + { + category: ts.DiagnosticCategory.Error, + code: 1234, + file: ts.createSourceFile('./tsconfig.json', '', { + languageVersion: ts.ScriptTarget.Latest, + }), + start: 0, + length: 0, + messageText: 'Oh no!', + }, + ] satisfies ts.Diagnostic[], }); expect(() => @@ -64,28 +69,30 @@ describe('createProjectService', () => { undefined, ), ).toThrow( - /Could not read default project '\.\/tsconfig.json': .+ error TS1234: Oh no!/, + /Could not parse default project '\.\/tsconfig.json': .+ error TS1234: Oh no!/, ); }); it('throws an error when options.defaultProject is set and readConfigFile throws an error', () => { - useMock(); mockGetParsedCommandLineOfConfigFile.mockImplementation(() => { throw new Error('Oh no!'); }); - expect(() => - createProjectService( + expect(() => { + return createProjectService( { allowDefaultProject: ['file.js'], defaultProject: './tsconfig.json', }, undefined, - ), - ).toThrow("Could not parse default project './tsconfig.json': Oh no!"); + ); + }).toThrow("Could not parse default project './tsconfig.json': Oh no!"); }); it('uses the default projects compiler options when options.defaultProject is set and getParsedCommandLineOfConfigFile succeeds', () => { + mockGetParsedCommandLineOfConfigFile.mockImplementation( + origGetParsedCommandLineOfConfigFile, + ); const base = JSON.parse( fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), ); @@ -107,6 +114,9 @@ describe('createProjectService', () => { }); it('uses the default projects compiler options when options.defaultProject is set with a single extends', () => { + mockGetParsedCommandLineOfConfigFile.mockImplementation( + origGetParsedCommandLineOfConfigFile, + ); const base = JSON.parse( fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), ); @@ -128,6 +138,9 @@ describe('createProjectService', () => { }); it('uses the default projects compiler options when options.defaultProject is set with multiple extends', () => { + mockGetParsedCommandLineOfConfigFile.mockImplementation( + origGetParsedCommandLineOfConfigFile, + ); const base = JSON.parse( fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), ); From 148a9d0f7c670a74152279974988e9107e4be2f7 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 8 Jun 2024 14:41:26 -0600 Subject: [PATCH 03/19] chore(typescript-estree): cleans up PR submission --- .../create-program/createProjectService.ts | 11 +++--- .../src/create-program/getParsedConfigFile.ts | 9 +++-- .../src/create-program/useProvidedPrograms.ts | 10 +++--- .../project-a/tsconfig.src.json | 9 +++++ .../tests/lib/createProjectService.test.ts | 34 +++++++------------ 5 files changed, 35 insertions(+), 38 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index a33bb2c7435a..709c670aff51 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -32,7 +32,6 @@ export function createProjectService( // We import this lazily to avoid its cost for users who don't use the service // TODO: Once we drop support for TS<5.3 we can import from "typescript" directly - // NOTE: Jest mock's rely on this behavior. // eslint-disable-next-line @typescript-eslint/no-require-imports const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts; @@ -76,11 +75,11 @@ export function createProjectService( path.dirname(options.defaultProject), ); service.setCompilerOptionsForInferredProjects( - // NOTE: The inferred projects APIs are not intended for source files when a tsconfig exists. - // There is no API that generates InferredProjectCompilerOptions suggesting it is meant for - // hard coded options passed in to be type checked. The original readConfigFile.config is - // type any and all available config parsing methods generate a CompilerOptions type. Hard - // casting as a work around. + // NOTE: The inferred projects API is not intended for source files when a tsconfig + // exists. There is no API that generates a InferredProjectCompilerOptions suggesting + // it is meant for hard coded options passed in. The original readConfigFile.config + // produced type any and all available config parsing methods generate a CompilerOptions + // type. Hard casting as a work around. // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any configFile.options as any, // providing CompilerOptions while expecting InferredProjectCompilerOptions diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts index 912b5319b4c0..de72cf743e8b 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -5,18 +5,17 @@ import type * as ts from 'typescript/lib/tsserverlibrary'; import { CORE_COMPILER_OPTIONS } from './shared'; /** - * Utility offered by parser to help consumers read a config file. + * Utility offered by parser to help consumers parse a config file. * - * @param configFilePath the path to the tsconfig.json file, relative to `projectDirectory` + * @param configFile the path to the tsconfig.json file, relative to `projectDirectory` * @param projectDirectory the project directory to use as the CWD, defaults to `process.cwd()` */ function getParsedConfigFile( - configFilePath: string, + configFile: string, projectDirectory?: string, ): ts.ParsedCommandLine { // We import this lazily to avoid its cost for users who don't use the service // TODO: Once we drop support for TS<5.3 we can import from "typescript" directly - // NOTE: Jest mock's rely on this behavior. // eslint-disable-next-line @typescript-eslint/no-require-imports const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts; @@ -28,7 +27,7 @@ function getParsedConfigFile( } const parsed = tsserver.getParsedCommandLineOfConfigFile( - configFilePath, + configFile, CORE_COMPILER_OPTIONS, { onUnRecoverableConfigFileDiagnostic: diag => { diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index 00b20fc47e23..e9d443a315a9 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -49,16 +49,16 @@ function useProvidedPrograms( /** * Utility offered by parser to help consumers construct their own program instance. * - * @param configFilePath the path to the tsconfig.json file, relative to `projectDirectory` + * @param configFile the path to the tsconfig.json file, relative to `projectDirectory` * @param projectDirectory the project directory to use as the CWD, defaults to `process.cwd()` */ function createProgramFromConfigFile( - configFilePath: string, + configFile: string, projectDirectory?: string, ): ts.Program { - const configFile = getParsedConfigFile(configFilePath, projectDirectory); - const host = ts.createCompilerHost(configFile.options, true); - return ts.createProgram(configFile.fileNames, configFile.options, host); + const parsed = getParsedConfigFile(configFile, projectDirectory); + const host = ts.createCompilerHost(parsed.options, true); + return ts.createProgram(parsed.fileNames, parsed.options, host); } export { useProvidedPrograms, createProgramFromConfigFile }; diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json index e69de29bb2d1..35eec1467e18 100644 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json +++ b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["./src"], + "compilerOptions": { + "paths": { + "@": ["./project-b/src/index.ts"] + } + } +} diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 3600c24585c9..84b20d6821b5 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -8,12 +8,12 @@ import { CORE_COMPILER_OPTIONS } from '../../src/create-program/shared'; const FIXTURES_DIR = join(__dirname, '../fixtures/projectServicesComplex'); -const mockGetParsedCommandLineOfConfigFile = jest.fn(); -const mockSetCompilerOptionsForInferredProjects = jest.fn(); - const origGetParsedCommandLineOfConfigFile = ts.getParsedCommandLineOfConfigFile; +const mockGetParsedCommandLineOfConfigFile = jest.fn(); +const mockSetCompilerOptionsForInferredProjects = jest.fn(); + jest.mock('typescript/lib/tsserverlibrary', () => ({ ...jest.requireActual('typescript/lib/tsserverlibrary'), getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, @@ -26,11 +26,6 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({ })); describe('createProjectService', () => { - // not strictly needed but removes the dependency on tests running in a specific order - afterEach(() => { - jest.resetAllMocks(); - }); - it('sets allowDefaultProject when options.allowDefaultProject is defined', () => { const allowDefaultProject = ['./*.js']; const settings = createProjectService({ allowDefaultProject }, undefined); @@ -93,11 +88,11 @@ describe('createProjectService', () => { mockGetParsedCommandLineOfConfigFile.mockImplementation( origGetParsedCommandLineOfConfigFile, ); + const base = JSON.parse( fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), ); const compilerOptions = base?.compilerOptions; - const { service } = createProjectService( { defaultProject: join(FIXTURES_DIR, 'tsconfig.base.json'), @@ -105,23 +100,21 @@ describe('createProjectService', () => { undefined, ); + // looser assertion since config parser adds metadata to track references to other files expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - // Looser assertion since config parser tacks on some meta data to track references to other files. - // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L2888 - // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L3125 expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), ); }); - it('uses the default projects compiler options when options.defaultProject is set with a single extends', () => { + it('uses the default projects extended compiler options when options.defaultProject is set and getParsedCommandLineOfConfigFile succeeds', () => { mockGetParsedCommandLineOfConfigFile.mockImplementation( origGetParsedCommandLineOfConfigFile, ); + const base = JSON.parse( fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), ); const compilerOptions = base?.compilerOptions; - const { service } = createProjectService( { defaultProject: join(FIXTURES_DIR, 'tsconfig.json'), @@ -129,18 +122,17 @@ describe('createProjectService', () => { undefined, ); + // looser assertion since config parser adds metadata to track references to other files expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - // Looser assertion since config parser tacks on some meta data to track references to other files. - // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L2888 - // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L3125 expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), ); }); - it('uses the default projects compiler options when options.defaultProject is set with multiple extends', () => { + it('uses the default projects multiple extended compiler options when options.defaultProject is set and getParsedCommandLineOfConfigFile succeeds', () => { mockGetParsedCommandLineOfConfigFile.mockImplementation( origGetParsedCommandLineOfConfigFile, ); + const base = JSON.parse( fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), ); @@ -151,18 +143,16 @@ describe('createProjectService', () => { ...base?.compilerOptions, ...overrides?.compilerOptions, }; - const { service } = createProjectService( { + // extends tsconfig.base.json and tsconfig.overrides.json defaultProject: join(FIXTURES_DIR, 'tsconfig.overridden.json'), }, undefined, ); + // looser assertion since config parser adds metadata to track references to other files expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - // Looser assertion since config parser tacks on some meta data to track references to other files. - // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L2888 - // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/compiler/commandLineParser.ts#L3125 expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), ); }); From b6718a10d624ee86a8b63f7060f881e8207f7480 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 8 Jun 2024 15:19:20 -0600 Subject: [PATCH 04/19] chore(typescript-estree): fixes test names to use the new function name --- .../typescript-estree/tests/lib/createProjectService.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 84b20d6821b5..8eba55d38e8b 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -39,7 +39,7 @@ describe('createProjectService', () => { expect(settings.allowDefaultProject).toBeUndefined(); }); - it('throws an error when options.defaultProject is set and readConfigFile returns an error', () => { + it('throws an error when options.defaultProject is set and getParsedCommandLineOfConfigFile returns an error', () => { mockGetParsedCommandLineOfConfigFile.mockReturnValue({ errors: [ { @@ -68,7 +68,7 @@ describe('createProjectService', () => { ); }); - it('throws an error when options.defaultProject is set and readConfigFile throws an error', () => { + it('throws an error when options.defaultProject is set and getParsedCommandLineOfConfigFile throws an error', () => { mockGetParsedCommandLineOfConfigFile.mockImplementation(() => { throw new Error('Oh no!'); }); From bde1a26e5bca15958833cb32a3901d874c844bff Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 8 Jun 2024 16:43:55 -0600 Subject: [PATCH 05/19] test(typescript-estree): adds additional tests for coverage --- .../tests/lib/createProjectService.test.ts | 79 ++++++++++++++++--- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 8eba55d38e8b..cd5644240cdf 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -8,24 +8,35 @@ import { CORE_COMPILER_OPTIONS } from '../../src/create-program/shared'; const FIXTURES_DIR = join(__dirname, '../fixtures/projectServicesComplex'); +const origSys = ts.sys; const origGetParsedCommandLineOfConfigFile = ts.getParsedCommandLineOfConfigFile; +let mockSys: typeof ts.sys; const mockGetParsedCommandLineOfConfigFile = jest.fn(); const mockSetCompilerOptionsForInferredProjects = jest.fn(); -jest.mock('typescript/lib/tsserverlibrary', () => ({ - ...jest.requireActual('typescript/lib/tsserverlibrary'), - getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, - server: { - ProjectService: class { - setCompilerOptionsForInferredProjects = - mockSetCompilerOptionsForInferredProjects; - }, - }, -})); - describe('createProjectService', () => { + beforeEach(() => { + jest.resetModules(); + + mockSys = origSys; + jest.doMock('typescript/lib/tsserverlibrary', () => { + const actualTs = jest.requireActual('typescript/lib/tsserverlibrary'); + return { + ...actualTs, + sys: mockSys, + getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, + server: { + ProjectService: class { + setCompilerOptionsForInferredProjects = + mockSetCompilerOptionsForInferredProjects; + }, + }, + }; + }); + }); + it('sets allowDefaultProject when options.allowDefaultProject is defined', () => { const allowDefaultProject = ['./*.js']; const settings = createProjectService({ allowDefaultProject }, undefined); @@ -68,6 +79,36 @@ describe('createProjectService', () => { ); }); + it('throws an error when options.defaultProject is absolute and getParsedCommandLineOfConfigFile returns an error', () => { + // NOTE: triggers getCanonicalFileName for coverage + mockGetParsedCommandLineOfConfigFile.mockReturnValue({ + errors: [ + { + category: ts.DiagnosticCategory.Error, + code: 1234, + file: ts.createSourceFile(join(process.cwd(), 'tsconfig.json'), '', { + languageVersion: ts.ScriptTarget.Latest, + }), + start: 0, + length: 0, + messageText: 'Oh no!', + }, + ] satisfies ts.Diagnostic[], + }); + + expect(() => + createProjectService( + { + allowDefaultProject: ['file.js'], + defaultProject: join(process.cwd(), 'tsconfig.json'), + }, + undefined, + ), + ).toThrow( + /Could not parse default project '\/(.*\/)+tsconfig.json': .+ error TS1234: Oh no!/, + ); + }); + it('throws an error when options.defaultProject is set and getParsedCommandLineOfConfigFile throws an error', () => { mockGetParsedCommandLineOfConfigFile.mockImplementation(() => { throw new Error('Oh no!'); @@ -84,6 +125,22 @@ describe('createProjectService', () => { }).toThrow("Could not parse default project './tsconfig.json': Oh no!"); }); + it('throws an error when options.defaultProject is set and tserver.sys is undefined', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockSys = undefined as any; + expect(() => { + return createProjectService( + { + allowDefaultProject: ['file.js'], + defaultProject: './tsconfig.json', + }, + undefined, + ); + }).toThrow( + "Could not parse default project './tsconfig.json': `createProgramFromConfigFile` is only supported in a Node-like environment.", + ); + }); + it('uses the default projects compiler options when options.defaultProject is set and getParsedCommandLineOfConfigFile succeeds', () => { mockGetParsedCommandLineOfConfigFile.mockImplementation( origGetParsedCommandLineOfConfigFile, From 331534521533603408c8d847dcb0eccde3ba1afd Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 8 Jun 2024 17:04:44 -0600 Subject: [PATCH 06/19] test(typescript-estree): fixes test for windows --- .../tests/lib/createProjectService.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index cd5644240cdf..4b9043b7483c 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -86,7 +86,7 @@ describe('createProjectService', () => { { category: ts.DiagnosticCategory.Error, code: 1234, - file: ts.createSourceFile(join(process.cwd(), 'tsconfig.json'), '', { + file: ts.createSourceFile('/tsconfig.json', '', { languageVersion: ts.ScriptTarget.Latest, }), start: 0, @@ -100,12 +100,12 @@ describe('createProjectService', () => { createProjectService( { allowDefaultProject: ['file.js'], - defaultProject: join(process.cwd(), 'tsconfig.json'), + defaultProject: '/tsconfig.json', }, undefined, ), ).toThrow( - /Could not parse default project '\/(.*\/)+tsconfig.json': .+ error TS1234: Oh no!/, + /Could not parse default project '\/tsconfig.json': .+ error TS1234: Oh no!/, ); }); From 2859ef96a13fe5e9d7be3d62728282515d2d69fc Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 8 Jun 2024 17:07:16 -0600 Subject: [PATCH 07/19] test(typescript-estree): fixes test name typo --- .../typescript-estree/tests/lib/createProjectService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 4b9043b7483c..e6a688820ca3 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -125,7 +125,7 @@ describe('createProjectService', () => { }).toThrow("Could not parse default project './tsconfig.json': Oh no!"); }); - it('throws an error when options.defaultProject is set and tserver.sys is undefined', () => { + it('throws an error when options.defaultProject is set and tsserver.sys is undefined', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any mockSys = undefined as any; expect(() => { From f01fe0eee3dc6d711fcf913358984b3e67c8b472 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 8 Jun 2024 18:04:34 -0600 Subject: [PATCH 08/19] chore(typescript-estree): minor cleanup to comments and variables --- .../src/create-program/createProjectService.ts | 8 +++----- .../tests/lib/createProjectService.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 709c670aff51..64283cef8a8c 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -76,13 +76,11 @@ export function createProjectService( ); service.setCompilerOptionsForInferredProjects( // NOTE: The inferred projects API is not intended for source files when a tsconfig - // exists. There is no API that generates a InferredProjectCompilerOptions suggesting - // it is meant for hard coded options passed in. The original readConfigFile.config - // produced type any and all available config parsing methods generate a CompilerOptions - // type. Hard casting as a work around. + // exists. There is no API that generates an InferredProjectCompilerOptions suggesting + // it is meant for hard coded options passed in. Hard casting as a work around. // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - configFile.options as any, // providing CompilerOptions while expecting InferredProjectCompilerOptions + configFile.options as ts.server.protocol.InferredProjectCompilerOptions, ); } catch (error) { throw new Error( diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index e6a688820ca3..addf70d7ebea 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -21,10 +21,10 @@ describe('createProjectService', () => { jest.resetModules(); mockSys = origSys; + // doMock is used over mock to handle the `sys` property mock jest.doMock('typescript/lib/tsserverlibrary', () => { - const actualTs = jest.requireActual('typescript/lib/tsserverlibrary'); return { - ...actualTs, + ...jest.requireActual('typescript/lib/tsserverlibrary'), sys: mockSys, getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, server: { From 73e2c7837d6d69524dc742bbc4b178af310b02ac Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 8 Jun 2024 18:19:51 -0600 Subject: [PATCH 09/19] style(typescript-estree): unifies how compiler options are defined in tests --- .../tests/lib/createProjectService.test.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index addf70d7ebea..26246fad0c86 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -21,7 +21,7 @@ describe('createProjectService', () => { jest.resetModules(); mockSys = origSys; - // doMock is used over mock to handle the `sys` property mock + // doMock is used over mock to handle the tsserver.sys property mock jest.doMock('typescript/lib/tsserverlibrary', () => { return { ...jest.requireActual('typescript/lib/tsserverlibrary'), @@ -80,12 +80,12 @@ describe('createProjectService', () => { }); it('throws an error when options.defaultProject is absolute and getParsedCommandLineOfConfigFile returns an error', () => { - // NOTE: triggers getCanonicalFileName for coverage mockGetParsedCommandLineOfConfigFile.mockReturnValue({ errors: [ { category: ts.DiagnosticCategory.Error, code: 1234, + // absolute path triggers getCanonicalFileName for coverage file: ts.createSourceFile('/tsconfig.json', '', { languageVersion: ts.ScriptTarget.Latest, }), @@ -149,7 +149,10 @@ describe('createProjectService', () => { const base = JSON.parse( fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), ); - const compilerOptions = base?.compilerOptions; + const compilerOptions = { + ...CORE_COMPILER_OPTIONS, + ...base?.compilerOptions, + }; const { service } = createProjectService( { defaultProject: join(FIXTURES_DIR, 'tsconfig.base.json'), @@ -157,9 +160,9 @@ describe('createProjectService', () => { undefined, ); - // looser assertion since config parser adds metadata to track references to other files expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), + // looser assertion since config parser adds metadata to track references to other files + expect.objectContaining(compilerOptions), ); }); @@ -171,7 +174,10 @@ describe('createProjectService', () => { const base = JSON.parse( fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), ); - const compilerOptions = base?.compilerOptions; + const compilerOptions = { + ...CORE_COMPILER_OPTIONS, + ...base?.compilerOptions, + }; const { service } = createProjectService( { defaultProject: join(FIXTURES_DIR, 'tsconfig.json'), @@ -179,9 +185,9 @@ describe('createProjectService', () => { undefined, ); - // looser assertion since config parser adds metadata to track references to other files expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), + // looser assertion since config parser adds metadata to track references to other files + expect.objectContaining(compilerOptions), ); }); @@ -197,6 +203,7 @@ describe('createProjectService', () => { fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.overrides.json'), 'utf8'), ); const compilerOptions = { + ...CORE_COMPILER_OPTIONS, ...base?.compilerOptions, ...overrides?.compilerOptions, }; @@ -208,9 +215,9 @@ describe('createProjectService', () => { undefined, ); - // looser assertion since config parser adds metadata to track references to other files expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - expect.objectContaining({ ...CORE_COMPILER_OPTIONS, ...compilerOptions }), + // looser assertion since config parser adds metadata to track references to other files + expect.objectContaining(compilerOptions), ); }); }); From 4b8c010a5a520072a0b767d82cdd4dfd6ae23c19 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Wed, 26 Jun 2024 15:12:45 +0000 Subject: [PATCH 10/19] chore(typescript-estree): updates create-program/getParsedConfigFile.ts with review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- .../typescript-estree/src/create-program/getParsedConfigFile.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts index de72cf743e8b..ddb239795770 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -51,7 +51,6 @@ function getParsedConfigFile( return result; - // scoped to parent function to use lazy typescript import function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { return tsserver.formatDiagnostics(diagnostics, { getCanonicalFileName: f => f, From abd009507dbb30dced4d14931c08aa397bd2ddb6 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Tue, 23 Jul 2024 08:01:44 -0600 Subject: [PATCH 11/19] test(typescript-estree): adds initial getParsedConfigFile tests --- .../src/create-program/getParsedConfigFile.ts | 22 ++-- .../src/create-program/useProvidedPrograms.ts | 4 +- .../tests/lib/getParsedConfigFile.test.ts | 107 ++++++++++++++++++ 3 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts index 5847fd50217e..449a902b67a2 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -18,10 +18,13 @@ function getParsedConfigFile( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (tsserver.sys === undefined) { throw new Error( - '`createProgramFromConfigFile` is only supported in a Node-like environment.', + '`getParsedConfigFile` is only supported in a Node-like environment.', ); } + const getCurrentDirectory = (): string => + (projectDirectory && path.resolve(projectDirectory)) || process.cwd(); + let parsingError: string | undefined; const parsed = tsserver.getParsedCommandLineOfConfigFile( @@ -32,31 +35,28 @@ function getParsedConfigFile( parsingError = formatDiagnostics([diag]); // ensures that `parsed` is defined. }, fileExists: fs.existsSync, - getCurrentDirectory: () => - (projectDirectory && path.resolve(projectDirectory)) || process.cwd(), + getCurrentDirectory, readDirectory: tsserver.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf-8'), useCaseSensitiveFileNames: tsserver.sys.useCaseSensitiveFileNames, }, ); - if (parsed?.errors?.length) { + if (parsed?.errors.length) { parsingError = formatDiagnostics(parsed.errors); } - if (parsed === undefined) { - return ( - parsingError ?? - 'Unknown config file parse error: no result or error diagnostic returned' - ); + if (parsingError !== undefined) { + return `Could not parse config file '${configFile}': ${parsingError}`; } - return parsed; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return parsed!; function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { return tsserver.formatDiagnostics(diagnostics, { getCanonicalFileName: f => f, - getCurrentDirectory: process.cwd, + getCurrentDirectory, getNewLine: () => '\n', }); } diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index aaf094723bb3..951678f90e65 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -55,10 +55,10 @@ function useProvidedPrograms( function createProgramFromConfigFile( configFile: string, projectDirectory?: string, -): ts.Program | string { +): ts.Program { const parsed = getParsedConfigFile(ts, configFile, projectDirectory); if (typeof parsed === 'string') { - return parsed; + throw new Error(parsed); } const host = ts.createCompilerHost(parsed.options, true); return ts.createProgram(parsed.fileNames, parsed.options, host); diff --git a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts b/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts new file mode 100644 index 000000000000..8f026e6af924 --- /dev/null +++ b/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts @@ -0,0 +1,107 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import * as ts from 'typescript'; + +import { getParsedConfigFile } from '../../src/create-program/getParsedConfigFile'; + +const FIXTURES_DIR = path.resolve( + __dirname, + '../fixtures/projectServicesComplex', +); + +const mockGetParsedCommandLineOfConfigFile = jest.fn(); + +const mockTsserver: typeof ts = { + sys: {} as ts.System, + formatDiagnostics: ts.formatDiagnostics, + getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} as any; + +describe('getParsedConfigFile', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('throws an error when tsserver.sys is undefined', () => { + expect(() => + getParsedConfigFile({} as typeof ts, './tsconfig.json'), + ).toThrow( + '`getParsedConfigFile` is only supported in a Node-like environment.', + ); + }); + + it('uses the cwd as the default project directory', () => { + getParsedConfigFile(mockTsserver, './tsconfig.json'); + expect(mockGetParsedCommandLineOfConfigFile).toHaveBeenCalledTimes(1); + const [_configFileName, _optionsToExtend, host] = + mockGetParsedCommandLineOfConfigFile.mock.calls[0]; + expect(host.getCurrentDirectory()).toBe(process.cwd()); + }); + + it('resolves a relative project directory when passed', () => { + getParsedConfigFile( + mockTsserver, + './tsconfig.json', + path.relative('./', path.dirname(__filename)), + ); + expect(mockGetParsedCommandLineOfConfigFile).toHaveBeenCalledTimes(1); + const [_configFileName, _optionsToExtend, host] = + mockGetParsedCommandLineOfConfigFile.mock.calls[0]; + expect(host.getCurrentDirectory()).toBe(path.dirname(__filename)); + }); + + it('resolves an absolute project directory when passed', () => { + getParsedConfigFile(mockTsserver, './tsconfig.json', __dirname); + expect(mockGetParsedCommandLineOfConfigFile).toHaveBeenCalledTimes(1); + const [_configFileName, _optionsToExtend, host] = + mockGetParsedCommandLineOfConfigFile.mock.calls[0]; + expect(host.getCurrentDirectory()).toBe(__dirname); + }); + + it('returns a diagnostic string when parsing a config file fails', () => { + mockGetParsedCommandLineOfConfigFile.mockReturnValue({ + errors: [ + { + category: ts.DiagnosticCategory.Error, + code: 1234, + file: ts.createSourceFile('./tsconfig.json', '', { + languageVersion: ts.ScriptTarget.Latest, + }), + start: 0, + length: 0, + messageText: 'Oh no!', + }, + ] satisfies ts.Diagnostic[], + }); + expect(getParsedConfigFile(mockTsserver, './tsconfig.json')).toMatch( + /Could not parse config file '\.\/tsconfig.json': .+ error TS1234: Oh no!/, + ); + }); + + it('uses extended compiler options when parsing a config file', () => { + mockGetParsedCommandLineOfConfigFile.mockImplementation( + ( + ...args: Parameters<(typeof ts)['getParsedCommandLineOfConfigFile']> + ) => { + return ts.getParsedCommandLineOfConfigFile(...args); + }, + ); + const base = JSON.parse( + fs.readFileSync(path.resolve(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), + ); + const config = { + options: expect.objectContaining(base.compilerOptions), + raw: JSON.parse( + fs.readFileSync(path.resolve(FIXTURES_DIR, 'tsconfig.json'), 'utf8'), + ), + }; + expect( + getParsedConfigFile( + mockTsserver, + path.resolve(FIXTURES_DIR, 'tsconfig.json'), + ), + ).toEqual(expect.objectContaining(config)); + }); +}); From c563b8de8aaeacdfea2dab6ba0c99029d4ca2653 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Tue, 23 Jul 2024 19:20:28 -0600 Subject: [PATCH 12/19] test(typescript-estree): updates createProjectService tests --- .../create-program/createProjectService.ts | 6 +- .../tests/lib/createProjectService.test.ts | 213 ++++-------------- 2 files changed, 47 insertions(+), 172 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index e83e78db5671..63f497607c82 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -132,7 +132,7 @@ export function createProjectService( // exists. There is no API that generates an InferredProjectCompilerOptions suggesting // it is meant for hard coded options passed in. Hard casting as a work around. // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + configFile.options as ts.server.protocol.InferredProjectCompilerOptions, ); } @@ -140,9 +140,7 @@ export function createProjectService( defaultProjectError = `Could not parse default project '${options.defaultProject}': ${(error as Error).message}`; } if (defaultProjectError !== undefined) { - throw new Error( - `Could not parse default project '${options.defaultProject}': ${defaultProjectError}`, - ); + throw new Error(defaultProjectError); } } diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index b5de1aba3ee1..7c98a26117aa 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -1,44 +1,16 @@ -import fs from 'node:fs'; -import { join } from 'node:path'; - import debug from 'debug'; import * as ts from 'typescript'; -import { createProjectService } from '../../src/create-program/createProjectService'; -import { CORE_COMPILER_OPTIONS } from '../../src/create-program/shared'; - -const FIXTURES_DIR = join(__dirname, '../fixtures/projectServicesComplex'); - -const origSys = ts.sys; -const origGetParsedCommandLineOfConfigFile = - ts.getParsedCommandLineOfConfigFile; - -let mockSys: typeof ts.sys; -const mockGetParsedCommandLineOfConfigFile = jest.fn(); +const mockGetParsedConfigFile = jest.fn(); const mockSetCompilerOptionsForInferredProjects = jest.fn(); +const mockSetHostConfiguration = jest.fn(); + +jest.mock('../../src/create-program/getParsedConfigFile', () => ({ + getParsedConfigFile: mockGetParsedConfigFile, +})); -describe('createProjectService', () => { - beforeEach(() => { - jest.resetModules(); - - mockSys = origSys; - // doMock is used over mock to handle the tsserver.sys property mock - jest.doMock('typescript/lib/tsserverlibrary', () => { - return { - ...jest.requireActual('typescript/lib/tsserverlibrary'), - sys: mockSys, - getParsedCommandLineOfConfigFile: mockGetParsedCommandLineOfConfigFile, - server: { - ProjectService: class { - setCompilerOptionsForInferredProjects = - mockSetCompilerOptionsForInferredProjects; - }, - }, - }; - }); jest.mock('typescript/lib/tsserverlibrary', () => ({ ...jest.requireActual('typescript/lib/tsserverlibrary'), - readConfigFile: mockReadConfigFile, server: { ...jest.requireActual('typescript/lib/tsserverlibrary').server, ProjectService: class { @@ -57,10 +29,16 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({ } setCompilerOptionsForInferredProjects = mockSetCompilerOptionsForInferredProjects; + setHostConfiguration = mockSetHostConfiguration; }, }, })); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { + createProjectService, +} = require('../../src/create-program/createProjectService'); + describe('createProjectService', () => { afterEach(() => { jest.resetAllMocks(); @@ -79,21 +57,10 @@ describe('createProjectService', () => { expect(settings.allowDefaultProject).toBeUndefined(); }); - it('throws an error when options.defaultProject is set and getParsedCommandLineOfConfigFile returns an error', () => { - mockGetParsedCommandLineOfConfigFile.mockReturnValue({ - errors: [ - { - category: ts.DiagnosticCategory.Error, - code: 1234, - file: ts.createSourceFile('./tsconfig.json', '', { - languageVersion: ts.ScriptTarget.Latest, - }), - start: 0, - length: 0, - messageText: 'Oh no!', - }, - ] satisfies ts.Diagnostic[], - }); + it('throws an error when options.defaultProject is set and getParsedConfigFile returns an error', () => { + mockGetParsedConfigFile.mockReturnValue( + "Could not parse config file './tsconfig.json': ./tsconfig.json(1,1): error TS1234: Oh no!", + ); expect(() => createProjectService( @@ -104,149 +71,44 @@ describe('createProjectService', () => { undefined, ), ).toThrow( - /Could not parse default project '\.\/tsconfig.json': .+ error TS1234: Oh no!/, + /Could not parse config file '\.\/tsconfig.json': .+ error TS1234: Oh no!/, ); }); - it('throws an error when options.defaultProject is absolute and getParsedCommandLineOfConfigFile returns an error', () => { - mockGetParsedCommandLineOfConfigFile.mockReturnValue({ - errors: [ - { - category: ts.DiagnosticCategory.Error, - code: 1234, - // absolute path triggers getCanonicalFileName for coverage - file: ts.createSourceFile('/tsconfig.json', '', { - languageVersion: ts.ScriptTarget.Latest, - }), - start: 0, - length: 0, - messageText: 'Oh no!', - }, - ] satisfies ts.Diagnostic[], + it('throws an error when options.defaultProject is set and getParsedConfigFile throws an error', () => { + mockGetParsedConfigFile.mockImplementation(() => { + throw new Error( + '`getParsedConfigFile` is only supported in a Node-like environment.', + ); }); expect(() => createProjectService( { allowDefaultProject: ['file.js'], - defaultProject: '/tsconfig.json', + defaultProject: './tsconfig.json', }, undefined, ), ).toThrow( - /Could not parse default project '\/tsconfig.json': .+ error TS1234: Oh no!/, - ); - }); - - it('throws an error when options.defaultProject is set and getParsedCommandLineOfConfigFile throws an error', () => { - mockGetParsedCommandLineOfConfigFile.mockImplementation(() => { - throw new Error('Oh no!'); - }); - - expect(() => { - return createProjectService( - { - allowDefaultProject: ['file.js'], - defaultProject: './tsconfig.json', - }, - undefined, - ); - }).toThrow("Could not parse default project './tsconfig.json': Oh no!"); - }); - - it('throws an error when options.defaultProject is set and tsserver.sys is undefined', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mockSys = undefined as any; - expect(() => { - return createProjectService( - { - allowDefaultProject: ['file.js'], - defaultProject: './tsconfig.json', - }, - undefined, - ); - }).toThrow( - "Could not parse default project './tsconfig.json': `createProgramFromConfigFile` is only supported in a Node-like environment.", - ); - }); - - it('uses the default projects compiler options when options.defaultProject is set and getParsedCommandLineOfConfigFile succeeds', () => { - mockGetParsedCommandLineOfConfigFile.mockImplementation( - origGetParsedCommandLineOfConfigFile, - ); - - const base = JSON.parse( - fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), - ); - const compilerOptions = { - ...CORE_COMPILER_OPTIONS, - ...base?.compilerOptions, - }; - const { service } = createProjectService( - { - defaultProject: join(FIXTURES_DIR, 'tsconfig.base.json'), - }, - undefined, - ); - - expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - // looser assertion since config parser adds metadata to track references to other files - expect.objectContaining(compilerOptions), - ); - }); - - it('uses the default projects extended compiler options when options.defaultProject is set and getParsedCommandLineOfConfigFile succeeds', () => { - mockGetParsedCommandLineOfConfigFile.mockImplementation( - origGetParsedCommandLineOfConfigFile, - ); - - const base = JSON.parse( - fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), - ); - const compilerOptions = { - ...CORE_COMPILER_OPTIONS, - ...base?.compilerOptions, - }; - const { service } = createProjectService( - { - defaultProject: join(FIXTURES_DIR, 'tsconfig.json'), - }, - undefined, - ); - - expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - // looser assertion since config parser adds metadata to track references to other files - expect.objectContaining(compilerOptions), + "Could not parse default project './tsconfig.json': `getParsedConfigFile` is only supported in a Node-like environment.", ); }); - it('uses the default projects multiple extended compiler options when options.defaultProject is set and getParsedCommandLineOfConfigFile succeeds', () => { - mockGetParsedCommandLineOfConfigFile.mockImplementation( - origGetParsedCommandLineOfConfigFile, - ); + it('uses the default projects compiler options when options.defaultProject is set and getParsedConfigFile succeeds', () => { + const compilerOptions = { strict: true }; + mockGetParsedConfigFile.mockReturnValue({ options: compilerOptions }); - const base = JSON.parse( - fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), - ); - const overrides = JSON.parse( - fs.readFileSync(join(FIXTURES_DIR, 'tsconfig.overrides.json'), 'utf8'), - ); - const compilerOptions = { - ...CORE_COMPILER_OPTIONS, - ...base?.compilerOptions, - ...overrides?.compilerOptions, - }; const { service } = createProjectService( { - // extends tsconfig.base.json and tsconfig.overrides.json - defaultProject: join(FIXTURES_DIR, 'tsconfig.overridden.json'), + allowDefaultProject: ['file.js'], + defaultProject: './tsconfig.json', }, undefined, ); expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( - // looser assertion since config parser adds metadata to track references to other files - expect.objectContaining(compilerOptions), + compilerOptions, ); }); @@ -364,4 +226,19 @@ describe('createProjectService', () => { expect(process.stderr.write).toHaveBeenCalledTimes(0); }); + + it('sets a host configuration', () => { + const { service } = createProjectService( + { + allowDefaultProject: ['file.js'], + }, + undefined, + ); + + expect(service.setHostConfiguration).toHaveBeenCalledWith({ + preferences: { + includePackageJsonAutoImports: 'off', + }, + }); + }); }); From 4d043be997bf0470d2688a6dae7d4ebab96183b0 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Tue, 23 Jul 2024 19:24:42 -0600 Subject: [PATCH 13/19] chore: merge upstream/v8 --- CHANGELOG.md | 26 + docs/getting-started/Quickstart.mdx | 13 +- docs/getting-started/Typed_Linting.mdx | 8 +- docs/packages/Parser.mdx | 4 +- docs/packages/TypeScript_ESLint.mdx | 10 +- docs/troubleshooting/faqs/ESLint.mdx | 2 +- docs/troubleshooting/faqs/Frameworks.mdx | 4 +- docs/troubleshooting/faqs/General.mdx | 2 +- .../typed-linting/Monorepos.mdx | 4 +- .../typed-linting/Performance.mdx | 2 +- docs/troubleshooting/typed-linting/index.mdx | 142 +- docs/users/Shared_Configurations.mdx | 22 +- docs/users/What_About_Formatting.mdx | 2 +- eslint.config.mjs | 10 + packages/ast-spec/CHANGELOG.md | 6 + packages/ast-spec/package.json | 2 +- packages/eslint-plugin-internal/CHANGELOG.md | 6 + packages/eslint-plugin-internal/package.json | 10 +- packages/eslint-plugin/CHANGELOG.md | 28 + .../eslint-plugin/docs/rules/return-await.mdx | 84 +- .../docs/rules/sort-type-constituents.mdx | 2 +- .../docs/rules/strict-boolean-expressions.mdx | 6 + packages/eslint-plugin/package.json | 14 +- .../src/configs/strict-type-checked-only.ts | 5 + .../src/configs/strict-type-checked.ts | 5 + .../src/rules/consistent-type-imports.ts | 37 +- .../eslint-plugin/src/rules/no-mixed-enums.ts | 21 +- .../src/rules/no-non-null-assertion.ts | 24 +- .../src/rules/no-unnecessary-condition.ts | 21 +- .../no-unnecessary-template-expression.ts | 188 ++- .../src/rules/no-unsafe-return.ts | 3 +- .../eslint-plugin/src/rules/no-unused-vars.ts | 109 +- .../analyzeChain.ts | 76 +- .../eslint-plugin/src/rules/return-await.ts | 170 ++- .../src/rules/strict-boolean-expressions.ts | 132 +- .../src/util/getFunctionHeadLoc.ts | 4 +- packages/eslint-plugin/src/util/misc.ts | 2 +- .../return-await.shot | 16 +- .../strict-boolean-expressions.shot | 6 + .../tests/rules/dot-notation.test.ts | 24 +- .../rules/no-unnecessary-condition.test.ts | 13 + ...no-unnecessary-template-expression.test.ts | 1318 +++++++++++------ .../tests/rules/no-unsafe-return.test.ts | 1 + .../no-unused-vars-eslint.test.ts | 116 +- .../tests/rules/return-await.test.ts | 185 +-- .../rules/strict-boolean-expressions.test.ts | 613 ++++++++ .../tests/schema-snapshots/return-await.shot | 11 +- packages/eslint-plugin/tests/schemas.test.ts | 2 +- packages/integration-tests/CHANGELOG.md | 6 + .../fixtures/flat-config-types/package.json | 5 +- packages/integration-tests/package.json | 2 +- packages/parser/CHANGELOG.md | 6 + packages/parser/package.json | 10 +- .../parser/tests/test-utils/test-utils.ts | 6 +- packages/repo-tools/CHANGELOG.md | 6 + packages/repo-tools/package.json | 12 +- .../CHANGELOG.md | 6 + .../package.json | 6 +- .../src/index.ts | 2 +- packages/rule-tester/CHANGELOG.md | 6 + packages/rule-tester/package.json | 8 +- packages/rule-tester/src/RuleTester.ts | 38 +- packages/rule-tester/tests/RuleTester.test.ts | 2 - packages/scope-manager/CHANGELOG.md | 6 + packages/scope-manager/package.json | 8 +- .../src/referencer/Referencer.ts | 6 +- packages/type-utils/CHANGELOG.md | 6 + packages/type-utils/package.json | 8 +- packages/types/CHANGELOG.md | 6 + packages/types/package.json | 2 +- packages/typescript-eslint/CHANGELOG.md | 17 + packages/typescript-eslint/package.json | 8 +- .../src/configs/strict-type-checked-only.ts | 5 + .../src/configs/strict-type-checked.ts | 5 + packages/typescript-estree/CHANGELOG.md | 19 + packages/typescript-estree/package.json | 6 +- packages/typescript-estree/src/convert.ts | 12 +- .../create-program/createProjectProgram.ts | 4 +- .../create-program/createProjectService.ts | 8 + .../src/create-program/useProvidedPrograms.ts | 48 +- packages/typescript-estree/src/parser.ts | 7 +- .../src/useProgramFromProjectService.ts | 245 ++- .../tests/lib/createProjectService.test.ts | 1 - .../typescript-estree/tests/lib/parse.test.ts | 29 +- .../tests/lib/semanticInfo.test.ts | 22 + .../lib/useProgramFromProjectService.test.ts | 55 +- packages/utils/CHANGELOG.md | 6 + packages/utils/package.json | 8 +- packages/utils/src/ts-eslint/Rule.ts | 2 +- .../tests/eslint-utils/deepMerge.test.ts | 2 +- packages/visitor-keys/CHANGELOG.md | 6 + packages/visitor-keys/package.json | 4 +- packages/website-eslint/CHANGELOG.md | 6 + packages/website-eslint/package.json | 12 +- packages/website-eslint/src/mock/path.js | 6 +- packages/website/CHANGELOG.md | 17 + ...7-announcing-typescript-eslint-v8-beta.mdx | 6 +- packages/website/package.json | 18 +- .../components/editor/useSandboxServices.ts | 18 + packages/website/src/vendor/sandbox.d.ts | 243 +-- .../website/src/vendor/typescript-vfs.d.ts | 22 +- yarn.lock | 207 ++- 102 files changed, 3318 insertions(+), 1430 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd805c784eeb..4f36c355a0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 7.17.0 (2024-07-22) + + +### 🚀 Features + +- **eslint-plugin:** backport no-unsafe-function type, no-wrapper-object-types from v8 to v7 ([#9507](https://github.com/typescript-eslint/typescript-eslint/pull/9507)) +- **eslint-plugin:** [return-await] add option to report in error-handling scenarios only, and deprecate "never" ([#9364](https://github.com/typescript-eslint/typescript-eslint/pull/9364)) + +### 🩹 Fixes + +- **eslint-plugin:** [no-floating-promises] check top-level type assertions (and more) ([#9043](https://github.com/typescript-eslint/typescript-eslint/pull/9043)) +- **eslint-plugin:** [strict-boolean-expressions] consider assertion function argument a boolean context ([#9074](https://github.com/typescript-eslint/typescript-eslint/pull/9074)) +- **eslint-plugin:** [no-unnecessary-condition] false positive on optional private field ([#9602](https://github.com/typescript-eslint/typescript-eslint/pull/9602)) +- **typescript-estree:** don't infer single-run when --fix is in proces.argv ([#9577](https://github.com/typescript-eslint/typescript-eslint/pull/9577)) +- **typescript-estree:** disable single-run inference with extraFileExtensions ([#9580](https://github.com/typescript-eslint/typescript-eslint/pull/9580)) +- **website:** expose ATA types to eslint instance ([#9598](https://github.com/typescript-eslint/typescript-eslint/pull/9598)) + +### ❤️ Thank You + +- Armano @armano2 +- Josh Goldberg ✨ +- Kirk Waiblinger @kirkwaiblinger +- StyleShit @StyleShit + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) diff --git a/docs/getting-started/Quickstart.mdx b/docs/getting-started/Quickstart.mdx index 6c4f0865d818..d1daa5761be7 100644 --- a/docs/getting-started/Quickstart.mdx +++ b/docs/getting-started/Quickstart.mdx @@ -29,9 +29,9 @@ npm install --save-dev eslint @eslint/js @types/eslint__js typescript typescript ### Step 2: Configuration -Next, create an `eslint.config.js` config file in the root of your project, and populate it with the following: +Next, create an `eslint.config.mjs` config file in the root of your project, and populate it with the following: -```js title="eslint.config.js" +```js title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; @@ -45,6 +45,13 @@ export default tseslint.config( This code will enable our [recommended configuration](../users/Shared_Configurations.mdx) for linting. +
+Aside on file extensions + +The `.mjs` extension makes the file use the [ES modules (ESM)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) format. Node interprets `.js` files in the [CommonJS (CJS)](https://nodejs.org/api/modules.html) format by default, but if you have `"type": "module"` in your `package.json`, you can also use `eslint.config.js`. + +
+ ### Step 3: Running ESLint Open a terminal to the root of your project and run the following command: @@ -92,7 +99,7 @@ We recommend you consider enabling the following two configs: - [`strict`](../users/Shared_Configurations.mdx#strict): a superset of `recommended` that includes more opinionated rules which may also catch bugs. - [`stylistic`](../users/Shared_Configurations.mdx#stylistic): additional rules that enforce consistent styling without significantly catching bugs or changing logic. -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, // Remove this line diff --git a/docs/getting-started/Typed_Linting.mdx b/docs/getting-started/Typed_Linting.mdx index 9dade9b0d091..08aa30b78ace 100644 --- a/docs/getting-started/Typed_Linting.mdx +++ b/docs/getting-started/Typed_Linting.mdx @@ -15,7 +15,7 @@ To tap into TypeScript's additional powers, there are two small changes you need 1. Add `TypeChecked` to the name of any preset configs you're using, namely `recommended`, `strict`, and `stylistic`. 2. Add `languageOptions.parserOptions` to tell our parser how to find the TSConfig for each source file. -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, // Remove this line @@ -97,7 +97,7 @@ If you enabled the [`strict` shared config](../users/Shared_Configurations.mdx#s -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, // Removed lines start @@ -152,7 +152,7 @@ For example, if you use a specific `tsconfig.eslint.json` for linting, you'd spe -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config({ // ... languageOptions: { @@ -195,7 +195,7 @@ You can combine ESLint's [overrides](https://eslint.org/docs/latest/use/configur -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, diff --git a/docs/packages/Parser.mdx b/docs/packages/Parser.mdx index 3e146d89ad26..6e1447349fde 100644 --- a/docs/packages/Parser.mdx +++ b/docs/packages/Parser.mdx @@ -326,7 +326,7 @@ This option brings two main benefits over the older `project`: For more information, see: - [feat(typescript-estree): add EXPERIMENTAL_useProjectService option to use TypeScript project service](https://github.com/typescript-eslint/typescript-eslint/pull/6754) -- [docs: blog post on parserOptions.projectService](docs: blog post on parserOptions.projectService ) +- [docs: blog post on parserOptions.projectService](https://github.com/typescript-eslint/typescript-eslint/pull/8031) #### `ProjectServiceOptions` @@ -416,7 +416,7 @@ Example usage: -```js title="eslint.config.js" +```js title="eslint.config.mjs" import * as parser from '@typescript-eslint/parser'; export default [ diff --git a/docs/packages/TypeScript_ESLint.mdx b/docs/packages/TypeScript_ESLint.mdx index 42ac4d58e83a..4abfb085d564 100644 --- a/docs/packages/TypeScript_ESLint.mdx +++ b/docs/packages/TypeScript_ESLint.mdx @@ -43,7 +43,7 @@ For more information on migrating from a "legacy" config setup, see [ESLint's Co The simplest usage of this package would be: -```js title="eslint.config.js" +```js title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; @@ -65,7 +65,7 @@ For more information on the `tseslint.config` function [see `config(...)` below] You can declare our plugin and parser in your config via this package, for example: -```js title="eslint.config.js" +```js title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; @@ -109,7 +109,7 @@ Below is a more complex example of how you might use our tooling with flat confi - Disables type-aware linting on JS files - Enables the recommended `eslint-plugin-jest` rules on test files only -```js title="eslint.config.js" +```js title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; @@ -163,7 +163,7 @@ By using this function you will get autocomplete and documentation for all confi -```ts title="eslint.config.js" +```ts title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; @@ -182,7 +182,7 @@ export default tseslint.config( -```ts title="eslint.config.js" +```ts title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; diff --git a/docs/troubleshooting/faqs/ESLint.mdx b/docs/troubleshooting/faqs/ESLint.mdx index a0e429e61031..5766af8b487a 100644 --- a/docs/troubleshooting/faqs/ESLint.mdx +++ b/docs/troubleshooting/faqs/ESLint.mdx @@ -48,7 +48,7 @@ Note, that for a mixed project including JavaScript and TypeScript, the `no-unde -```js title="eslint.config.js" +```js title="eslint.config.mjs" import tseslint from 'typescript-eslint'; export default tseslint.config( diff --git a/docs/troubleshooting/faqs/Frameworks.mdx b/docs/troubleshooting/faqs/Frameworks.mdx index 128b15fb33fc..2c0d5a6b7306 100644 --- a/docs/troubleshooting/faqs/Frameworks.mdx +++ b/docs/troubleshooting/faqs/Frameworks.mdx @@ -18,7 +18,7 @@ See [Changes to `extraFileExtensions` with `projectService`](../typed-linting/Pe -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( // ... the rest of your config ... { @@ -59,7 +59,7 @@ If you are running into issues parsing .vue files, it might be because parsers l -```js title="eslint.config.js" +```js title="eslint.config.mjs" import tseslint from 'typescript-eslint'; // Add this line import vueParser from 'vue-eslint-parser'; diff --git a/docs/troubleshooting/faqs/General.mdx b/docs/troubleshooting/faqs/General.mdx index addc2ce6b213..05d383ffb64a 100644 --- a/docs/troubleshooting/faqs/General.mdx +++ b/docs/troubleshooting/faqs/General.mdx @@ -34,7 +34,7 @@ Some examples -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( // ... the rest of your config ... { diff --git a/docs/troubleshooting/typed-linting/Monorepos.mdx b/docs/troubleshooting/typed-linting/Monorepos.mdx index 5a567b044aef..eb95d674084f 100644 --- a/docs/troubleshooting/typed-linting/Monorepos.mdx +++ b/docs/troubleshooting/typed-linting/Monorepos.mdx @@ -53,7 +53,7 @@ For each file being linted, the first matching project path will be used as its -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, @@ -105,7 +105,7 @@ Instead of globs that use `**` to recursively check all folders, prefer paths th -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, diff --git a/docs/troubleshooting/typed-linting/Performance.mdx b/docs/troubleshooting/typed-linting/Performance.mdx index 45564404730c..4a8efafd9d3d 100644 --- a/docs/troubleshooting/typed-linting/Performance.mdx +++ b/docs/troubleshooting/typed-linting/Performance.mdx @@ -43,7 +43,7 @@ Instead of globs that use `**` to recursively check all folders, prefer paths th -```js title="eslint.config.js" +```js title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; diff --git a/docs/troubleshooting/typed-linting/index.mdx b/docs/troubleshooting/typed-linting/index.mdx index f39e89876328..4e0f62c3749f 100644 --- a/docs/troubleshooting/typed-linting/index.mdx +++ b/docs/troubleshooting/typed-linting/index.mdx @@ -25,32 +25,26 @@ See [ESLint does not re-compute cross-file information on file changes (microsof ## I get errors telling me "Having many files run with the default project is known to cause performance issues and slow down linting." -These errors are caused by using the [`EXPERIMENTAL_useProjectService`](../../packages/Parser.mdx#experimental_useprojectservice) `allowDefaultProject` with an excessively wide glob. -`allowDefaultProject` causes a new TypeScript "program" to be built for each "out of project" file it includes, which incurs a performance overhead for each file. - -To resolve this error, narrow the glob(s) used for `allowDefaultProject` to include fewer files. -For example: - -```diff title="eslint.config.js" -parserOptions: { - EXPERIMENTAL_useProjectService: { - allowDefaultProject: [ -- "**/*.js", -+ "./*.js" - ] - } -} -``` +These errors are caused by attempting to use the [`projectService`](../../packages/Parser.mdx#projectservice) to lint a file not explicitly included in a `tsconfig.json`. -You may also need to include more files in your `tsconfig.json`. -For example: +For each file being reported: -```diff title="tsconfig.json" -"include": [ - "src", -+ "*.js" -] -``` +- If you **do not** want to lint the file: + - Use [one of the options ESLint offers to ignore files](https://eslint.org/docs/latest/user-guide/configuring/ignoring-code), such an `ignores` config key. +- If you **do** want to lint the file: + - If you **do not** want to lint the file with [type-aware linting](../../getting-started/Typed_Linting.mdx): [disable type-checked linting for that file](#how-do-i-disable-type-checked-linting-for-a-file). + - If you **do** want to lint the file with [type-aware linting](../../getting-started/Typed_Linting.mdx): + 1. If possible, add the file to the closest `tsconfig.json`'s `include`. For example, allowing `.js` files: + ```diff title="tsconfig.json" + "include": [ + "src", + + "*.js" + ] + ``` + 2. Otherwise, consider setting [`parserOptions.createProjectService.allowDefaultProject`](../../packages/parser#allowdefaultproject). + +typescript-eslint allows up to 8 "out of project" files by default. +Each file causes a new TypeScript "program" to be built for each file it includes, which incurs a performance overhead _for each file_. If you cannot do this, please [file an issue on typescript-eslint's typescript-estree package](https://github.com/typescript-eslint/typescript-eslint/issues/new?assignees=&labels=enhancement%2Ctriage&projects=&template=07-enhancement-other.yaml&title=Enhancement%3A+%3Ca+short+description+of+my+proposal%3E) telling us your use case and why you need more out-of-project files linted. Be sure to include a minimal reproduction we can work with to understand your use case! @@ -61,46 +55,11 @@ These errors are caused by an ESLint config requesting type information be gener ### Fixing the Error - - - If you **do not** want to lint the file: - - Use [one of the options ESLint offers](https://eslint.org/docs/latest/user-guide/configuring/ignoring-code) to ignore files, namely a `.eslintignore` file, or `ignorePatterns` config. + - Use [one of the options ESLint offers to ignore files](https://eslint.org/docs/latest/user-guide/configuring/ignoring-code), namely a `.eslintignore` file, or `ignorePatterns` config. - If you **do** want to lint the file: - If you **do not** want to lint the file with [type-aware linting](../../getting-started/Typed_Linting.mdx): - - Use [ESLint's `overrides` configuration](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#configuration-based-on-glob-patterns) with our [`disable-type-checked`](../../users/Shared_Configurations.mdx#disable-type-checked) config to disable type checking for just that type of file. - - - - ```js title="eslint.config.js" - import tseslint from 'typescript-eslint'; - - export default tseslint.config( - // ... the rest of your config ... - { - files: ['**/*.js'], - extends: [tseslint.configs.disableTypeChecked], - }, - ); - ``` - - - - - ```js title=".eslintrc.cjs" - module.exports = { - // ... the rest of your config ... - overrides: [ - { - extends: ['plugin:@typescript-eslint/disable-type-checked'], - files: ['./**/*.js'], - }, - ], - }; - ``` - - - - - Alternatively to disable type checking for files manually, you can set `parserOptions: { project: false }` to an override for the files you wish to exclude. + - Use [ESLint's configuration objects](https://eslint.org/docs/latest/use/configure/configuration-files#specifying-files-with-arbitrary-extensions) with our [`disable-type-checked`](../../users/Shared_Configurations.mdx#disable-type-checked) config to disable type checking for just that type of file. - If you **do** want to lint the file with [type-aware linting](../../getting-started/Typed_Linting.mdx): - Check the `include` option of each of the TSConfigs that you provide to `parserOptions.project` - you must ensure that all files match an `include` glob, or else our tooling will not be able to find it. - If the file is a `.cjs`, `.js`, or `.mjs` file, make sure [`allowJs`](https://www.typescriptlang.org/tsconfig#allowJs) is enabled. @@ -108,8 +67,6 @@ These errors are caused by an ESLint config requesting type information be gener - [`tsconfig.eslint.json`](https://github.com/typescript-eslint/typescript-eslint/blob/main/tsconfig.eslint.json) - [`eslint.config.mjs`](https://github.com/typescript-eslint/typescript-eslint/blob/main/eslint.config.mjs) - - ### More Details This error may appear from the combination of two things: @@ -124,7 +81,7 @@ However, if no specified TSConfig includes the source file, the parser won't be This error most commonly happens on config files or similar that are not included in their project TSConfig(s). For example, many projects have files like: -- An `.eslintrc.cjs` / `eslint.config.js` with `parserOptions.project: ["./tsconfig.json"]` +- An `.eslintrc.cjs` / `eslint.config.mjs` with `parserOptions.project: ["./tsconfig.json"]` - A `tsconfig.json` with `include: ["src"]` In that case, viewing the file in an IDE with the ESLint extension will show the error notice that the file couldn't be linted because it isn't included in `tsconfig.json`. @@ -136,39 +93,46 @@ See our docs on [type aware linting](../../getting-started/Typed_Linting.mdx) fo You're using an outdated version of `@typescript-eslint/parser`. Update to the latest version to see a more informative version of this error message, explained [above](#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file 'backlink to I get errors telling me ESLint was configured to run ...'). - +## How do I disable type-checked linting for a file? -## I get errors telling me "Having many files run with the default project is known to cause performance issues and slow down linting." +Use [ESLint's configuration objects](https://eslint.org/docs/latest/use/configure/configuration-files#specifying-files-with-arbitrary-extensions) with our [`disable-type-checked`](../../users/Shared_Configurations.mdx#disable-type-checked) config to disable type checking for a `files` match that includes that file. -These errors are caused by using the [`EXPERIMENTAL_useProjectService`](../../packages/Parser.mdx#experimental_useprojectservice) `allowDefaultProject` with an excessively wide glob. -`allowDefaultProject` causes a new TypeScript "program" to be built for each "out of project" file it includes, which incurs a performance overhead for each file. +For example, to disable type-checked linting on all `.js` files: -To resolve this error, narrow the glob(s) used for `allowDefaultProject` to include fewer files. -For example: + + -```diff title="eslint.config.js" -parserOptions: { - EXPERIMENTAL_useProjectService: { - allowDefaultProject: [ -- "**/*.js", -+ "./*.js" - ] - } -} -``` +```js title="eslint.config.mjs" +import tseslint from 'typescript-eslint'; -You may also need to include more files in your `tsconfig.json`. -For example: +export default tseslint.config( + // ... the rest of your config ... + { + files: ['**/*.js'], + extends: [tseslint.configs.disableTypeChecked], + }, +); +``` -```diff title="tsconfig.json" -"include": [ - "src", -+ "*.js" -] + + + +```js title=".eslintrc.cjs" +module.exports = { + // ... the rest of your config ... + overrides: [ + { + extends: ['plugin:@typescript-eslint/disable-type-checked'], + files: ['./**/*.js'], + }, + ], +}; ``` -If you cannot do this, please [file an issue on typescript-eslint's typescript-estree package](https://github.com/typescript-eslint/typescript-eslint/issues/new?assignees=&labels=enhancement%2Ctriage&projects=&template=07-enhancement-other.yaml&title=Enhancement%3A+%3Ca+short+description+of+my+proposal%3E) telling us your use case and why you need more out-of-project files linted. -Be sure to include a minimal reproduction we can work with to understand your use case! + + + +Alternatively to disable type checking for files manually, you can set [`parserOptions: { project: false }`](../../packages/Parser.mdx#project) to an override for the files you wish to exclude. ## typescript-eslint thinks my variable is never nullish / is `any` / etc., but that is clearly not the case to me diff --git a/docs/users/Shared_Configurations.mdx b/docs/users/Shared_Configurations.mdx index f57bfccb0e72..b5b8810ffd98 100644 --- a/docs/users/Shared_Configurations.mdx +++ b/docs/users/Shared_Configurations.mdx @@ -17,7 +17,7 @@ import TabItem from '@theme/TabItem'; See [Getting Started > Quickstart](../getting-started/Quickstart.mdx) first to set up your ESLint configuration file. [Packages > typescript-eslint](../packages/TypeScript_ESLint.mdx) includes more documentation on the `tseslint` helper. -```js title="eslint.config.js" +```js title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; @@ -36,7 +36,7 @@ If your project does not enable [typed linting](../getting-started/Typed_Linting -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, @@ -69,7 +69,7 @@ If your project enables [typed linting](../getting-started/Typed_Linting.mdx), w -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, @@ -125,7 +125,7 @@ These rules are those whose reports are almost always for a bad practice and/or -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config(...tseslint.configs.recommended); ``` @@ -151,7 +151,7 @@ Rules newly added in this configuration are similarly useful to those in `recomm -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config(...tseslint.configs.recommendedTypeChecked); ``` @@ -177,7 +177,7 @@ Rules added in `strict` are more opinionated than recommended rules and might no -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config(...tseslint.configs.strict); ``` @@ -213,7 +213,7 @@ Rules newly added in this configuration are similarly useful (and opinionated) t -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config(...tseslint.configs.strictTypeChecked); ``` @@ -249,7 +249,7 @@ These rules are generally opinionated about enforcing simpler code patterns. -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config(...tseslint.configs.stylistic); ``` @@ -278,7 +278,7 @@ Rules newly added in this configuration are similarly opinionated to those in `s -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config(...tseslint.configs.stylisticTypeChecked); ``` @@ -343,7 +343,7 @@ If you use type-aware rules from other plugins, you will need to manually disabl -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, @@ -407,7 +407,7 @@ Additionally, it enables rules that promote using the more modern constructs Typ -```js title="eslint.config.js" +```js title="eslint.config.mjs" export default tseslint.config( eslint.configs.recommended, tseslint.configs.eslintRecommended, diff --git a/docs/users/What_About_Formatting.mdx b/docs/users/What_About_Formatting.mdx index 75a83c81ff59..711b77a33ff3 100644 --- a/docs/users/What_About_Formatting.mdx +++ b/docs/users/What_About_Formatting.mdx @@ -46,7 +46,7 @@ Using this config by adding it to the end of your `extends`: -```js title="eslint.config.js" +```js title="eslint.config.mjs" // @ts-check import eslint from '@eslint/js'; diff --git a/eslint.config.mjs b/eslint.config.mjs index 94efd64eff91..67bab36e44bb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -135,6 +135,7 @@ export default tseslint.config( { allowConstantLoopConditions: true }, ], '@typescript-eslint/no-unnecessary-type-parameters': 'error', + '@typescript-eslint/no-unused-expressions': 'error', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/prefer-literal-enum-member': [ 'error', @@ -212,13 +213,22 @@ export default tseslint.config( 'error', { commentPattern: '.*intentional fallthrough.*' }, ], + 'no-implicit-coercion': ['error', { boolean: false }], + 'no-lonely-if': 'error', + 'no-unreachable-loop': 'error', 'no-useless-call': 'error', 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-var': 'error', 'no-void': ['error', { allowAsStatement: true }], 'one-var': ['error', 'never'], + 'operator-assignment': 'error', 'prefer-arrow-callback': 'error', + 'prefer-const': 'error', 'prefer-object-has-own': 'error', + 'prefer-object-spread': 'error', 'prefer-rest-params': 'error', + radix: 'error', // // eslint-plugin-eslint-comment diff --git a/packages/ast-spec/CHANGELOG.md b/packages/ast-spec/CHANGELOG.md index 873fd871aadd..b0f17ded0be1 100644 --- a/packages/ast-spec/CHANGELOG.md +++ b/packages/ast-spec/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for ast-spec to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for ast-spec to align it with other projects, there were no code changes. diff --git a/packages/ast-spec/package.json b/packages/ast-spec/package.json index 917b095a755e..02cbf6e0ed97 100644 --- a/packages/ast-spec/package.json +++ b/packages/ast-spec/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/ast-spec", - "version": "7.16.1", + "version": "7.17.0", "description": "Complete specification for the TypeScript-ESTree AST", "private": true, "keywords": [ diff --git a/packages/eslint-plugin-internal/CHANGELOG.md b/packages/eslint-plugin-internal/CHANGELOG.md index dd7781f44040..2b030192c252 100644 --- a/packages/eslint-plugin-internal/CHANGELOG.md +++ b/packages/eslint-plugin-internal/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for eslint-plugin-internal to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for eslint-plugin-internal to align it with other projects, there were no code changes. diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 0dfda4102076..9706c5d3cd6d 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-internal", - "version": "7.16.1", + "version": "7.17.0", "private": true, "main": "dist/index.js", "types": "index.d.ts", @@ -15,10 +15,10 @@ }, "dependencies": { "@prettier/sync": "^0.5.1", - "@typescript-eslint/rule-tester": "7.16.1", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/type-utils": "7.16.1", - "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/rule-tester": "7.17.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "prettier": "^3.2.5" }, "devDependencies": { diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index f257f4f40395..895c2e348c6c 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,3 +1,31 @@ +## 7.17.0 (2024-07-22) + + +### 🚀 Features + +- **eslint-plugin:** backport no-unsafe-function type, no-wrapper-object-types from v8 to v7 + +- **eslint-plugin:** [return-await] add option to report in error-handling scenarios only, and deprecate "never" + + +### 🩹 Fixes + +- **eslint-plugin:** [no-floating-promises] check top-level type assertions (and more) + +- **eslint-plugin:** [strict-boolean-expressions] consider assertion function argument a boolean context + +- **eslint-plugin:** [no-unnecessary-condition] false positive on optional private field + + +### ❤️ Thank You + +- Armano +- Josh Goldberg ✨ +- Kirk Waiblinger +- StyleShit + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) diff --git a/packages/eslint-plugin/docs/rules/return-await.mdx b/packages/eslint-plugin/docs/rules/return-await.mdx index ea5304bdfc39..9be63cc9600c 100644 --- a/packages/eslint-plugin/docs/rules/return-await.mdx +++ b/packages/eslint-plugin/docs/rules/return-await.mdx @@ -21,23 +21,43 @@ The extended rule is named `return-await` instead of `no-return-await` because t ## Options ```ts -type Options = 'in-try-catch' | 'always' | 'never'; +type Options = + | 'in-try-catch' + | 'always' + | 'error-handling-correctness-only' + | 'never'; const defaultOptions: Options = 'in-try-catch'; ``` +The options in this rule distinguish between "ordinary contexts" and "error-handling contexts". +An error-handling context is anywhere where returning an unawaited promise would cause unexpected control flow regarding exceptions/rejections. +See detailed examples in the sections for each option. + +- If you return a promise within a `try` block, it should be awaited in order to trigger subsequent `catch` or `finally` blocks as expected. +- If you return a promise within a `catch` block, and there _is_ a `finally` block, it should be awaited in order to trigger the `finally` block as expected. +- If you return a promise between a `using` or `await using` declaration and the end of its scope, it should be awaited, since it behaves equivalently to code wrapped in a `try` block followed by a `finally`. + +Ordinary contexts are anywhere else a promise may be returned. +The choice of whether to await a returned promise in an ordinary context is mostly stylistic. + +With these terms defined, the options may be summarized as follows: + +| Option | Ordinary Context
(stylistic preference 🎨) | Error-Handling Context
(catches bugs 🐛) | Should I use this option? | +| :-------------------------------: | :----------------------------------------------: | :----------------------------------------------------------: | :--------------------------------------------------------: | +| `always` | `return await promise;` | `return await promise;` | ✅ Yes! | +| `in-try-catch` | `return promise;` | `return await promise;` | ✅ Yes! | +| `error-handling-correctness-only` | don't care 🤷 | `return await promise;` | 🟡 Okay to use, but the above options would be preferable. | +| `never` | `return promise;` | `return promise;`
(⚠️ This behavior may be harmful ⚠️) | ❌ No. This option is deprecated. | + ### `in-try-catch` -In cases where returning an unawaited promise would cause unexpected error-handling control flow, the rule enforces that `await` must be used. -Otherwise, the rule enforces that `await` must _not_ be used. +In error-handling contexts, the rule enforces that returned promises must be awaited. +In ordinary contexts, the rule enforces that returned promises _must not_ be awaited. -Listing the error-handling cases exhaustively: +This is a good option if you prefer the shorter `return promise` form for stylistic reasons, wherever it's safe to use. -- if you `return` a promise within a `try`, then it must be `await`ed, since it will always be followed by a `catch` or `finally`. -- if you `return` a promise within a `catch`, and there is _no_ `finally`, then it must _not_ be `await`ed. -- if you `return` a promise within a `catch`, and there _is_ a `finally`, then it _must_ be `await`ed. -- if you `return` a promise within a `finally`, then it must not be `await`ed. -- if you `return` a promise between a `using` or `await using` declaration and the end of its scope, it must be `await`ed, since it behaves equivalently to code wrapped in a `try` block. +Examples of code with `in-try-catch`: @@ -169,7 +189,9 @@ async function validInTryCatch7() { ### `always` -Requires that all returned promises are `await`ed. +Requires that all returned promises be awaited. + +This is a good option if you like the consistency of simply always awaiting promises, or prefer not having to consider the distinction between error-handling contexts and ordinary contexts. Examples of code with `always`: @@ -214,9 +236,49 @@ async function validAlways3() { +### `error-handling-correctness-only` + +In error-handling contexts, the rule enforces that returned promises must be awaited. +In ordinary contexts, the rule does not enforce any particular behavior around whether returned promises are awaited. + +This is a good option if you only want to benefit from rule's ability to catch control flow bugs in error-handling contexts, but don't want to enforce a particular style otherwise. + +:::info +We recommend you configure either `in-try-catch` or `always` instead of this option. +While the choice of whether to await promises outside of error-handling contexts is mostly stylistic, it's generally best to be consistent. +::: + +Examples of additional correct code with `error-handling-correctness-only`: + + + + +```ts option='"error-handling-correctness-only"' +async function asyncFunction(): Promise { + if (Math.random() < 0.5) { + return await Promise.resolve(); + } else { + return Promise.resolve(); + } +} +``` + + + + ### `never` -Disallows all `await`ing any returned promises. +Disallows awaiting any returned promises. + +:::warning + +This option is deprecated and will be removed in a future major version of typescript-eslint. + +The `never` option introduces undesirable behavior in error-handling contexts. +If you prefer to minimize returning awaited promises, consider instead using `in-try-catch` instead, which also generally bans returning awaited promises, but only where it is _safe_ not to await a promise. + +See more details at [typescript-eslint#9433](https://github.com/typescript-eslint/typescript-eslint/issues/9433). +::: Examples of code with `never`: diff --git a/packages/eslint-plugin/docs/rules/sort-type-constituents.mdx b/packages/eslint-plugin/docs/rules/sort-type-constituents.mdx index 188590aa38b1..9ce8157deffb 100644 --- a/packages/eslint-plugin/docs/rules/sort-type-constituents.mdx +++ b/packages/eslint-plugin/docs/rules/sort-type-constituents.mdx @@ -10,7 +10,7 @@ import TabItem from '@theme/TabItem'; > See **https://typescript-eslint.io/rules/sort-type-constituents** for documentation. :::danger Deprecated -This rule has been deprecated in favor of the [`perfectionist/sort-intersection-types`](https://eslint-plugin-perfectionist.azat.io/rules/sort-intersection-types) and [`perfectionist/sort-union-types`](https://eslint-plugin-perfectionist.azat.io/rules/sort-union-types) rules. +This rule has been deprecated in favor of the [`perfectionist/sort-intersection-types`](https://perfectionist.dev/rules/sort-intersection-types) and [`perfectionist/sort-union-types`](https://perfectionist.dev/rules/sort-union-types) rules. See [Docs: Deprecate sort-type-constituents in favor of eslint-plugin-perfectionist](https://github.com/typescript-eslint/typescript-eslint/issues/8915) and [eslint-plugin: Feature freeze naming and sorting stylistic rules](https://github.com/typescript-eslint/typescript-eslint/issues/8792) for more information. ::: diff --git a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx index ab7653a5c0db..5ce9e5dd975d 100644 --- a/packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx +++ b/packages/eslint-plugin/docs/rules/strict-boolean-expressions.mdx @@ -21,6 +21,7 @@ The following nodes are considered boolean expressions and their type is checked - Operands of logical binary operators (`lhs || rhs` and `lhs && rhs`). - Right-hand side operand is ignored when it's not a descendant of another boolean expression. This is to allow usage of boolean operators for their short-circuiting behavior. +- Asserted argument of an assertion function (`assert(arg)`). ## Examples @@ -55,6 +56,11 @@ let obj = {}; while (obj) { obj = getObj(); } + +// assertion functions without an `is` are boolean contexts. +declare function assert(value: unknown): asserts value; +let maybeString = Math.random() > 0.5 ? '' : undefined; +assert(maybeString); ```
diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 58b84f670e24..1bfaa0d8e30f 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "7.16.1", + "version": "7.17.0", "description": "TypeScript plugin for ESLint", "files": [ "dist", @@ -60,10 +60,10 @@ }, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/type-utils": "7.16.1", - "@typescript-eslint/utils": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -74,8 +74,8 @@ "@types/marked": "^5.0.2", "@types/mdast": "^4.0.3", "@types/natural-compare": "*", - "@typescript-eslint/rule-schema-to-typescript-types": "7.16.1", - "@typescript-eslint/rule-tester": "7.16.1", + "@typescript-eslint/rule-schema-to-typescript-types": "7.17.0", + "@typescript-eslint/rule-tester": "7.17.0", "ajv": "^6.12.6", "cross-env": "^7.0.3", "cross-fetch": "*", diff --git a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts index 53f13d96748f..0935496d4c96 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts @@ -64,6 +64,11 @@ export = { allowNever: false, }, ], + 'no-return-await': 'off', + '@typescript-eslint/return-await': [ + 'error', + 'error-handling-correctness-only', + ], '@typescript-eslint/unbound-method': 'error', '@typescript-eslint/use-unknown-in-catch-callback-variable': 'error', }, diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index 11d65130de62..3703af22c8d9 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -97,6 +97,11 @@ export = { allowNever: false, }, ], + 'no-return-await': 'off', + '@typescript-eslint/return-await': [ + 'error', + 'error-handling-correctness-only', + ], '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/unbound-method': 'error', '@typescript-eslint/unified-signatures': 'error', diff --git a/packages/eslint-plugin/src/rules/consistent-type-imports.ts b/packages/eslint-plugin/src/rules/consistent-type-imports.ts index f4e98d2d9e00..f2aa28297ae1 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-imports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-imports.ts @@ -177,25 +177,23 @@ export default createRule({ // definitely import type { TypeX } sourceImports.typeOnlyNamedImport = node; } - } else { - if ( - !sourceImports.valueOnlyNamedImport && - node.specifiers.length && - node.specifiers.every( - specifier => specifier.type === AST_NODE_TYPES.ImportSpecifier, - ) - ) { - sourceImports.valueOnlyNamedImport = node; - sourceImports.valueImport = node; - } else if ( - !sourceImports.valueImport && - node.specifiers.some( - specifier => - specifier.type === AST_NODE_TYPES.ImportDefaultSpecifier, - ) - ) { - sourceImports.valueImport = node; - } + } else if ( + !sourceImports.valueOnlyNamedImport && + node.specifiers.length && + node.specifiers.every( + specifier => specifier.type === AST_NODE_TYPES.ImportSpecifier, + ) + ) { + sourceImports.valueOnlyNamedImport = node; + sourceImports.valueImport = node; + } else if ( + !sourceImports.valueImport && + node.specifiers.some( + specifier => + specifier.type === AST_NODE_TYPES.ImportDefaultSpecifier, + ) + ) { + sourceImports.valueImport = node; } const typeSpecifiers: TSESTree.ImportClause[] = []; @@ -732,6 +730,7 @@ export default createRule({ } } else { // The import is both default and named. Insert named on new line because can't mix default type import and named type imports + // eslint-disable-next-line no-lonely-if if (fixStyle === 'inline-type-imports') { yield fixer.insertTextBefore( node, diff --git a/packages/eslint-plugin/src/rules/no-mixed-enums.ts b/packages/eslint-plugin/src/rules/no-mixed-enums.ts index d95d4a9058b7..ce6a706a10d0 100644 --- a/packages/eslint-plugin/src/rules/no-mixed-enums.ts +++ b/packages/eslint-plugin/src/rules/no-mixed-enums.ts @@ -170,18 +170,15 @@ export default createRule({ .getSymbolAtLocation(tsNode)! .getDeclarations()!; - for (const declaration of declarations) { - for (const member of (declaration as ts.EnumDeclaration).members) { - return member.initializer - ? tsutils.isTypeFlagSet( - typeChecker.getTypeAtLocation(member.initializer), - ts.TypeFlags.StringLike, - ) - ? AllowedType.String - : AllowedType.Number - : AllowedType.Number; - } - } + const [{ initializer }] = (declarations[0] as ts.EnumDeclaration) + .members; + return initializer && + tsutils.isTypeFlagSet( + typeChecker.getTypeAtLocation(initializer), + ts.TypeFlags.StringLike, + ) + ? AllowedType.String + : AllowedType.Number; } // Finally, we default to the type of the first enum member diff --git a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts index 8545b0e1c110..475262c2a4a0 100644 --- a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts @@ -79,20 +79,18 @@ export default createRule<[], MessageIds>({ }, }); } + } else if (node.parent.computed) { + // it is x!?.[y].z + suggest.push({ + messageId: 'suggestOptionalChain', + fix: removeToken(), + }); } else { - if (node.parent.computed) { - // it is x!?.[y].z - suggest.push({ - messageId: 'suggestOptionalChain', - fix: removeToken(), - }); - } else { - // it is x!?.y.z - suggest.push({ - messageId: 'suggestOptionalChain', - fix: removeToken(), - }); - } + // it is x!?.y.z + suggest.push({ + messageId: 'suggestOptionalChain', + fix: removeToken(), + }); } } else if ( node.parent.type === AST_NODE_TYPES.CallExpression && diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 9a9b836bcc92..e3bf94a30c78 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -211,15 +211,20 @@ export default createRule({ } const property = node.property; - if (property.type === AST_NODE_TYPES.Identifier) { - const propertyType = objectType.getProperty(property.name); - if ( - propertyType && - tsutils.isSymbolFlagSet(propertyType, ts.SymbolFlags.Optional) - ) { - return true; - } + // Get the actual property name, to account for private properties (this.#prop). + const propertyName = context.sourceCode.getText(property); + + const propertyType = objectType + .getProperties() + .find(prop => prop.name === propertyName); + + if ( + propertyType && + tsutils.isSymbolFlagSet(propertyType, ts.SymbolFlags.Optional) + ) { + return true; } + return false; } diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts index 44e10c5e33c8..3f6e7f69ea8b 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts @@ -6,13 +6,23 @@ import { createRule, getConstrainedTypeAtLocation, getParserServices, - getStaticStringValue, isTypeFlagSet, isUndefinedIdentifier, } from '../util'; type MessageId = 'noUnnecessaryTemplateExpression'; +const evenNumOfBackslashesRegExp = /(?({ name: 'no-unnecessary-template-expression', meta: { @@ -53,11 +63,15 @@ export default createRule<[], MessageId>({ return isString(type); } - function isLiteral(expression: TSESTree.Expression): boolean { + function isLiteral( + expression: TSESTree.Expression, + ): expression is TSESTree.Literal { return expression.type === AST_NODE_TYPES.Literal; } - function isTemplateLiteral(expression: TSESTree.Expression): boolean { + function isTemplateLiteral( + expression: TSESTree.Expression, + ): expression is TSESTree.TemplateLiteral { return expression.type === AST_NODE_TYPES.TemplateLiteral; } @@ -113,62 +127,150 @@ export default createRule<[], MessageId>({ return; } - const fixableExpressions = node.expressions.filter( - expression => - isLiteral(expression) || - isTemplateLiteral(expression) || - isUndefinedIdentifier(expression) || - isInfinityIdentifier(expression) || - isNaNIdentifier(expression), - ); + const fixableExpressions = node.expressions + .filter( + expression => + isLiteral(expression) || + isTemplateLiteral(expression) || + isUndefinedIdentifier(expression) || + isInfinityIdentifier(expression) || + isNaNIdentifier(expression), + ) + .reverse(); + + let nextCharacterIsOpeningCurlyBrace = false; + + for (const expression of fixableExpressions) { + const fixers: ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[])[] = + []; + const index = node.expressions.indexOf(expression); + const prevQuasi = node.quasis[index]; + const nextQuasi = node.quasis[index + 1]; + + if (nextQuasi.value.raw.length !== 0) { + nextCharacterIsOpeningCurlyBrace = + nextQuasi.value.raw.startsWith('{'); + } + + if (isLiteral(expression)) { + let escapedValue = ( + typeof expression.value === 'string' + ? // The value is already a string, so we're removing quotes: + // "'va`lue'" -> "va`lue" + expression.raw.slice(1, -1) + : // The value may be one of number | bigint | boolean | RegExp | null. + // In regular expressions, we escape every backslash + String(expression.value).replace(/\\/g, '\\\\') + ) + // The string or RegExp may contain ` or ${. + // We want both of these to be escaped in the final template expression. + // + // A pair of backslashes means "escaped backslash", so backslashes + // from this pair won't escape ` or ${. Therefore, to escape these + // sequences in the resulting template expression, we need to escape + // all sequences that are preceded by an even number of backslashes. + // + // This RegExp does the following transformations: + // \` -> \` + // \\` -> \\\` + // \${ -> \${ + // \\${ -> \\\${ + .replace( + new RegExp( + String(evenNumOfBackslashesRegExp.source) + '(`|\\${)', + 'g', + ), + '\\$1', + ); + + // `...${'...$'}{...` + // ^^^^ + if ( + nextCharacterIsOpeningCurlyBrace && + endsWithUnescapedDollarSign(escapedValue) + ) { + escapedValue = escapedValue.replaceAll(/\$$/g, '\\$'); + } + + if (escapedValue.length !== 0) { + nextCharacterIsOpeningCurlyBrace = escapedValue.startsWith('{'); + } + + fixers.push(fixer => [fixer.replaceText(expression, escapedValue)]); + } else if (isTemplateLiteral(expression)) { + // Since we iterate from the last expression to the first, + // a subsequent expression can tell the current expression + // that it starts with {. + // + // `... ${`... $`}${'{...'} ...` + // ^ ^ subsequent expression starts with { + // current expression ends with a dollar sign, + // so '$' + '{' === '${' (bad news for us). + // Let's escape the dollar sign at the end. + if ( + nextCharacterIsOpeningCurlyBrace && + endsWithUnescapedDollarSign( + expression.quasis[expression.quasis.length - 1].value.raw, + ) + ) { + fixers.push(fixer => [ + fixer.replaceTextRange( + [expression.range[1] - 2, expression.range[1] - 2], + '\\', + ), + ]); + } + if ( + expression.quasis.length === 1 && + expression.quasis[0].value.raw.length !== 0 + ) { + nextCharacterIsOpeningCurlyBrace = + expression.quasis[0].value.raw.startsWith('{'); + } + + // Remove the beginning and trailing backtick characters. + fixers.push(fixer => [ + fixer.removeRange([expression.range[0], expression.range[0] + 1]), + fixer.removeRange([expression.range[1] - 1, expression.range[1]]), + ]); + } else { + nextCharacterIsOpeningCurlyBrace = false; + } + + // `... $${'{...'} ...` + // ^^^^^ + if ( + nextCharacterIsOpeningCurlyBrace && + endsWithUnescapedDollarSign(prevQuasi.value.raw) + ) { + fixers.push(fixer => [ + fixer.replaceTextRange( + [prevQuasi.range[1] - 3, prevQuasi.range[1] - 2], + '\\$', + ), + ]); + } - fixableExpressions.forEach(expression => { context.report({ node: expression, messageId: 'noUnnecessaryTemplateExpression', fix(fixer): TSESLint.RuleFix[] { - const index = node.expressions.indexOf(expression); - const prevQuasi = node.quasis[index]; - const nextQuasi = node.quasis[index + 1]; - - // Remove the quasis' parts that are related to the current expression. - const fixes = [ + return [ + // Remove the quasis' parts that are related to the current expression. fixer.removeRange([ prevQuasi.range[1] - 2, expression.range[0], ]), - fixer.removeRange([ expression.range[1], nextQuasi.range[0] + 1, ]), - ]; - const stringValue = getStaticStringValue(expression); - - if (stringValue != null) { - const escapedValue = stringValue.replace(/([`$\\])/g, '\\$1'); - - fixes.push(fixer.replaceText(expression, escapedValue)); - } else if (isTemplateLiteral(expression)) { - // Note that some template literals get handled in the previous branch too. - // Remove the beginning and trailing backtick characters. - fixes.push( - fixer.removeRange([ - expression.range[0], - expression.range[0] + 1, - ]), - fixer.removeRange([ - expression.range[1] - 1, - expression.range[1], - ]), - ); - } - - return fixes; + ...fixers.flatMap(cb => cb(fixer)), + ]; }, }); - }); + } }, }; }, diff --git a/packages/eslint-plugin/src/rules/no-unsafe-return.ts b/packages/eslint-plugin/src/rules/no-unsafe-return.ts index ae959627c09d..d9c7cd60cc0f 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-return.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-return.ts @@ -166,7 +166,8 @@ export default createRule({ }); } - for (const signature of functionType.getCallSignatures()) { + const signature = functionType.getCallSignatures().at(0); + if (signature) { const functionReturnType = signature.getReturnType(); const result = isUnsafeAssignment( returnNodeType, diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 8359c4cce0d9..49a074062ba9 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -1,4 +1,7 @@ -import type { ScopeVariable } from '@typescript-eslint/scope-manager'; +import type { + Definition, + ScopeVariable, +} from '@typescript-eslint/scope-manager'; import { DefinitionType, PatternVisitor, @@ -181,6 +184,38 @@ export default createRule({ return options; })(); + /** + * Determines what variable type a def is. + * @param def the declaration to check + * @returns a simple name for the types of variables that this rule supports + */ + function defToVariableType(def: Definition): VariableType { + /* + * This `destructuredArrayIgnorePattern` error report works differently from the catch + * clause and parameter error reports. _Both_ the `varsIgnorePattern` and the + * `destructuredArrayIgnorePattern` will be checked for array destructuring. However, + * for the purposes of the report, the currently defined behavior is to only inform the + * user of the `destructuredArrayIgnorePattern` if it's present (regardless of the fact + * that the `varsIgnorePattern` would also apply). If it's not present, the user will be + * informed of the `varsIgnorePattern`, assuming that's present. + */ + if ( + options.destructuredArrayIgnorePattern && + def.name.parent.type === AST_NODE_TYPES.ArrayPattern + ) { + return 'array-destructure'; + } + + switch (def.type) { + case DefinitionType.CatchClause: + return 'catch-clause'; + case DefinitionType.Parameter: + return 'parameter'; + default: + return 'variable'; + } + } + /** * Gets a given variable's description and configured ignore pattern * based on the provided variableType @@ -202,7 +237,7 @@ export default createRule({ case 'catch-clause': return { pattern: options.caughtErrorsIgnorePattern?.toString(), - variableDescription: 'args', + variableDescription: 'caught errors', }; case 'parameter': @@ -228,33 +263,13 @@ export default createRule({ function getDefinedMessageData( unusedVar: ScopeVariable, ): Record { - const def = unusedVar.defs.at(0)?.type; + const def = unusedVar.defs.at(0); let additionalMessageData = ''; if (def) { - const { variableDescription, pattern } = (() => { - switch (def) { - case DefinitionType.CatchClause: - if (options.caughtErrorsIgnorePattern) { - return getVariableDescription('catch-clause'); - } - break; - - case DefinitionType.Parameter: - if (options.argsIgnorePattern) { - return getVariableDescription('parameter'); - } - break; - - default: - if (options.varsIgnorePattern) { - return getVariableDescription('variable'); - } - break; - } - - return { pattern: undefined, variableDescription: undefined }; - })(); + const { variableDescription, pattern } = getVariableDescription( + defToVariableType(def), + ); if (pattern && variableDescription) { additionalMessageData = `. Allowed unused ${variableDescription} must match ${pattern}`; @@ -281,18 +296,9 @@ export default createRule({ let additionalMessageData = ''; if (def) { - const { variableDescription, pattern } = (() => { - if ( - def.name.parent.type === AST_NODE_TYPES.ArrayPattern && - options.destructuredArrayIgnorePattern - ) { - return getVariableDescription('array-destructure'); - } else if (options.varsIgnorePattern) { - return getVariableDescription('variable'); - } - - return { pattern: undefined, variableDescription: undefined }; - })(); + const { variableDescription, pattern } = getVariableDescription( + defToVariableType(def), + ); if (pattern && variableDescription) { additionalMessageData = `. Allowed unused ${variableDescription} must match ${pattern}`; @@ -491,21 +497,20 @@ export default createRule({ ) { continue; } - } else { - // skip ignored variables - if ( - def.name.type === AST_NODE_TYPES.Identifier && - options.varsIgnorePattern?.test(def.name.name) - ) { - if (options.reportUsedIgnorePattern && used) { - context.report({ - node: def.name, - messageId: 'usedIgnoredVar', - data: getUsedIgnoredMessageData(variable, 'variable'), - }); - } - continue; + } + // skip ignored variables + else if ( + def.name.type === AST_NODE_TYPES.Identifier && + options.varsIgnorePattern?.test(def.name.name) + ) { + if (options.reportUsedIgnorePattern && used) { + context.report({ + node: def.name, + messageId: 'usedIgnoredVar', + data: getUsedIgnoredMessageData(variable, 'variable'), + }); } + continue; } if (hasRestSpreadSibling(variable)) { diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index af68f6e6c515..ad6579b5f1db 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -219,50 +219,44 @@ function getFixer( ) { // user has opted-in to the unsafe behavior useSuggestionFixer = false; + } + // optional chain specifically will union `undefined` into the final type + // so we need to make sure that there is at least one operand that includes + // `undefined`, or else we're going to change the final type - which is + // unsafe and might cause downstream type errors. + else if ( + lastOperand.comparisonType === NullishComparisonType.EqualNullOrUndefined || + lastOperand.comparisonType === + NullishComparisonType.NotEqualNullOrUndefined || + lastOperand.comparisonType === NullishComparisonType.StrictEqualUndefined || + lastOperand.comparisonType === + NullishComparisonType.NotStrictEqualUndefined || + (operator === '||' && + lastOperand.comparisonType === NullishComparisonType.NotBoolean) + ) { + // we know the last operand is an equality check - so the change in types + // DOES NOT matter and will not change the runtime result or cause a type + // check error + useSuggestionFixer = false; } else { - // optional chain specifically will union `undefined` into the final type - // so we need to make sure that there is at least one operand that includes - // `undefined`, or else we're going to change the final type - which is - // unsafe and might cause downstream type errors. - - if ( - lastOperand.comparisonType === - NullishComparisonType.EqualNullOrUndefined || - lastOperand.comparisonType === - NullishComparisonType.NotEqualNullOrUndefined || - lastOperand.comparisonType === - NullishComparisonType.StrictEqualUndefined || - lastOperand.comparisonType === - NullishComparisonType.NotStrictEqualUndefined || - (operator === '||' && - lastOperand.comparisonType === NullishComparisonType.NotBoolean) - ) { - // we know the last operand is an equality check - so the change in types - // DOES NOT matter and will not change the runtime result or cause a type - // check error - useSuggestionFixer = false; - } else { - useSuggestionFixer = true; - - for (const operand of chain) { - if ( - includesType(parserServices, operand.node, ts.TypeFlags.Undefined) - ) { - useSuggestionFixer = false; - break; - } - } + useSuggestionFixer = true; - // TODO - we could further reduce the false-positive rate of this check by - // checking for cases where the change in types don't matter like - // the test location of an if/while/etc statement. - // but it's quite complex to do this without false-negatives, so - // for now we'll just be over-eager with our matching. - // - // it's MUCH better to false-positive here and only provide a - // suggestion fixer, rather than false-negative and autofix to - // broken code. + for (const operand of chain) { + if (includesType(parserServices, operand.node, ts.TypeFlags.Undefined)) { + useSuggestionFixer = false; + break; + } } + + // TODO - we could further reduce the false-positive rate of this check by + // checking for cases where the change in types don't matter like + // the test location of an if/while/etc statement. + // but it's quite complex to do this without false-negatives, so + // for now we'll just be over-eager with our matching. + // + // it's MUCH better to false-positive here and only provide a + // suggestion fixer, rather than false-negative and autofix to + // broken code. } // In its most naive form we could just slap `?.` for every single part of the diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index ab4534d2a269..716962a8a11b 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -24,6 +24,12 @@ interface ScopeInfo { owningFunc: FunctionNode; } +type Option = + | 'in-try-catch' + | 'always' + | 'never' + | 'error-handling-correctness-only'; + export default createRule({ name: 'return-await', meta: { @@ -31,6 +37,9 @@ export default createRule({ description: 'Enforce consistent awaiting of returned promises', requiresTypeChecking: true, extendsBaseRule: 'no-return-await', + recommended: { + strict: ['error-handling-correctness-only'], + }, }, fixable: 'code', hasSuggestions: true, @@ -50,7 +59,12 @@ export default createRule({ schema: [ { type: 'string', - enum: ['in-try-catch', 'always', 'never'], + enum: [ + 'in-try-catch', + 'always', + 'never', + 'error-handling-correctness-only', + ] satisfies Option[], }, ], }, @@ -271,92 +285,70 @@ export default createRule({ const type = checker.getTypeAtLocation(child); const isThenable = tsutils.isThenableType(checker, expression, type); - if (!isAwait && !isThenable) { - return; - } - - if (isAwait && !isThenable) { - // any/unknown could be thenable; do not auto-fix - const useAutoFix = !(isTypeAnyType(type) || isTypeUnknownType(type)); - - context.report({ - messageId: 'nonPromiseAwait', - node, - ...fixOrSuggest(useAutoFix, { - messageId: 'nonPromiseAwait', - fix: fixer => removeAwait(fixer, node), - }), - }); - return; - } + // handle awaited _non_thenables - const affectsErrorHandling = - affectsExplicitErrorHandling(expression) || - affectsExplicitResourceManagement(node); - const useAutoFix = !affectsErrorHandling; + if (!isThenable) { + if (isAwait) { + // any/unknown could be thenable; do not auto-fix + const useAutoFix = !(isTypeAnyType(type) || isTypeUnknownType(type)); - if (option === 'always') { - if (!isAwait && isThenable) { context.report({ - messageId: 'requiredPromiseAwait', + messageId: 'nonPromiseAwait', node, ...fixOrSuggest(useAutoFix, { - messageId: 'requiredPromiseAwaitSuggestion', - fix: fixer => - insertAwait( - fixer, - node, - isHigherPrecedenceThanAwait(expression), - ), + messageId: 'nonPromiseAwait', + fix: fixer => removeAwait(fixer, node), }), }); } - return; } - if (option === 'never') { - if (isAwait) { - context.report({ - messageId: 'disallowedPromiseAwait', - node, - ...fixOrSuggest(useAutoFix, { - messageId: 'disallowedPromiseAwaitSuggestion', - fix: fixer => removeAwait(fixer, node), - }), - }); - } + // At this point it's definitely a thenable. - return; - } + const affectsErrorHandling = + affectsExplicitErrorHandling(expression) || + affectsExplicitResourceManagement(node); + const useAutoFix = !affectsErrorHandling; - if (option === 'in-try-catch') { - if (isAwait && !affectsErrorHandling) { - context.report({ - messageId: 'disallowedPromiseAwait', - node, - ...fixOrSuggest(useAutoFix, { - messageId: 'disallowedPromiseAwaitSuggestion', - fix: fixer => removeAwait(fixer, node), - }), - }); - } else if (!isAwait && affectsErrorHandling) { - context.report({ - messageId: 'requiredPromiseAwait', - node, - ...fixOrSuggest(useAutoFix, { - messageId: 'requiredPromiseAwaitSuggestion', - fix: fixer => - insertAwait( - fixer, - node, - isHigherPrecedenceThanAwait(expression), - ), - }), - }); - } + const ruleConfiguration = getConfiguration(option as Option); - return; + const shouldAwaitInCurrentContext = affectsErrorHandling + ? ruleConfiguration.errorHandlingContext + : ruleConfiguration.ordinaryContext; + + switch (shouldAwaitInCurrentContext) { + case "don't-care": + break; + case 'await': + if (!isAwait) { + context.report({ + messageId: 'requiredPromiseAwait', + node, + ...fixOrSuggest(useAutoFix, { + messageId: 'requiredPromiseAwaitSuggestion', + fix: fixer => + insertAwait( + fixer, + node, + isHigherPrecedenceThanAwait(expression), + ), + }), + }); + } + break; + case 'no-await': + if (isAwait) { + context.report({ + messageId: 'disallowedPromiseAwait', + node, + ...fixOrSuggest(useAutoFix, { + messageId: 'disallowedPromiseAwaitSuggestion', + fix: fixer => removeAwait(fixer, node), + }), + }); + } + break; } } @@ -406,6 +398,38 @@ export default createRule({ }, }); +type WhetherToAwait = 'await' | 'no-await' | "don't-care"; + +interface RuleConfiguration { + ordinaryContext: WhetherToAwait; + errorHandlingContext: WhetherToAwait; +} + +function getConfiguration(option: Option): RuleConfiguration { + switch (option) { + case 'always': + return { + ordinaryContext: 'await', + errorHandlingContext: 'await', + }; + case 'never': + return { + ordinaryContext: 'no-await', + errorHandlingContext: 'no-await', + }; + case 'error-handling-correctness-only': + return { + ordinaryContext: "don't-care", + errorHandlingContext: 'await', + }; + case 'in-try-catch': + return { + ordinaryContext: 'no-await', + errorHandlingContext: 'await', + }; + } +} + function fixOrSuggest( useFix: boolean, suggestion: TSESLint.SuggestionReportDescriptor, diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index a158dfff2ba0..931ed66fd3a5 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -189,6 +189,7 @@ export default createRule({ WhileStatement: traverseTestExpression, 'LogicalExpression[operator!="??"]': traverseLogicalExpression, 'UnaryExpression[operator="!"]': traverseUnaryLogicalExpression, + CallExpression: traverseCallExpression, }; type TestExpression = @@ -232,10 +233,139 @@ export default createRule({ // left argument is always treated as a condition traverseNode(node.left, true); // if the logical expression is used for control flow, - // then it's right argument is used for it's side effects only + // then its right argument is used for its side effects only traverseNode(node.right, isCondition); } + function traverseCallExpression(node: TSESTree.CallExpression): void { + const assertedArgument = findAssertedArgument(node); + if (assertedArgument != null) { + traverseNode(assertedArgument, true); + } + } + + /** + * Inspect a call expression to see if it's a call to an assertion function. + * If it is, return the node of the argument that is asserted. + */ + function findAssertedArgument( + node: TSESTree.CallExpression, + ): TSESTree.Expression | undefined { + // If the call looks like `assert(expr1, expr2, ...c, d, e, f)`, then we can + // only care if `expr1` or `expr2` is asserted, since anything that happens + // within or after a spread argument is out of scope to reason about. + const checkableArguments: TSESTree.Expression[] = []; + for (const argument of node.arguments) { + if (argument.type === AST_NODE_TYPES.SpreadElement) { + break; + } + + checkableArguments.push(argument); + } + + // nothing to do + if (checkableArguments.length === 0) { + return undefined; + } + + // Game plan: we're going to check the type of the callee. If it has call + // signatures and they _ALL_ agree that they assert on a parameter at the + // _SAME_ position, we'll consider the argument in that position to be an + // asserted argument. + const calleeType = getConstrainedTypeAtLocation(services, node.callee); + const callSignatures = tsutils.getCallSignaturesOfType(calleeType); + + let assertedParameterIndex: number | undefined = undefined; + for (const signature of callSignatures) { + const declaration = signature.getDeclaration(); + const returnTypeAnnotation = declaration.type; + + // Be sure we're dealing with a truthiness assertion function. + if ( + !( + returnTypeAnnotation != null && + ts.isTypePredicateNode(returnTypeAnnotation) && + // This eliminates things like `x is string` and `asserts x is T` + // leaving us with just the `asserts x` cases. + returnTypeAnnotation.type == null && + // I think this is redundant but, still, it needs to be true + returnTypeAnnotation.assertsModifier != null + ) + ) { + return undefined; + } + + const assertionTarget = returnTypeAnnotation.parameterName; + if (assertionTarget.kind !== ts.SyntaxKind.Identifier) { + // This can happen when asserting on `this`. Ignore! + return undefined; + } + + // If the first parameter is `this`, skip it, so that our index matches + // the index of the argument at the call site. + const firstParameter = declaration.parameters.at(0); + const nonThisParameters = + firstParameter?.name.kind === ts.SyntaxKind.Identifier && + firstParameter.name.text === 'this' + ? declaration.parameters.slice(1) + : declaration.parameters; + + // Don't bother inspecting parameters past the number of + // arguments we have at the call site. + const checkableNonThisParameters = nonThisParameters.slice( + 0, + checkableArguments.length, + ); + + let assertedParameterIndexForThisSignature: number | undefined; + for (const [index, parameter] of checkableNonThisParameters.entries()) { + if (parameter.dotDotDotToken != null) { + // Cannot assert a rest parameter, and can't have a rest parameter + // before the asserted parameter. It's not only a TS error, it's + // not something we can logically make sense of, so give up here. + return undefined; + } + + if (parameter.name.kind !== ts.SyntaxKind.Identifier) { + // Only identifiers are valid for assertion targets, so skip over + // anything like `{ destructuring: parameter }: T` + continue; + } + + // we've found a match between the "target"s in + // `function asserts(target: T): asserts target;` + if (parameter.name.text === assertionTarget.text) { + assertedParameterIndexForThisSignature = index; + break; + } + } + + if (assertedParameterIndexForThisSignature == null) { + // Didn't find an assertion target in this signature that could match + // the call site. + return undefined; + } + + if ( + assertedParameterIndex != null && + assertedParameterIndex !== assertedParameterIndexForThisSignature + ) { + // The asserted parameter we found for this signature didn't match + // previous signatures. + return undefined; + } + + assertedParameterIndex = assertedParameterIndexForThisSignature; + } + + // Didn't find a unique assertion index. + if (assertedParameterIndex == null) { + return undefined; + } + + return checkableArguments[assertedParameterIndex]; + } + /** * Inspects any node. * diff --git a/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts b/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts index 516fceda7699..56227d7ed11d 100644 --- a/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts +++ b/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts @@ -197,7 +197,7 @@ export function getFunctionHeadLoc( } return { - start: Object.assign({}, start), - end: Object.assign({}, end), + start: { ...start }, + end: { ...end }, }; } diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 9ba55c9d87f7..c175bb736882 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -160,7 +160,7 @@ type RequireKeys< > = ExcludeKeys & { [k in Keys]-?: Exclude }; function getEnumNames(myEnum: Record): T[] { - return Object.keys(myEnum).filter(x => isNaN(parseInt(x))) as T[]; + return Object.keys(myEnum).filter(x => isNaN(Number(x))) as T[]; } /** diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/return-await.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/return-await.shot index 1bd15b8a41e8..d10c743c959c 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/return-await.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/return-await.shot @@ -178,6 +178,20 @@ async function validAlways3() { `; exports[`Validating rule docs return-await.mdx code examples ESLint output 5`] = ` +"Correct +Options: "error-handling-correctness-only" + +async function asyncFunction(): Promise { + if (Math.random() < 0.5) { + return await Promise.resolve(); + } else { + return Promise.resolve(); + } +} +" +`; + +exports[`Validating rule docs return-await.mdx code examples ESLint output 6`] = ` "Incorrect Options: "never" @@ -200,7 +214,7 @@ async function invalidNever3() { " `; -exports[`Validating rule docs return-await.mdx code examples ESLint output 6`] = ` +exports[`Validating rule docs return-await.mdx code examples ESLint output 7`] = ` "Correct Options: "never" diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-boolean-expressions.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-boolean-expressions.shot index b547a7e6e0c5..ac6b144bf4bd 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-boolean-expressions.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/strict-boolean-expressions.shot @@ -34,6 +34,12 @@ while (obj) { ~~~ Unexpected object value in conditional. The condition is always true. obj = getObj(); } + +// assertion functions without an \`is\` are boolean contexts. +declare function assert(value: unknown): asserts value; +let maybeString = Math.random() > 0.5 ? '' : undefined; +assert(maybeString); + ~~~~~~~~~~~ Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly. " `; diff --git a/packages/eslint-plugin/tests/rules/dot-notation.test.ts b/packages/eslint-plugin/tests/rules/dot-notation.test.ts index 181cf0d6d3be..b799ef3243c3 100644 --- a/packages/eslint-plugin/tests/rules/dot-notation.test.ts +++ b/packages/eslint-plugin/tests/rules/dot-notation.test.ts @@ -1,4 +1,4 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import rule from '../../src/rules/dot-notation'; import { getFixturesRootDir } from '../RuleTester'; @@ -239,13 +239,19 @@ x.pub_prop = 123; errors: [{ messageId: 'useDot', data: { key: q('SHOUT_CASE') } }], }, { - code: 'a\n' + " ['SHOUT_CASE'];", - output: 'a\n' + ' .SHOUT_CASE;', + code: noFormat` +a + ['SHOUT_CASE']; + `, + output: ` +a + .SHOUT_CASE; + `, errors: [ { messageId: 'useDot', data: { key: q('SHOUT_CASE') }, - line: 2, + line: 3, column: 4, }, ], @@ -279,8 +285,14 @@ x.pub_prop = 123; ], }, { - code: 'foo\n' + ' .while;', - output: 'foo\n' + ' ["while"];', + code: noFormat` +foo + .while; + `, + output: ` +foo + ["while"]; + `, options: [{ allowKeywords: false }], errors: [{ messageId: 'useBrackets', data: { key: 'while' } }], }, diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index 5b84d30123de..5b43fab4b275 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -852,6 +852,19 @@ type Foo = { [key: string]: () => number | undefined } | null; declare const foo: Foo; foo?.['bar']()?.toExponential(); `, + { + parserOptions: optionsWithExactOptionalPropertyTypes, + code: ` +class ConsistentRand { + #rand?: number; + + getCachedRand() { + this.#rand ??= Math.random(); + return this.#rand; + } +} + `, + }, ], invalid: [ // Ensure that it's checking in all the right places diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts index fff88fdc3bf3..e7137ada9073 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts @@ -1,3 +1,4 @@ +import type { InvalidTestCase } from '@typescript-eslint/rule-tester'; import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import rule from '../../src/rules/no-unnecessary-template-expression'; @@ -13,6 +14,888 @@ const ruleTester = new RuleTester({ }, }); +const invalidCases: readonly InvalidTestCase< + 'noUnnecessaryTemplateExpression', + [] +>[] = [ + { + code: '`${1}`;', + output: '`1`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 5, + }, + ], + }, + { + code: '`${1n}`;', + output: '`1`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 6, + }, + ], + }, + { + code: '`${0o25}`;', + output: '`21`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + { + code: '`${0b1010} ${0b1111}`;', + output: '`10 15`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 10, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 14, + endColumn: 20, + }, + ], + }, + { + code: '`${0x25}`;', + output: '`37`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + { + code: '`${/a/}`;', + output: '`/a/`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + ], + }, + { + code: '`${/a/gim}`;', + output: '`/a/gim`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 10, + }, + ], + }, + + { + code: noFormat`\`\${ 1 }\`;`, + output: '`1`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ 'a' }\`;`, + output: `'a';`, + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ "a" }\`;`, + output: `"a";`, + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ 'a' + 'b' }\`;`, + output: `'a' + 'b';`, + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`${true}`;', + output: '`true`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + + { + code: noFormat`\`\${ true }\`;`, + output: '`true`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`${null}`;', + output: '`null`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + + { + code: noFormat`\`\${ null }\`;`, + output: '`null`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`${undefined}`;', + output: '`undefined`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 13, + }, + ], + }, + + { + code: noFormat`\`\${ undefined }\`;`, + output: '`undefined`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`${Infinity}`;', + output: '`Infinity`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 12, + }, + ], + }, + + { + code: '`${NaN}`;', + output: '`NaN`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + ], + }, + + { + code: "`${'a'} ${'b'}`;", + output: '`a b`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 11, + endColumn: 14, + }, + ], + }, + + { + code: noFormat`\`\${ 'a' } \${ 'b' }\`;`, + output: '`a b`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`use${'less'}`;", + output: '`useless`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + }, + ], + }, + + { + code: '`use${`less`}`;', + output: '`useless`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + }, + ], + }, + { + code: noFormat` +\`u\${ + // hopefully this comment is not needed. + 'se' + +}\${ + \`le\${ \`ss\` }\` +}\`; + `, + output: [ + ` +\`use\${ + \`less\` +}\`; + `, + ` +\`useless\`; + `, + ], + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 4, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 7, + column: 3, + endLine: 7, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 7, + column: 10, + endLine: 7, + }, + ], + }, + { + code: noFormat` +\`use\${ + \`less\` +}\`; + `, + output: ` +\`useless\`; + `, + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 3, + column: 3, + endColumn: 9, + }, + ], + }, + + { + code: "`${'1 + 1 ='} ${2}`;", + output: '`1 + 1 = 2`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 13, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 17, + endColumn: 18, + }, + ], + }, + + { + code: "`${'a'} ${true}`;", + output: '`a true`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 11, + endColumn: 15, + }, + ], + }, + + { + code: "`${String(Symbol.for('test'))}`;", + output: "String(Symbol.for('test'));", + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 30, + }, + ], + }, + + { + code: "`${'`'}`;", + output: "'`';", + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`back${'`'}tick`;", + output: '`back\\`tick`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`dollar${'${`this is test`}'}sign`;", + output: '`dollar\\${\\`this is test\\`}sign`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`complex${\'`${"`${test}`"}`\'}case`;', + output: '`complex\\`\\${"\\`\\${test}\\`"}\\`case`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`some ${'\\\\${test}'} string`;", + output: '`some \\\\\\${test} string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`some ${'\\\\`'} string`;", + output: '`some \\\\\\` string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`some ${/`/} string`;', + output: '`some /\\`/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\`/} string`;', + output: '`some /\\\\\\`/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\`/} string`;', + output: '`some /\\\\\\\\\\`/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\\\`/} string`;', + output: '`some /\\\\\\\\\\\\\\`/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/${}/} string`;', + output: '`some /\\${}/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/$ {}/} string`;', + output: '`some /$ {}/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\/} string`;', + output: '`some /\\\\\\\\/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\\\b/} string`;', + output: '`some /\\\\\\\\\\\\b/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\\\\\/} string`;', + output: '`some /\\\\\\\\\\\\\\\\/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: "` ${''} `;", + output: '` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${""} \`;`, + output: '` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${``} `;', + output: '` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${'\\\`'} \`;`, + output: '` \\` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\\\`'} `;", + output: '` \\\\\\` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'$'}{} `;", + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${'\\$'}{} \`;`, + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\\\$'}{} `;", + output: '` \\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\\\$ '}{} `;", + output: '` \\\\$ {} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${'\\\\\\$'}{} \`;`, + output: '` \\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` \\\\${'\\\\$'}{} `;", + output: '` \\\\\\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${'{$'}{} `;", + output: '` \\${\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${'${$'}{} `;", + output: '` $\\${\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'foo$'}{} `;", + output: '` foo\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`$`} `;', + output: '` $ `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`$`}{} `;', + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`$`} {} `;', + output: '` $ {} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`$`}${undefined}{} `;', + output: ['` $${undefined}{} `;', '` $undefined{} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: '` ${`foo$`}{} `;', + output: '` foo\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'$'}${''}{} `;", + output: ["` \\$${''}{} `;", '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` ${'$'}${``}{} `;", + output: ['` \\$${``}{} `;', '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` ${'foo$'}${''}${``}{} `;", + output: ["` foo\\$${''}{} `;", '` foo\\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${'{}'} `;", + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${undefined}${'{}'} `;", + output: ["` $undefined${'{}'} `;", '` $undefined{} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${''}${undefined}${'{}'} `;", + output: ['` $${undefined}{} `;', '` $undefined{} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` \\$${'{}'} `;", + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${'foo'}${'{'} `;", + output: ["` $foo${'{'} `;", '` $foo{ `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${'{ foo'}${'{'} `;", + output: ["` \\${ foo${'{'} `;", '` \\${ foo{ `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` \\\\$${'{}'} `;", + output: '` \\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` \\\\\\$${'{}'} `;", + output: '` \\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` foo$${'{}'} `;", + output: '` foo\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${''}${'{}'} `;", + output: ["` \\$${'{}'} `;", '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${''} `;", + output: '` $ `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` $${`{}`} `;', + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` $${``}${`{}`} `;', + output: ['` \\$${`{}`} `;', '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: '` $${``}${`foo{}`} `;', + output: ['` $${`foo{}`} `;', '` $foo{} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${`${''}${`${``}`}`}${`{a}`} `;", + output: [ + "` \\$${''}${`${``}`}${`{a}`} `;", + '` \\$${``}{a} `;', + '` \\${a} `;', + ], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${''}${`{}`} `;", + output: ['` \\$${`{}`} `;', '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${``}${'{}'} `;", + output: ["` \\$${'{}'} `;", '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${''}${``}${'{}'} `;", + output: ['` \\$${``}{} `;', '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` ${'$'} `;", + output: '` $ `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'$'}${'{}'} `;", + output: ["` \\$${'{}'} `;", '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` ${'$'}${''}${'{'} `;", + output: ["` \\$${''}{ `;", '` \\${ `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: '` ${`\n\\$`}{} `;', + output: '` \n\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`\n\\\\$`}{} `;', + output: '` \n\\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + + { + code: "`${'\\u00E5'}`;", + output: "'\\u00E5';", + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "`${'\\n'}`;", + output: "'\\n';", + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\u00E5'} `;", + output: '` \\u00E5 `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\n'} `;", + output: '` \\n `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${"\\n"} \`;`, + output: '` \\n `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`\\n`} `;', + output: '` \\n `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${ 'A\\u0307\\u0323' } \`;`, + output: '` A\\u0307\\u0323 `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'👨‍👩‍👧‍👦'} `;", + output: '` 👨‍👩‍👧‍👦 `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\ud83d\\udc68'} `;", + output: '` \\ud83d\\udc68 `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, +]; + +describe('fixer should not change runtime value', () => { + for (const { code, output } of invalidCases) { + if (!output) { + continue; + } + + test(code, () => { + expect(eval(code)).toEqual( + eval(Array.isArray(output) ? output.at(-1)! : output), + ); + }); + } +}); + ruleTester.run('no-unnecessary-template-expression', rule, { valid: [ "const string = 'a';", @@ -138,210 +1021,27 @@ ruleTester.run('no-unnecessary-template-expression', rule, { ], invalid: [ + ...invalidCases, { - code: '`${1}`;', - output: '`1`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 5, - }, - ], - }, - { - code: '`${1n}`;', - output: '`1`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 6, - }, - ], - }, - { - code: '`${/a/}`;', - output: '`/a/`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 7, - }, - ], - }, - - { - code: noFormat`\`\${ 1 }\`;`, - output: '`1`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: noFormat`\`\${ 'a' }\`;`, - output: `'a';`, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: noFormat`\`\${ "a" }\`;`, - output: `"a";`, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: noFormat`\`\${ 'a' + 'b' }\`;`, - output: `'a' + 'b';`, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`${true}`;', - output: '`true`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 8, - }, - ], - }, - - { - code: noFormat`\`\${ true }\`;`, - output: '`true`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`${null}`;', - output: '`null`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 8, - }, - ], - }, - - { - code: noFormat`\`\${ null }\`;`, - output: '`null`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`${undefined}`;', - output: '`undefined`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 13, - }, - ], - }, - - { - code: noFormat`\`\${ undefined }\`;`, - output: '`undefined`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`${Infinity}`;', - output: '`Infinity`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 12, - }, - ], - }, - - { - code: '`${NaN}`;', - output: '`NaN`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 7, - }, - ], - }, - - { - code: "`${'a'} ${'b'}`;", - output: '`a b`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 7, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 11, - endColumn: 14, - }, - ], - }, - - { - code: noFormat`\`\${ 'a' } \${ 'b' }\`;`, - output: '`a b`;', + code: ` + function func(arg: T) { + \`\${arg}\`; + } + `, + output: ` + function func(arg: T) { + arg; + } + `, errors: [ { messageId: 'noUnnecessaryTemplateExpression', - }, - { - messageId: 'noUnnecessaryTemplateExpression', + line: 3, + column: 14, + endColumn: 17, }, ], }, - { code: ` declare const b: 'b'; @@ -360,29 +1060,6 @@ ruleTester.run('no-unnecessary-template-expression', rule, { }, ], }, - - { - code: "`use${'less'}`;", - output: '`useless`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - }, - ], - }, - - { - code: '`use${`less`}`;', - output: '`useless`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - }, - ], - }, - { code: ` declare const nested: string, interpolation: string; @@ -398,103 +1075,21 @@ declare const nested: string, interpolation: string; }, ], }, - - { - code: noFormat` -\`u\${ - // hopefully this comment is not needed. - 'se' - -}\${ - \`le\${ \`ss\` }\` -}\`; - `, - output: [ - ` -\`use\${ - \`less\` -}\`; - `, - ` -\`useless\`; - `, - ], - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 4, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 7, - column: 3, - endLine: 7, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 7, - column: 10, - endLine: 7, - }, - ], - }, { code: noFormat` -\`use\${ - \`less\` -}\`; + declare const string: 'a'; + \`\${ string }\`; `, output: ` -\`useless\`; + declare const string: 'a'; + string; `, errors: [ { messageId: 'noUnnecessaryTemplateExpression', - line: 3, - column: 3, - endColumn: 9, }, ], }, - - { - code: "`${'1 + 1 ='} ${2}`;", - output: '`1 + 1 = 2`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 13, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 17, - endColumn: 18, - }, - ], - }, - - { - code: "`${'a'} ${true}`;", - output: '`a true`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 7, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 11, - endColumn: 15, - }, - ], - }, - { code: ` declare const string: 'a'; @@ -513,36 +1108,6 @@ declare const nested: string, interpolation: string; }, ], }, - - { - code: noFormat` - declare const string: 'a'; - \`\${ string }\`; - `, - output: ` - declare const string: 'a'; - string; - `, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`${String(Symbol.for('test'))}`;", - output: "String(Symbol.for('test'));", - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 30, - }, - ], - }, - { code: ` declare const intersection: string & { _brand: 'test-brand' }; @@ -561,86 +1126,5 @@ declare const nested: string, interpolation: string; }, ], }, - - { - code: ` - function func(arg: T) { - \`\${arg}\`; - } - `, - output: ` - function func(arg: T) { - arg; - } - `, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 3, - column: 14, - endColumn: 17, - }, - ], - }, - - { - code: "`${'`'}`;", - output: "'`';", - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`back${'`'}tick`;", - output: '`back\\`tick`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`dollar${'${`this is test`}'}sign`;", - output: '`dollar\\${\\`this is test\\`}sign`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`complex${\'`${"`${test}`"}`\'}case`;', - output: '`complex\\`\\${"\\`\\${test}\\`"}\\`case`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`some ${'\\\\${test}'} string`;", - output: '`some \\\\\\${test} string`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`some ${'\\\\`'} string`;", - output: '`some \\\\\\` string`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts index 6f964842026c..30af0b8e5127 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts @@ -127,6 +127,7 @@ function foo(): Set { return [] as any[]; } `, + 'const foo: (() => void) | undefined = () => 1;', ], invalid: [ { diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts index 223b9405d4e4..45b65d8bac8c 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts @@ -2536,7 +2536,10 @@ try { `, options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], errors: [ - definedError('err', '. Allowed unused args must match /^ignore/u'), + definedError( + 'err', + '. Allowed unused caught errors must match /^ignore/u', + ), ], }, { @@ -2566,7 +2569,10 @@ try { `, options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], errors: [ - definedError('err', '. Allowed unused args must match /^ignore/u'), + definedError( + 'err', + '. Allowed unused caught errors must match /^ignore/u', + ), ], }, @@ -2580,8 +2586,14 @@ try { `, options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], errors: [ - definedError('error', '. Allowed unused args must match /^ignore/u'), - definedError('err', '. Allowed unused args must match /^ignore/u'), + definedError( + 'error', + '. Allowed unused caught errors must match /^ignore/u', + ), + definedError( + 'err', + '. Allowed unused caught errors must match /^ignore/u', + ), ], }, @@ -3330,7 +3342,101 @@ try { reportUsedIgnorePattern: true, }, ], - errors: [usedIgnoredError('_err', '. Used args must not match /^_/u')], + errors: [ + usedIgnoredError('_err', '. Used caught errors must not match /^_/u'), + ], + }, + { + code: ` +try { +} catch (_) { + _ = 'foo'; +} + `, + options: [{ caughtErrorsIgnorePattern: 'foo' }], + errors: [ + assignedError('_', '. Allowed unused caught errors must match /foo/u'), + ], + }, + { + code: ` +try { +} catch (_) { + _ = 'foo'; +} + `, + options: [ + { + caughtErrorsIgnorePattern: 'ignored', + varsIgnorePattern: '_', + }, + ], + errors: [ + assignedError( + '_', + '. Allowed unused caught errors must match /ignored/u', + ), + ], + }, + { + code: ` +try { +} catch ({ message, errors: [firstError] }) {} + `, + options: [{ caughtErrorsIgnorePattern: 'foo' }], + errors: [ + { + ...definedError( + 'message', + '. Allowed unused caught errors must match /foo/u', + ), + column: 12, + endColumn: 19, + }, + { + ...definedError( + 'firstError', + '. Allowed unused caught errors must match /foo/u', + ), + column: 30, + endColumn: 40, + }, + ], + }, + { + code: ` +try { +} catch ({ stack: $ }) { + $ = 'Something broke: ' + $; +} + `, + options: [{ caughtErrorsIgnorePattern: '\\w' }], + errors: [ + { + ...assignedError( + '$', + '. Allowed unused caught errors must match /\\w/u', + ), + column: 3, + endColumn: 4, + }, + ], + }, + { + code: ` +_ => { + _ = _ + 1; +}; + `, + options: [ + { + argsIgnorePattern: 'ignored', + varsIgnorePattern: '_', + }, + ], + errors: [ + assignedError('_', '. Allowed unused args must match /ignored/u'), + ], }, ], }); diff --git a/packages/eslint-plugin/tests/rules/return-await.test.ts b/packages/eslint-plugin/tests/rules/return-await.test.ts index bc527b805e57..98175766e733 100644 --- a/packages/eslint-plugin/tests/rules/return-await.test.ts +++ b/packages/eslint-plugin/tests/rules/return-await.test.ts @@ -1,4 +1,5 @@ import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; +import type { InvalidTestCase } from '@typescript-eslint/utils/ts-eslint'; import rule from '../../src/rules/return-await'; import { getFixturesRootDir } from '../RuleTester'; @@ -56,6 +57,25 @@ ruleTester.run('return-await', rule, { } } `, + { + code: ` + async function test() { + if (Math.random() < 0.33) { + return await Promise.resolve(1); + } else if (Math.random() < 0.5) { + return Promise.resolve(2); + } + + try { + } catch (e) { + return await Promise.resolve(3); + } finally { + console.log('cleanup'); + } + } + `, + options: ['error-handling-correctness-only'], + }, ` async function test() { try { @@ -548,8 +568,11 @@ async function test() { }, ], }, - { - code: ` + + ...['error-handling-correctness-only', 'always', 'in-try-catch'].map( + option => + ({ + code: ` async function test() { try { return Promise.resolve(1); @@ -560,15 +583,15 @@ async function test() { } } `, - output: null, - errors: [ - { - line: 4, - messageId: 'requiredPromiseAwait', - suggestions: [ + output: null, + errors: [ { - messageId: 'requiredPromiseAwaitSuggestion', - output: ` + line: 4, + messageId: 'requiredPromiseAwait', + suggestions: [ + { + messageId: 'requiredPromiseAwaitSuggestion', + output: ` async function test() { try { return await Promise.resolve(1); @@ -579,16 +602,16 @@ async function test() { } } `, + }, + ], }, - ], - }, - { - line: 6, - messageId: 'requiredPromiseAwait', - suggestions: [ { - messageId: 'requiredPromiseAwaitSuggestion', - output: ` + line: 6, + messageId: 'requiredPromiseAwait', + suggestions: [ + { + messageId: 'requiredPromiseAwaitSuggestion', + output: ` async function test() { try { return Promise.resolve(1); @@ -599,11 +622,17 @@ async function test() { } } `, + }, + ], }, ], - }, - ], - }, + options: [option], + }) satisfies InvalidTestCase< + 'requiredPromiseAwait' | 'requiredPromiseAwaitSuggestion', + [string] + >, + ), + { code: ` async function test() { @@ -663,63 +692,6 @@ async function test() { }, ], }, - { - options: ['in-try-catch'], - code: ` - async function test() { - try { - return Promise.resolve(1); - } catch (e) { - return Promise.resolve(2); - } finally { - console.log('cleanup'); - } - } - `, - output: null, - errors: [ - { - line: 4, - messageId: 'requiredPromiseAwait', - suggestions: [ - { - messageId: 'requiredPromiseAwaitSuggestion', - output: ` - async function test() { - try { - return await Promise.resolve(1); - } catch (e) { - return Promise.resolve(2); - } finally { - console.log('cleanup'); - } - } - `, - }, - ], - }, - { - line: 6, - messageId: 'requiredPromiseAwait', - suggestions: [ - { - messageId: 'requiredPromiseAwaitSuggestion', - output: ` - async function test() { - try { - return Promise.resolve(1); - } catch (e) { - return await Promise.resolve(2); - } finally { - console.log('cleanup'); - } - } - `, - }, - ], - }, - ], - }, { options: ['in-try-catch'], code: ` @@ -853,63 +825,6 @@ async function test() { }, ], }, - { - options: ['always'], - code: ` - async function test() { - try { - return Promise.resolve(1); - } catch (e) { - return Promise.resolve(2); - } finally { - console.log('cleanup'); - } - } - `, - output: null, - errors: [ - { - line: 4, - messageId: 'requiredPromiseAwait', - suggestions: [ - { - messageId: 'requiredPromiseAwaitSuggestion', - output: ` - async function test() { - try { - return await Promise.resolve(1); - } catch (e) { - return Promise.resolve(2); - } finally { - console.log('cleanup'); - } - } - `, - }, - ], - }, - { - line: 6, - messageId: 'requiredPromiseAwait', - suggestions: [ - { - messageId: 'requiredPromiseAwaitSuggestion', - output: ` - async function test() { - try { - return Promise.resolve(1); - } catch (e) { - return await Promise.resolve(2); - } finally { - console.log('cleanup'); - } - } - `, - }, - ], - }, - ], - }, { options: ['always'], code: ` diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index dd1bb0a92a38..640c9a212cd1 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -410,6 +410,139 @@ if (y) { }, ], }, + ` +declare function assert(a: number, b: unknown): asserts a; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, nullableString); + `, + ` +declare function assert(a: boolean, b: unknown): asserts b is string; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, nullableString); + `, + // Intentional TS error - cannot assert a parameter in a binding pattern. + ` +declare function assert(a: boolean, b: unknown): asserts b; +declare function assert(a: boolean, { b }: { b: unknown }): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, nullableString); + `, + ` +declare function assert(a: number, b: unknown): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(nullableString, boo); + `, + ` +declare function assert(a: number, b: unknown): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(...nullableString, nullableString); + `, + ` +declare function assert( + this: object, + a: number, + b?: unknown, + c?: unknown, +): asserts c; +declare const nullableString: string | null; +declare const foo: number; +const o: { assert: typeof assert } = { + assert, +}; +o.assert(foo, nullableString); + `, + { + code: ` +declare function assert(x: unknown): x is string; +declare const nullableString: string | null; +assert(nullableString); + `, + }, + { + code: ` +class ThisAsserter { + assertThis(this: unknown, arg2: unknown): asserts this {} +} + +declare const lol: string | number | unknown | null; + +const thisAsserter: ThisAsserter = new ThisAsserter(); +thisAsserter.assertThis(lol); + `, + }, + { + code: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; +function assert(...args: any[]): void; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, nullableString); + `, + }, + // Intentional TS error - A rest parameter must be last in a parameter list. + // This is just to test that we don't crash or falsely report. + ` +declare function assert(...a: boolean[], b: unknown): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, nullableString); + `, + // Intentional TS error - A type predicate cannot reference a rest parameter. + // This is just to test that we don't crash or falsely report. + ` +declare function assert(a: boolean, ...b: unknown[]): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, nullableString); + `, + // Intentional TS error - An assertion function must have a parameter to assert. + // This is just to test that we don't crash or falsely report. + ` +declare function assert(): asserts x; +declare const nullableString: string | null; +assert(nullableString); + `, + ` +function assert(one: unknown): asserts one; +function assert(one: unknown, two: unknown): asserts two; +function assert(...args: unknown[]) { + throw new Error('not implemented'); +} +declare const nullableString: string | null; +assert(nullableString); +assert('one', nullableString); + `, + // Intentional use of `any` to test a function call with no call signatures. + ` +declare const assert: any; +declare const nullableString: string | null; +assert(nullableString); + `, + // Coverage for absent "test expression". + // Ensure that no crash or false positive occurs + ` + for (let x = 0; ; x++) { + break; + } + `, ], invalid: [ @@ -2144,5 +2277,485 @@ if (x) { }, ], }, + { + code: ` +declare function assert(x: unknown): asserts x; +declare const nullableString: string | null; +assert(nullableString); + `, + output: null, + errors: [ + { + messageId: 'conditionErrorNullableString', + line: 4, + column: 8, + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +declare function assert(x: unknown): asserts x; +declare const nullableString: string | null; +assert(nullableString != null); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +declare function assert(x: unknown): asserts x; +declare const nullableString: string | null; +assert(nullableString ?? ""); + `, + }, + { + messageId: 'conditionFixCastBoolean', + output: ` +declare function assert(x: unknown): asserts x; +declare const nullableString: string | null; +assert(Boolean(nullableString)); + `, + }, + ], + }, + ], + }, + { + code: ` +declare function assert(a: number, b: unknown): asserts b; +declare const nullableString: string | null; +assert(foo, nullableString); + `, + output: null, + errors: [ + { + messageId: 'conditionErrorNullableString', + line: 4, + column: 13, + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +declare function assert(a: number, b: unknown): asserts b; +declare const nullableString: string | null; +assert(foo, nullableString != null); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +declare function assert(a: number, b: unknown): asserts b; +declare const nullableString: string | null; +assert(foo, nullableString ?? ""); + `, + }, + { + messageId: 'conditionFixCastBoolean', + output: ` +declare function assert(a: number, b: unknown): asserts b; +declare const nullableString: string | null; +assert(foo, Boolean(nullableString)); + `, + }, + ], + }, + ], + }, + { + code: ` +declare function assert(a: number, b: unknown): asserts b; +declare function assert(one: number, two: unknown): asserts two; +declare const nullableString: string | null; +assert(foo, nullableString); + `, + output: null, + errors: [ + { + messageId: 'conditionErrorNullableString', + line: 5, + column: 13, + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +declare function assert(a: number, b: unknown): asserts b; +declare function assert(one: number, two: unknown): asserts two; +declare const nullableString: string | null; +assert(foo, nullableString != null); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +declare function assert(a: number, b: unknown): asserts b; +declare function assert(one: number, two: unknown): asserts two; +declare const nullableString: string | null; +assert(foo, nullableString ?? ""); + `, + }, + { + messageId: 'conditionFixCastBoolean', + output: ` +declare function assert(a: number, b: unknown): asserts b; +declare function assert(one: number, two: unknown): asserts two; +declare const nullableString: string | null; +assert(foo, Boolean(nullableString)); + `, + }, + ], + }, + ], + }, + { + code: ` +declare function assert(this: object, a: number, b: unknown): asserts b; +declare const nullableString: string | null; +assert(foo, nullableString); + `, + output: null, + errors: [ + { + messageId: 'conditionErrorNullableString', + line: 4, + column: 13, + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +declare function assert(this: object, a: number, b: unknown): asserts b; +declare const nullableString: string | null; +assert(foo, nullableString != null); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +declare function assert(this: object, a: number, b: unknown): asserts b; +declare const nullableString: string | null; +assert(foo, nullableString ?? ""); + `, + }, + { + messageId: 'conditionFixCastBoolean', + output: ` +declare function assert(this: object, a: number, b: unknown): asserts b; +declare const nullableString: string | null; +assert(foo, Boolean(nullableString)); + `, + }, + ], + }, + ], + }, + { + code: ` +function asserts1(x: string | number | undefined): asserts x {} +function asserts2(x: string | number | undefined): asserts x {} + +const maybeString = Math.random() ? 'string'.slice() : undefined; + +const someAssert: typeof asserts1 | typeof asserts2 = + Math.random() > 0.5 ? asserts1 : asserts2; + +someAssert(maybeString); + `, + output: null, + errors: [ + { + messageId: 'conditionErrorNullableString', + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +function asserts1(x: string | number | undefined): asserts x {} +function asserts2(x: string | number | undefined): asserts x {} + +const maybeString = Math.random() ? 'string'.slice() : undefined; + +const someAssert: typeof asserts1 | typeof asserts2 = + Math.random() > 0.5 ? asserts1 : asserts2; + +someAssert(maybeString != null); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +function asserts1(x: string | number | undefined): asserts x {} +function asserts2(x: string | number | undefined): asserts x {} + +const maybeString = Math.random() ? 'string'.slice() : undefined; + +const someAssert: typeof asserts1 | typeof asserts2 = + Math.random() > 0.5 ? asserts1 : asserts2; + +someAssert(maybeString ?? ""); + `, + }, + { + messageId: 'conditionFixCastBoolean', + output: ` +function asserts1(x: string | number | undefined): asserts x {} +function asserts2(x: string | number | undefined): asserts x {} + +const maybeString = Math.random() ? 'string'.slice() : undefined; + +const someAssert: typeof asserts1 | typeof asserts2 = + Math.random() > 0.5 ? asserts1 : asserts2; + +someAssert(Boolean(maybeString)); + `, + }, + ], + }, + ], + }, + { + // The implementation signature doesn't count towards the call signatures + code: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, nullableString); + `, + output: null, + errors: [ + { + messageId: 'conditionErrorNullableString', + line: 18, + column: 18, + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, nullableString != null); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, nullableString ?? ""); + `, + }, + { + messageId: 'conditionFixCastBoolean', + output: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, Boolean(nullableString)); + `, + }, + ], + }, + ], + }, + { + // The implementation signature doesn't count towards the call signatures + code: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; +function assert(a: any, two: unknown, ...rest: any[]): asserts two; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, nullableString, 'more', 'args', 'afterwards'); + `, + output: null, + errors: [ + { + messageId: 'conditionErrorNullableString', + line: 19, + column: 18, + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; +function assert(a: any, two: unknown, ...rest: any[]): asserts two; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, nullableString != null, 'more', 'args', 'afterwards'); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; +function assert(a: any, two: unknown, ...rest: any[]): asserts two; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, nullableString ?? "", 'more', 'args', 'afterwards'); + `, + }, + { + messageId: 'conditionFixCastBoolean', + output: ` +function assert(this: object, a: number, b: unknown): asserts b; +function assert(a: bigint, b: unknown): asserts b; +function assert(this: object, a: string, two: string): asserts two; +function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, +): asserts assertee; +function assert(a: any, two: unknown, ...rest: any[]): asserts two; + +function assert(...args: any[]) { + throw new Error('lol'); +} + +declare const nullableString: string | null; +assert(3 as any, Boolean(nullableString), 'more', 'args', 'afterwards'); + `, + }, + ], + }, + ], + }, + { + code: ` +declare function assert(a: boolean, b: unknown): asserts b; +declare function assert({ a }: { a: boolean }, b: unknown): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, nullableString); + `, + output: null, + errors: [ + { + line: 6, + messageId: 'conditionErrorNullableString', + suggestions: [ + { + messageId: 'conditionFixCompareNullish', + output: ` +declare function assert(a: boolean, b: unknown): asserts b; +declare function assert({ a }: { a: boolean }, b: unknown): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, nullableString != null); + `, + }, + { + messageId: 'conditionFixDefaultEmptyString', + output: ` +declare function assert(a: boolean, b: unknown): asserts b; +declare function assert({ a }: { a: boolean }, b: unknown): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, nullableString ?? ""); + `, + }, + + { + messageId: 'conditionFixCastBoolean', + output: ` +declare function assert(a: boolean, b: unknown): asserts b; +declare function assert({ a }: { a: boolean }, b: unknown): asserts b; +declare const nullableString: string | null; +declare const boo: boolean; +assert(boo, Boolean(nullableString)); + `, + }, + ], + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/schema-snapshots/return-await.shot b/packages/eslint-plugin/tests/schema-snapshots/return-await.shot index 5d79a331bf33..7096730066db 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/return-await.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/return-await.shot @@ -6,7 +6,12 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos [ { - "enum": ["always", "in-try-catch", "never"], + "enum": [ + "always", + "error-handling-correctness-only", + "in-try-catch", + "never" + ], "type": "string" } ] @@ -14,6 +19,8 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos # TYPES: -type Options = ['always' | 'in-try-catch' | 'never']; +type Options = [ + 'always' | 'error-handling-correctness-only' | 'in-try-catch' | 'never', +]; " `; diff --git a/packages/eslint-plugin/tests/schemas.test.ts b/packages/eslint-plugin/tests/schemas.test.ts index b1670bc6b9f1..acb93bb3f4ea 100644 --- a/packages/eslint-plugin/tests/schemas.test.ts +++ b/packages/eslint-plugin/tests/schemas.test.ts @@ -169,7 +169,7 @@ describe('Rules should only define valid keys on schemas', () => { // definition keys and property keys should not be validated, only the values return Object.values(value as object); } - if (parseInt(key).toString() === key) { + if (`${Number(key)}` === key) { // hack to detect arrays as JSON.stringify will traverse them and stringify the number return value; } diff --git a/packages/integration-tests/CHANGELOG.md b/packages/integration-tests/CHANGELOG.md index af5077b1c119..f46642583f69 100644 --- a/packages/integration-tests/CHANGELOG.md +++ b/packages/integration-tests/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for integration-tests to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for integration-tests to align it with other projects, there were no code changes. diff --git a/packages/integration-tests/fixtures/flat-config-types/package.json b/packages/integration-tests/fixtures/flat-config-types/package.json index 5f3e28a91d56..4aaf3629d59b 100644 --- a/packages/integration-tests/fixtures/flat-config-types/package.json +++ b/packages/integration-tests/fixtures/flat-config-types/package.json @@ -1,11 +1,14 @@ { "type": "module", + "scripts": { + "// NOTE: @types/eslint has to be pinned until @stylistic/eslint-plugin latest works with latest": "" + }, "devDependencies": { "@types/eslint__eslintrc": "latest", "@eslint/eslintrc": "latest", "@types/eslint__js": "latest", "@eslint/js": "latest", - "@types/eslint": "latest", + "@types/eslint": "^8.56.10", "eslint": "latest", "@stylistic/eslint-plugin": "latest", "eslint-plugin-deprecation": "latest", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 4ab50e0010e2..2997e45f6543 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/integration-tests", - "version": "7.16.1", + "version": "7.17.0", "private": true, "scripts": { "format": "prettier --write \"./**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md,css}\" --ignore-path ../../.prettierignore", diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index a9a5917aabaa..3582d87cce61 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for parser to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for parser to align it with other projects, there were no code changes. diff --git a/packages/parser/package.json b/packages/parser/package.json index 61bf12241d86..b9ccc466c328 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "7.16.1", + "version": "7.17.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "files": [ "dist", @@ -52,10 +52,10 @@ "eslint": "^8.57.0 || ^9.0.0" }, "dependencies": { - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4" }, "devDependencies": { diff --git a/packages/parser/tests/test-utils/test-utils.ts b/packages/parser/tests/test-utils/test-utils.ts index 4d86b0d17cf3..a78f11dd3296 100644 --- a/packages/parser/tests/test-utils/test-utils.ts +++ b/packages/parser/tests/test-utils/test-utils.ts @@ -10,7 +10,7 @@ const defaultConfig = { tokens: true, comment: true, errorOnUnknownASTType: true, - sourceType: 'module', + sourceType: 'module' as const, }; /** @@ -40,7 +40,7 @@ export function createSnapshotTestBlock( code: string, config: ParserOptions = {}, ): () => void { - config = Object.assign({}, defaultConfig, config); + config = { ...defaultConfig, ...config }; /** * @returns the AST object @@ -72,7 +72,7 @@ export function createSnapshotTestBlock( * @param config The configuration object for the parser */ export function testServices(code: string, config: ParserOptions = {}): void { - config = Object.assign({}, defaultConfig, config); + config = { ...defaultConfig, ...config }; const services = parser.parseForESLint(code, config).services; expect(services).toBeDefined(); diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 90bcc56f10de..d195f77ada6b 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for repo-tools to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for repo-tools to align it with other projects, there were no code changes. diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index dc0a57279d44..7997f33fd687 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/repo-tools", - "version": "7.16.1", + "version": "7.17.0", "private": true, "//": "NOTE: intentionally no build step in this package", "scripts": { @@ -18,11 +18,11 @@ "devDependencies": { "@jest/types": "29.6.3", "@nx/devkit": "*", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "cross-fetch": "*", "execa": "*", "prettier": "^3.2.5", diff --git a/packages/rule-schema-to-typescript-types/CHANGELOG.md b/packages/rule-schema-to-typescript-types/CHANGELOG.md index 8e4d5c760977..16b084522546 100644 --- a/packages/rule-schema-to-typescript-types/CHANGELOG.md +++ b/packages/rule-schema-to-typescript-types/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for rule-schema-to-typescript-types to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for rule-schema-to-typescript-types to align it with other projects, there were no code changes. diff --git a/packages/rule-schema-to-typescript-types/package.json b/packages/rule-schema-to-typescript-types/package.json index 7c3302c77ea4..d44eee2b79c4 100644 --- a/packages/rule-schema-to-typescript-types/package.json +++ b/packages/rule-schema-to-typescript-types/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/rule-schema-to-typescript-types", - "version": "7.16.1", + "version": "7.17.0", "private": true, "type": "commonjs", "exports": { @@ -34,8 +34,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@typescript-eslint/type-utils": "7.16.1", - "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "natural-compare": "^1.4.0", "prettier": "^3.2.5" }, diff --git a/packages/rule-schema-to-typescript-types/src/index.ts b/packages/rule-schema-to-typescript-types/src/index.ts index 32da7e7efd2e..f4175fba634c 100644 --- a/packages/rule-schema-to-typescript-types/src/index.ts +++ b/packages/rule-schema-to-typescript-types/src/index.ts @@ -50,7 +50,7 @@ export async function compile( return await prettier.format(unformattedCode, await prettierConfig); } catch (e) { if (e instanceof Error) { - e.message = e.message + `\n\nUnformatted Code:\n${unformattedCode}`; + e.message += `\n\nUnformatted Code:\n${unformattedCode}`; } throw e; } diff --git a/packages/rule-tester/CHANGELOG.md b/packages/rule-tester/CHANGELOG.md index 6a47c2a596e1..43bba025aac9 100644 --- a/packages/rule-tester/CHANGELOG.md +++ b/packages/rule-tester/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for rule-tester to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for rule-tester to align it with other projects, there were no code changes. diff --git a/packages/rule-tester/package.json b/packages/rule-tester/package.json index 53e6dfd0553c..3d164b75f362 100644 --- a/packages/rule-tester/package.json +++ b/packages/rule-tester/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/rule-tester", - "version": "7.16.1", + "version": "7.17.0", "description": "Tooling to test ESLint rules", "files": [ "dist", @@ -48,8 +48,8 @@ }, "//": "NOTE - AJV is out-of-date, but it's intentionally synced with ESLint - https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/package.json#L70", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", @@ -63,7 +63,7 @@ "@jest/types": "29.6.3", "@types/json-stable-stringify-without-jsonify": "^1.0.2", "@types/lodash.merge": "4.6.9", - "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/parser": "7.17.0", "chai": "^4.4.1", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.1", diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index ec7dfe0a260e..85ed874bafdd 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -411,19 +411,17 @@ export class RuleTester extends TestFramework { emitLegacyRuleAPIWarning(ruleName); } - this.#linter.defineRule( - ruleName, - Object.assign({}, rule, { - // Create a wrapper rule that freezes the `context` properties. - create(context: RuleContext) { - freezeDeeply(context.options); - freezeDeeply(context.settings); - freezeDeeply(context.parserOptions); - - return (typeof rule === 'function' ? rule : rule.create)(context); - }, - }), - ); + this.#linter.defineRule(ruleName, { + ...rule, + // Create a wrapper rule that freezes the `context` properties. + create(context: RuleContext) { + freezeDeeply(context.options); + freezeDeeply(context.settings); + freezeDeeply(context.parserOptions); + + return (typeof rule === 'function' ? rule : rule.create)(context); + }, + }); this.#linter.defineRules(this.#rules); @@ -1208,14 +1206,12 @@ export class RuleTester extends TestFramework { 'Outputs do not match.', ); } - } else { - if (result.outputs.length) { - assert.strictEqual( - result.outputs[0], - item.code, - "The rule fixed the code. Please add 'output' property.", - ); - } + } else if (result.outputs.length) { + assert.strictEqual( + result.outputs[0], + item.code, + "The rule fixed the code. Please add 'output' property.", + ); } assertASTDidntChange(result.beforeAST, result.afterAST); diff --git a/packages/rule-tester/tests/RuleTester.test.ts b/packages/rule-tester/tests/RuleTester.test.ts index 2550d0239ab6..fe765a5fd024 100644 --- a/packages/rule-tester/tests/RuleTester.test.ts +++ b/packages/rule-tester/tests/RuleTester.test.ts @@ -370,7 +370,6 @@ describe('RuleTester', () => { { code: 'const x = 2;' }, { code: 'const x = 3;', - // eslint-disable-next-line eslint-plugin/no-only-tests -- intentional only for test purposes only: true, }, { @@ -406,7 +405,6 @@ describe('RuleTester', () => { { code: 'const x = 3;', errors: [{ messageId: 'error' }], - // eslint-disable-next-line eslint-plugin/no-only-tests -- intentional only for test purposes only: true, }, ], diff --git a/packages/scope-manager/CHANGELOG.md b/packages/scope-manager/CHANGELOG.md index 75d983775244..9dd4733ad0d2 100644 --- a/packages/scope-manager/CHANGELOG.md +++ b/packages/scope-manager/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for scope-manager to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for scope-manager to align it with other projects, there were no code changes. diff --git a/packages/scope-manager/package.json b/packages/scope-manager/package.json index 9641ea9a355f..e0bf76ce16e6 100644 --- a/packages/scope-manager/package.json +++ b/packages/scope-manager/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/scope-manager", - "version": "7.16.1", + "version": "7.17.0", "description": "TypeScript scope analyser for ESLint", "files": [ "dist", @@ -46,13 +46,13 @@ "typecheck": "npx nx typecheck" }, "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1" + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" }, "devDependencies": { "@jest/types": "29.6.3", "@types/glob": "*", - "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/typescript-estree": "7.17.0", "glob": "*", "jest-specific-snapshot": "*", "make-dir": "*", diff --git a/packages/scope-manager/src/referencer/Referencer.ts b/packages/scope-manager/src/referencer/Referencer.ts index 575964c2b3e0..1006edc1f0cc 100644 --- a/packages/scope-manager/src/referencer/Referencer.ts +++ b/packages/scope-manager/src/referencer/Referencer.ts @@ -531,10 +531,8 @@ class Referencer extends Visitor { protected JSXMemberExpression(node: TSESTree.JSXMemberExpression): void { if (node.object.type !== AST_NODE_TYPES.JSXIdentifier) { this.visit(node.object); - } else { - if (node.object.name !== 'this') { - this.visit(node.object); - } + } else if (node.object.name !== 'this') { + this.visit(node.object); } // we don't ever reference the property as it's always going to be a property on the thing } diff --git a/packages/type-utils/CHANGELOG.md b/packages/type-utils/CHANGELOG.md index 4e3128ca4d5a..6a813a65ff07 100644 --- a/packages/type-utils/CHANGELOG.md +++ b/packages/type-utils/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for type-utils to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for type-utils to align it with other projects, there were no code changes. diff --git a/packages/type-utils/package.json b/packages/type-utils/package.json index 5739345fb716..897b89f11569 100644 --- a/packages/type-utils/package.json +++ b/packages/type-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/type-utils", - "version": "7.16.1", + "version": "7.17.0", "description": "Type utilities for working with TypeScript + ESLint together", "files": [ "dist", @@ -46,14 +46,14 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "devDependencies": { "@jest/types": "29.6.3", - "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/parser": "7.17.0", "ajv": "^6.12.6", "downlevel-dts": "*", "jest": "29.7.0", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 1a00f130b667..857b5190464e 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for types to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for types to align it with other projects, there were no code changes. diff --git a/packages/types/package.json b/packages/types/package.json index 444b39fda21f..5767d7b1d864 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/types", - "version": "7.16.1", + "version": "7.17.0", "description": "Types for the TypeScript-ESTree AST spec", "files": [ "dist", diff --git a/packages/typescript-eslint/CHANGELOG.md b/packages/typescript-eslint/CHANGELOG.md index 63ed46fb197a..cac255e52396 100644 --- a/packages/typescript-eslint/CHANGELOG.md +++ b/packages/typescript-eslint/CHANGELOG.md @@ -1,3 +1,20 @@ +## 7.17.0 (2024-07-22) + + +### 🚀 Features + +- **eslint-plugin:** backport no-unsafe-function type, no-wrapper-object-types from v8 to v7 + + +### ❤️ Thank You + +- Armano +- Josh Goldberg ✨ +- Kirk Waiblinger +- StyleShit + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for typescript-eslint to align it with other projects, there were no code changes. diff --git a/packages/typescript-eslint/package.json b/packages/typescript-eslint/package.json index d134ece49a0d..754e1114e6e5 100644 --- a/packages/typescript-eslint/package.json +++ b/packages/typescript-eslint/package.json @@ -1,6 +1,6 @@ { "name": "typescript-eslint", - "version": "7.16.1", + "version": "7.17.0", "description": "Tooling which enables you to use TypeScript with ESLint", "files": [ "dist", @@ -52,9 +52,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", - "@typescript-eslint/utils": "7.16.1" + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", + "@typescript-eslint/utils": "7.17.0" }, "devDependencies": { "@jest/types": "29.6.3", diff --git a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts index 415dd3eb342b..4d902ef37e52 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts @@ -73,6 +73,11 @@ export default ( allowNever: false, }, ], + 'no-return-await': 'off', + '@typescript-eslint/return-await': [ + 'error', + 'error-handling-correctness-only', + ], '@typescript-eslint/unbound-method': 'error', '@typescript-eslint/use-unknown-in-catch-callback-variable': 'error', }, diff --git a/packages/typescript-eslint/src/configs/strict-type-checked.ts b/packages/typescript-eslint/src/configs/strict-type-checked.ts index fb53665756e3..c41cef185b4d 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts @@ -106,6 +106,11 @@ export default ( allowNever: false, }, ], + 'no-return-await': 'off', + '@typescript-eslint/return-await': [ + 'error', + 'error-handling-correctness-only', + ], '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/unbound-method': 'error', '@typescript-eslint/unified-signatures': 'error', diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index 37bbf36421ed..944370fe10a1 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -1,3 +1,22 @@ +## 7.17.0 (2024-07-22) + + +### 🩹 Fixes + +- **typescript-estree:** don't infer single-run when --fix is in proces.argv + +- **typescript-estree:** disable single-run inference with extraFileExtensions + + +### ❤️ Thank You + +- Armano +- Josh Goldberg ✨ +- Kirk Waiblinger +- StyleShit + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for typescript-estree to align it with other projects, there were no code changes. diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 3ed619aa0656..809d69a60be7 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "7.16.1", + "version": "7.17.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "files": [ "dist", @@ -54,8 +54,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index 63653a4e1b82..308bc1ac79f5 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -977,13 +977,11 @@ export class Converter { 'Generators are not allowed in an ambient context.', ); } - } else { - if (!node.body && isGenerator) { - this.#throwError( - node, - 'A function signature cannot be declared as a generator.', - ); - } + } else if (!node.body && isGenerator) { + this.#throwError( + node, + 'A function signature cannot be declared as a generator.', + ); } const result = this.createNode< diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index af4e6378972e..4a9fc7824115 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -52,10 +52,10 @@ function createProjectProgram( ); const describedPrograms = relativeProjects.length === 1 - ? relativeProjects[0] + ? ` ${relativeProjects[0]}` : `\n${relativeProjects.map(project => `- ${project}`).join('\n')}`; const errorLines = [ - `ESLint was configured to run on \`${describedFilePath}\` using \`parserOptions.project\`: ${describedPrograms}`, + `ESLint was configured to run on \`${describedFilePath}\` using \`parserOptions.project\`:${describedPrograms}`, ]; let hasMatchedAnError = false; diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 63f497607c82..dc51c7dfaff2 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -115,9 +115,17 @@ export function createProjectService( session: undefined, jsDocParsingMode, }); + + service.setHostConfiguration({ + preferences: { + includePackageJsonAutoImports: 'off', + }, + }); + if (options.defaultProject) { log('Enabling default project: %s', options.defaultProject); let defaultProjectError: string | undefined; + try { const configFile = getParsedConfigFile( tsserver, diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index 951678f90e65..070a014daf93 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -2,48 +2,52 @@ import debug from 'debug'; import * as path from 'path'; import * as ts from 'typescript'; +import type { ParseSettings } from '../parseSettings'; import { getParsedConfigFile } from './getParsedConfigFile'; import type { ASTAndDefiniteProgram } from './shared'; -import { getAstFromProgram } from './shared'; +import { CORE_COMPILER_OPTIONS, getAstFromProgram } from './shared'; const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); -export interface ProvidedProgramsSettings { - filePath: string; - tsconfigRootDir: string; -} - function useProvidedPrograms( programInstances: Iterable, - { filePath, tsconfigRootDir }: ProvidedProgramsSettings, + parseSettings: ParseSettings, ): ASTAndDefiniteProgram | undefined { - log('Retrieving ast for %s from provided program instance(s)', filePath); + log( + 'Retrieving ast for %s from provided program instance(s)', + parseSettings.filePath, + ); let astAndProgram: ASTAndDefiniteProgram | undefined; for (const programInstance of programInstances) { - astAndProgram = getAstFromProgram(programInstance, filePath); + astAndProgram = getAstFromProgram(programInstance, parseSettings.filePath); // Stop at the first applicable program instance if (astAndProgram) { break; } } - if (!astAndProgram) { - const relativeFilePath = path.relative( - tsconfigRootDir || process.cwd(), - filePath, - ); - const errorLines = [ - '"parserOptions.programs" has been provided for @typescript-eslint/parser.', - `The file was not found in any of the provided program instance(s): ${relativeFilePath}`, - ]; - - throw new Error(errorLines.join('\n')); + if (astAndProgram) { + astAndProgram.program.getTypeChecker(); // ensure parent pointers are set in source files + return astAndProgram; } - astAndProgram.program.getTypeChecker(); // ensure parent pointers are set in source files + const relativeFilePath = path.relative( + parseSettings.tsconfigRootDir, + parseSettings.filePath, + ); + + const [typeSource, typeSources] = + parseSettings.projects.size > 0 + ? ['project', 'project(s)'] + : ['programs', 'program instance(s)']; + + const errorLines = [ + `"parserOptions.${typeSource}" has been provided for @typescript-eslint/parser.`, + `The file was not found in any of the provided ${typeSources}: ${relativeFilePath}`, + ]; - return astAndProgram; + throw new Error(errorLines.join('\n')); } /** diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 721242dd39f8..45e5a0a92b87 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -195,11 +195,10 @@ function parseAndGenerateServices( }; } - /** - * Generate a full ts.Program or offer provided instances in order to be able to provide parser services, such as type-checking - */ const hasFullTypeInformation = - parseSettings.programs != null || parseSettings.projects.size > 0; + parseSettings.programs != null || + parseSettings.projects.size > 0 || + !!parseSettings.projectService; if ( typeof tsestreeOptions.errorOnTypeScriptSyntacticAndSemanticIssues === diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 516ad0dbcdb5..fdb438930df1 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -7,7 +7,12 @@ import * as ts from 'typescript'; import { createProjectProgram } from './create-program/createProjectProgram'; import type { ProjectServiceSettings } from './create-program/createProjectService'; -import type { ASTAndDefiniteProgram } from './create-program/shared'; +import { createNoProgram } from './create-program/createSourceFile'; +import type { + ASTAndDefiniteProgram, + ASTAndNoProgram, + ASTAndProgram, +} from './create-program/shared'; import { DEFAULT_PROJECT_FILES_ERROR_EXPLANATION } from './create-program/validateDefaultProjectForFilesGlob'; import type { MutableParseSettings } from './parseSettings'; @@ -42,70 +47,107 @@ const updateExtraFileExtensions = ( } }; -export function useProgramFromProjectService( - { - allowDefaultProject, - maximumDefaultProjectFileMatchCount, - service, - }: ProjectServiceSettings, - parseSettings: Readonly, - hasFullTypeInformation: boolean, +function openClientFileFromProjectService( defaultProjectMatchedFiles: Set, -): ASTAndDefiniteProgram | undefined { - // NOTE: triggers a full project reload when changes are detected - updateExtraFileExtensions(service, parseSettings.extraFileExtensions); - - // We don't canonicalize the filename because it caused a performance regression. - // See https://github.com/typescript-eslint/typescript-eslint/issues/8519 - const filePathAbsolute = absolutify(parseSettings.filePath); - log( - 'Opening project service file for: %s at absolute path %s', - parseSettings.filePath, - filePathAbsolute, - ); - - const opened = service.openClientFile( + isDefaultProjectAllowed: boolean, + filePathAbsolute: string, + parseSettings: Readonly, + serviceSettings: ProjectServiceSettings, +): ts.server.OpenConfiguredProjectResult { + const opened = serviceSettings.service.openClientFile( filePathAbsolute, parseSettings.codeFullText, /* scriptKind */ undefined, parseSettings.tsconfigRootDir, ); - log('Opened project service file: %o', opened); + log( + 'Project service type information enabled; checking for file path match on: %o', + serviceSettings.allowDefaultProject, + ); - if (hasFullTypeInformation) { - log( - 'Project service type information enabled; checking for file path match on: %o', - allowDefaultProject, - ); - const isDefaultProjectAllowedPath = filePathMatchedBy( - parseSettings.filePath, - allowDefaultProject, - ); + log( + 'Default project allowed path: %s, based on config file: %s', + isDefaultProjectAllowed, + opened.configFileName, + ); - log( - 'Default project allowed path: %s, based on config file: %s', - isDefaultProjectAllowedPath, - opened.configFileName, + if (opened.configFileName) { + if (isDefaultProjectAllowed) { + throw new Error( + `${parseSettings.filePath} was included by allowDefaultProject but also was found in the project service. Consider removing it from allowDefaultProject.`, + ); + } + } else if (!isDefaultProjectAllowed) { + throw new Error( + `${parseSettings.filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.`, ); + } + + // No a configFileName indicates this file wasn't included in a TSConfig. + // That means it must get its type information from the default project. + if (!opened.configFileName) { + defaultProjectMatchedFiles.add(filePathAbsolute); + if ( + defaultProjectMatchedFiles.size > + serviceSettings.maximumDefaultProjectFileMatchCount + ) { + const filePrintLimit = 20; + const filesToPrint = Array.from(defaultProjectMatchedFiles).slice( + 0, + filePrintLimit, + ); + const truncatedFileCount = + defaultProjectMatchedFiles.size - filesToPrint.length; - if (opened.configFileName) { - if (isDefaultProjectAllowedPath) { - throw new Error( - `${parseSettings.filePath} was included by allowDefaultProject but also was found in the project service. Consider removing it from allowDefaultProject.`, - ); - } - } else if (!isDefaultProjectAllowedPath) { throw new Error( - `${parseSettings.filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.`, + `Too many files (>${serviceSettings.maximumDefaultProjectFileMatchCount}) have matched the default project.${DEFAULT_PROJECT_FILES_ERROR_EXPLANATION} +Matching files: +${filesToPrint.map(file => `- ${file}`).join('\n')} +${truncatedFileCount ? `...and ${truncatedFileCount} more files\n` : ''} +If you absolutely need more files included, set parserOptions.projectService.maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING to a larger value. +`, ); } } + + return opened; +} + +function createNoProgramWithProjectService( + filePathAbsolute: string, + parseSettings: Readonly, + service: ts.server.ProjectService, +): ASTAndNoProgram { + log('No project service information available. Creating no program.'); + + // If the project service knows about this file, this informs if of changes. + // Doing so ensures that: + // - if the file is not part of a project, we don't waste time creating a program (fast non-type-aware linting) + // - otherwise, we refresh the file in the project service (moderately fast, since the project is already loaded) + if (service.getScriptInfo(filePathAbsolute)) { + log('Script info available. Opening client file in project service.'); + service.openClientFile( + filePathAbsolute, + parseSettings.codeFullText, + /* scriptKind */ undefined, + parseSettings.tsconfigRootDir, + ); + } + + return createNoProgram(parseSettings); +} + +function retrieveASTAndProgramFor( + filePathAbsolute: string, + parseSettings: Readonly, + serviceSettings: ProjectServiceSettings, +): ASTAndDefiniteProgram | undefined { log('Retrieving script info and then program for: %s', filePathAbsolute); - const scriptInfo = service.getScriptInfo(filePathAbsolute); + const scriptInfo = serviceSettings.service.getScriptInfo(filePathAbsolute); /* eslint-disable @typescript-eslint/no-non-null-assertion */ - const program = service + const program = serviceSettings.service .getDefaultProjectForFile(scriptInfo!.fileName, true)! .getLanguageService(/*ensureSynchronized*/ true) .getProgram(); @@ -116,37 +158,96 @@ export function useProgramFromProjectService( return undefined; } - if (!opened.configFileName) { - defaultProjectMatchedFiles.add(filePathAbsolute); - } - if (defaultProjectMatchedFiles.size > maximumDefaultProjectFileMatchCount) { - const filePrintLimit = 20; - const filesToPrint = Array.from(defaultProjectMatchedFiles).slice( - 0, - filePrintLimit, - ); - const truncatedFileCount = - defaultProjectMatchedFiles.size - filesToPrint.length; + log('Found project service program for: %s', filePathAbsolute); - throw new Error( - `Too many files (>${maximumDefaultProjectFileMatchCount}) have matched the default project.${DEFAULT_PROJECT_FILES_ERROR_EXPLANATION} -Matching files: -${filesToPrint.map(file => `- ${file}`).join('\n')} -${truncatedFileCount ? `...and ${truncatedFileCount} more files\n` : ''} -If you absolutely need more files included, set parserOptions.projectService.maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING to a larger value. -`, + return createProjectProgram(parseSettings, [program]); +} + +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: boolean, + defaultProjectMatchedFiles: Set, +): ASTAndProgram | undefined; +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: true, + defaultProjectMatchedFiles: Set, +): ASTAndDefiniteProgram | undefined; +export function useProgramFromProjectService( + settings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: false, + defaultProjectMatchedFiles: Set, +): ASTAndNoProgram | undefined; +export function useProgramFromProjectService( + serviceSettings: ProjectServiceSettings, + parseSettings: Readonly, + hasFullTypeInformation: boolean, + defaultProjectMatchedFiles: Set, +): ASTAndProgram | undefined { + // NOTE: triggers a full project reload when changes are detected + updateExtraFileExtensions( + serviceSettings.service, + parseSettings.extraFileExtensions, + ); + + // We don't canonicalize the filename because it caused a performance regression. + // See https://github.com/typescript-eslint/typescript-eslint/issues/8519 + const filePathAbsolute = absolutify(parseSettings.filePath, serviceSettings); + log( + 'Opening project service file for: %s at absolute path %s', + parseSettings.filePath, + filePathAbsolute, + ); + + const isDefaultProjectAllowed = filePathMatchedBy( + parseSettings.filePath, + serviceSettings.allowDefaultProject, + ); + + // Type-aware linting is disabled for this file. + // However, type-aware lint rules might still rely on its contents. + if (!hasFullTypeInformation && !isDefaultProjectAllowed) { + return createNoProgramWithProjectService( + filePathAbsolute, + parseSettings, + serviceSettings.service, ); } - log('Found project service program for: %s', filePathAbsolute); + // If type info was requested, we attempt to open it in the project service. + // By now, the file is known to be one of: + // - in the project service (valid configuration) + // - allowlisted in the default project (valid configuration) + // - neither, which openClientFileFromProjectService will throw an error for + const opened = + hasFullTypeInformation && + openClientFileFromProjectService( + defaultProjectMatchedFiles, + isDefaultProjectAllowed, + filePathAbsolute, + parseSettings, + serviceSettings, + ); - return createProjectProgram(parseSettings, [program]); + log('Opened project service file: %o', opened); - function absolutify(filePath: string): string { - return path.isAbsolute(filePath) - ? filePath - : path.join(service.host.getCurrentDirectory(), filePath); - } + return retrieveASTAndProgramFor( + filePathAbsolute, + parseSettings, + serviceSettings, + ); +} + +function absolutify( + filePath: string, + serviceSettings: ProjectServiceSettings, +): string { + return path.isAbsolute(filePath) + ? filePath + : path.join(serviceSettings.service.host.getCurrentDirectory(), filePath); } function filePathMatchedBy( diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 7c98a26117aa..90efc15f77cd 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -34,7 +34,6 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({ }, })); -// eslint-disable-next-line @typescript-eslint/no-require-imports const { createProjectService, } = require('../../src/create-program/createProjectService'); diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index 0b976af5b7a8..1be10f4d5eca 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -369,17 +369,12 @@ describe('parseAndGenerateServices', () => { }; const testExtraFileExtensions = (filePath: string, extraFileExtensions: string[]) => (): void => { - const result = parser.parseAndGenerateServices(code, { + parser.parseAndGenerateServices(code, { ...config, extraFileExtensions, filePath: join(PROJECT_DIR, filePath), projectService: true, }); - const compilerOptions = result.services.program?.getCompilerOptions(); - - if (!compilerOptions?.configFilePath) { - throw new Error('No config file found, using inferred project'); - } }; describe('project includes', () => { @@ -505,35 +500,31 @@ describe('parseAndGenerateServices', () => { it("the file isn't included", () => { expect( testExtraFileExtensions('other/notIncluded.vue', ['.vue']), - ).toThrowErrorMatchingInlineSnapshot( - `"No config file found, using inferred project"`, - ); + ).toThrow(/notIncluded\.vue was not found by the project service/); }); it('duplicate extension', () => { expect( testExtraFileExtensions('ts/notIncluded.ts', ['.ts']), - ).toThrowErrorMatchingInlineSnapshot( - `"No config file found, using inferred project"`, - ); + ).toThrow(/notIncluded\.ts was not found by the project service/); }); }); - it('invalid extension', () => { + it('extension matching the file name but not a file on disk', () => { expect( testExtraFileExtensions('other/unknownFileType.unknown', [ 'unknown', ]), - ).toThrowErrorMatchingInlineSnapshot( - `"No config file found, using inferred project"`, + ).toThrow( + /unknownFileType\.unknown was not found by the project service/, ); }); - it('the extension does not match', () => { + it('the extension does not match the file name', () => { expect( testExtraFileExtensions('other/unknownFileType.unknown', ['.vue']), - ).toThrowErrorMatchingInlineSnapshot( - `"No config file found, using inferred project"`, + ).toThrow( + /unknownFileType\.unknown was not found by the project service/, ); }); }); @@ -566,7 +557,7 @@ describe('parseAndGenerateServices', () => { expect(testParse('ts/notIncluded0j1.ts')) .toThrowErrorMatchingInlineSnapshot(` - "ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: + "ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: - /tsconfig.json - /tsconfig.extra.json However, none of those TSConfigs include this file. Either: diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index f6818dc14076..eb0426399afb 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -41,6 +41,10 @@ function createOptions(fileName: string): TSESTreeOptions & { cwd?: string } { beforeEach(() => clearCaches()); describe('semanticInfo', () => { + beforeEach(() => { + process.env.TSESTREE_SINGLE_RUN = ''; + }); + // test all AST snapshots testFiles.forEach(filename => { const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); @@ -327,6 +331,24 @@ describe('semanticInfo', () => { const parseResult = parseAndGenerateServices(code, optionsProjectString); expect(parseResult.services.program).toBe(program1); }); + + it('file not in single provided project instance in single-run mode should throw', () => { + process.env.TSESTREE_SINGLE_RUN = 'true'; + const filename = 'non-existent-file.ts'; + const options = createOptions(filename); + const optionsWithProjectTrue = { + ...options, + project: true, + programs: undefined, + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithProjectTrue), + ).toThrow( + process.env.TYPESCRIPT_ESLINT_PROJECT_SERVICE === 'true' + ? `${filename} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject.` + : `The file was not found in any of the provided project(s): ${filename}`, + ); + }); } it('file not in single provided program instance should throw', () => { diff --git a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index 03940dda400e..b3a8e4e4e4e4 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type -- Fancy mocks */ import path from 'path'; -import { ScriptKind } from 'typescript'; +import * as ts from 'typescript'; import type { ProjectServiceSettings, @@ -9,6 +9,14 @@ import type { import type { ParseSettings } from '../../src/parseSettings'; import { useProgramFromProjectService } from '../../src/useProgramFromProjectService'; +const mockCreateNoProgram = jest.fn(); + +jest.mock('../../src/create-program/createSourceFile', () => ({ + get createNoProgram() { + return mockCreateNoProgram; + }, +})); + const mockCreateProjectProgram = jest.fn(); jest.mock('../../src/create-program/createProjectProgram', () => ({ @@ -44,8 +52,10 @@ function createMockProjectService() { }; } +const mockFileName = 'camelCaseFile.ts'; + const mockParseSettings = { - filePath: 'path/PascalCaseDirectory/camelCaseFile.ts', + filePath: `path/PascalCaseDirectory/${mockFileName}`, extraFileExtensions: [] as readonly string[], } as ParseSettings; @@ -59,6 +69,29 @@ const createProjectServiceSettings = < }); describe('useProgramFromProjectService', () => { + it('creates a standalone AST with no program when hasFullTypeInformation is false and allowDefaultProject is falsy', () => { + const { service } = createMockProjectService(); + + const stubASTAndNoProgram = { + ast: ts.createSourceFile(mockFileName, '', ts.ScriptTarget.Latest), + program: null, + }; + + mockCreateNoProgram.mockReturnValueOnce(stubASTAndNoProgram); + + const actual = useProgramFromProjectService( + createProjectServiceSettings({ + allowDefaultProject: undefined, + service, + }), + mockParseSettings, + false, + new Set(), + ); + + expect(actual).toBe(stubASTAndNoProgram); + }); + it('passes an absolute, case-matching file path to service.openClientFile', () => { const { service } = createMockProjectService(); @@ -123,7 +156,7 @@ describe('useProgramFromProjectService', () => { ); }); - it('throws an error when called more than the maximum allowed file count', () => { + it('throws an error when more than the maximum allowed file count is matched to the default project', () => { const { service } = createMockProjectService(); const program = { getSourceFile: jest.fn() }; @@ -255,7 +288,7 @@ If you absolutely need more files included, set parserOptions.projectService.max expect(actual).toBe(program); }); - it('returns a created program when hasFullTypeInformation is disabled, the file is neither in the project service nor allowDefaultProject, and the service has a matching program', () => { + it('returns undefined when hasFullTypeInformation is disabled, the file is neither in the project service nor allowDefaultProject, and the service has a matching program', () => { const { service } = createMockProjectService(); const program = { getSourceFile: jest.fn() }; @@ -274,10 +307,10 @@ If you absolutely need more files included, set parserOptions.projectService.max new Set(), ); - expect(actual).toBe(program); + expect(actual).toBeUndefined(); }); - it('returns a created program when hasFullTypeInformation is disabled, the file is in the project service, the service has a matching program, and no out-of-project files are allowed', () => { + it('returns undefined when hasFullTypeInformation is disabled, the file is in the project service, the service has a matching program, and no out', () => { const { service } = createMockProjectService(); const program = { getSourceFile: jest.fn() }; @@ -299,7 +332,7 @@ If you absolutely need more files included, set parserOptions.projectService.max new Set(), ); - expect(actual).toBe(program); + expect(actual).toBeUndefined(); }); it('does not call setHostConfiguration on the service with default extensions if extraFileExtensions are not provided', () => { @@ -358,7 +391,7 @@ If you absolutely need more files included, set parserOptions.projectService.max { extension: '.vue', isMixedContent: false, - scriptKind: ScriptKind.Deferred, + scriptKind: ts.ScriptKind.Deferred, }, ], }); @@ -387,7 +420,7 @@ If you absolutely need more files included, set parserOptions.projectService.max { extension: '.vue', isMixedContent: false, - scriptKind: ScriptKind.Deferred, + scriptKind: ts.ScriptKind.Deferred, }, ], }); @@ -427,7 +460,7 @@ If you absolutely need more files included, set parserOptions.projectService.max { extension: '.vue', isMixedContent: false, - scriptKind: ScriptKind.Deferred, + scriptKind: ts.ScriptKind.Deferred, }, ], }); @@ -471,7 +504,7 @@ If you absolutely need more files included, set parserOptions.projectService.max { extension: '.vue', isMixedContent: false, - scriptKind: ScriptKind.Deferred, + scriptKind: ts.ScriptKind.Deferred, }, ], }); diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index a830c0c2ccef..ec949ee51b61 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for utils to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for utils to align it with other projects, there were no code changes. diff --git a/packages/utils/package.json b/packages/utils/package.json index 4daaa39a4400..6bc00a2c3fb5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/utils", - "version": "7.16.1", + "version": "7.17.0", "description": "Utilities for working with TypeScript + ESLint together", "files": [ "dist", @@ -64,9 +64,9 @@ }, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1" + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" diff --git a/packages/utils/src/ts-eslint/Rule.ts b/packages/utils/src/ts-eslint/Rule.ts index e159ea8c42dc..62cf392d38b3 100644 --- a/packages/utils/src/ts-eslint/Rule.ts +++ b/packages/utils/src/ts-eslint/Rule.ts @@ -11,7 +11,7 @@ export type RuleRecommendation = 'recommended' | 'strict' | 'stylistic'; export interface RuleRecommendationAcrossConfigs< Options extends readonly unknown[], > { - recommended: true; + recommended?: true; strict: Partial; } diff --git a/packages/utils/tests/eslint-utils/deepMerge.test.ts b/packages/utils/tests/eslint-utils/deepMerge.test.ts index 26fb02e3bc4d..27c671c99aae 100644 --- a/packages/utils/tests/eslint-utils/deepMerge.test.ts +++ b/packages/utils/tests/eslint-utils/deepMerge.test.ts @@ -38,7 +38,7 @@ describe('deepMerge', () => { }, }; - expect(ESLintUtils.deepMerge(a, b)).toStrictEqual(Object.assign({}, a, b)); + expect(ESLintUtils.deepMerge(a, b)).toStrictEqual({ ...a, ...b }); }); it('deeply overwrites properties in the first one with the second', () => { diff --git a/packages/visitor-keys/CHANGELOG.md b/packages/visitor-keys/CHANGELOG.md index 75e596d890d1..4a3890da9e46 100644 --- a/packages/visitor-keys/CHANGELOG.md +++ b/packages/visitor-keys/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for visitor-keys to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for visitor-keys to align it with other projects, there were no code changes. diff --git a/packages/visitor-keys/package.json b/packages/visitor-keys/package.json index bcb3dc0d8afa..3dca148c2afe 100644 --- a/packages/visitor-keys/package.json +++ b/packages/visitor-keys/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/visitor-keys", - "version": "7.16.1", + "version": "7.17.0", "description": "Visitor keys used to help traverse the TypeScript-ESTree AST", "files": [ "dist", @@ -47,7 +47,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/types": "7.17.0", "eslint-visitor-keys": "^3.4.3" }, "devDependencies": { diff --git a/packages/website-eslint/CHANGELOG.md b/packages/website-eslint/CHANGELOG.md index d7559a0f8e01..203647ff68cf 100644 --- a/packages/website-eslint/CHANGELOG.md +++ b/packages/website-eslint/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.17.0 (2024-07-22) + +This was a version bump only for website-eslint to align it with other projects, there were no code changes. + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for website-eslint to align it with other projects, there were no code changes. diff --git a/packages/website-eslint/package.json b/packages/website-eslint/package.json index 893041b19321..d003bf35bab1 100644 --- a/packages/website-eslint/package.json +++ b/packages/website-eslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/website-eslint", - "version": "7.16.1", + "version": "7.17.0", "private": true, "description": "ESLint which works in browsers.", "files": [ @@ -24,11 +24,11 @@ }, "devDependencies": { "@eslint/js": "*", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "esbuild": "~0.23.0", "eslint": "*", "esquery": "*", diff --git a/packages/website-eslint/src/mock/path.js b/packages/website-eslint/src/mock/path.js index e021496409a7..dceb2bb8f3d5 100644 --- a/packages/website-eslint/src/mock/path.js +++ b/packages/website-eslint/src/mock/path.js @@ -92,8 +92,8 @@ export function resolve(...args) { // path.normalize(path) // posix version export function normalize(path) { - let isPathAbsolute = isAbsolute(path); - let trailingSlash = path.endsWith('/'); + const isPathAbsolute = isAbsolute(path); + const trailingSlash = path.endsWith('/'); // Normalize the path path = normalizeArray( @@ -142,7 +142,7 @@ export function relative(from, to) { } } - var end = arr.length - 1; + let end = arr.length - 1; for (; end >= 0; end--) { if (arr[end] !== '') { break; diff --git a/packages/website/CHANGELOG.md b/packages/website/CHANGELOG.md index 1f442d37b2b2..f8b0181cc962 100644 --- a/packages/website/CHANGELOG.md +++ b/packages/website/CHANGELOG.md @@ -1,3 +1,20 @@ +## 7.17.0 (2024-07-22) + + +### 🩹 Fixes + +- **website:** expose ATA types to eslint instance + + +### ❤️ Thank You + +- Armano +- Josh Goldberg ✨ +- Kirk Waiblinger +- StyleShit + +You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website. + ## 7.16.1 (2024-07-15) This was a version bump only for website to align it with other projects, there were no code changes. diff --git a/packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx b/packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx index 02ff22ed12ef..78c17a30e539 100644 --- a/packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx +++ b/packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx @@ -86,7 +86,7 @@ It's been experimentally available since v6.1.0 under the name `EXPERIMENTAL_use You can use the new project service in your configuration instead of the previous `parserOptions.project`: -```js title="eslint.config.js" +```js title="eslint.config.mjs" import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; @@ -109,14 +109,14 @@ export default tseslint.config( The project service will automatically find the closest `tsconfig.json` for each file (like `project: true`). It also allows enabling typed linting for files not explicitly included in a `tsconfig.json`. -This should remove the need for custom `tsconfig.eslint.json` files to lint files like `eslint.config.js`! +This should remove the need for custom `tsconfig.eslint.json` files to lint files like `eslint.config.mjs`! Typed linting for out-of-project files can be done by specifying two properties of a `parserOptions.projectService` object: - `allowDefaultProject`: a glob of a small number of out-of-project files to enable a slower default project on - `defaultProject`: path to a TypeScript configuration file to use for the slower default project -```js title="eslint.config.js" +```js title="eslint.config.mjs" import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; diff --git a/packages/website/package.json b/packages/website/package.json index 6a2b8c1fabdd..e76f72e7fa1a 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "website", - "version": "7.16.1", + "version": "7.17.0", "private": true, "scripts": { "build": "docusaurus build", @@ -23,8 +23,8 @@ "@docusaurus/preset-classic": "^3.2.1", "@docusaurus/remark-plugin-npm2yarn": "^3.2.1", "@docusaurus/theme-common": "^3.2.1", - "@typescript-eslint/parser": "7.16.1", - "@typescript-eslint/website-eslint": "7.16.1", + "@typescript-eslint/parser": "7.17.0", + "@typescript-eslint/website-eslint": "7.17.0", "@uiw/react-shields": "2.0.1", "clsx": "^2.1.0", "eslint": "*", @@ -47,12 +47,12 @@ "@types/mdast": "^4.0.3", "@types/react": "*", "@types/unist": "^3.0.2", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/rule-schema-to-typescript-types": "7.16.1", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/rule-schema-to-typescript-types": "7.17.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "copy-webpack-plugin": "^12.0.0", "cross-fetch": "*", "history": "^4.9.0", diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 7bceb14f7574..530ee3fde6e7 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -81,6 +81,24 @@ export const useSandboxServices = ( const system = createFileSystem(props, sandboxInstance.tsvfs); + // Write files in vfs when a model is created in the editor (this is used only for ATA types) + sandboxInstance.monaco.editor.onDidCreateModel(model => { + if (!model.uri.path.includes('node_modules')) { + return; + } + const path = model.uri.path.replace('/file:///', '/'); + system.writeFile(path, model.getValue()); + }); + // Delete files in vfs when a model is disposed in the editor (this is used only for ATA types) + sandboxInstance.monaco.editor.onWillDisposeModel(model => { + if (!model.uri.path.includes('node_modules')) { + return; + } + const path = model.uri.path.replace('/file:///', '/'); + system.deleteFile(path); + }); + + // Load the lib files from typescript to vfs (eg. es2020.d.ts) const worker = await sandboxInstance.getWorkerProcess(); if (worker.getLibFiles) { const libs = await worker.getLibFiles(); diff --git a/packages/website/src/vendor/sandbox.d.ts b/packages/website/src/vendor/sandbox.d.ts index 9a43757ba3db..8760842cc6c8 100644 --- a/packages/website/src/vendor/sandbox.d.ts +++ b/packages/website/src/vendor/sandbox.d.ts @@ -60,23 +60,23 @@ export declare function defaultPlaygroundSettings(): { /** The default source code for the playground */ text: string; /** @deprecated */ - useJavaScript?: boolean | undefined; + useJavaScript?: boolean; /** The default file for the playground */ filetype: 'js' | 'ts' | 'd.ts'; /** Compiler options which are automatically just forwarded on */ - compilerOptions: MonacoEditor.languages.typescript.CompilerOptions; + compilerOptions: CompilerOptions; /** Optional monaco settings overrides */ - monacoSettings?: MonacoEditor.editor.IEditorOptions | undefined; + monacoSettings?: MonacoEditor.editor.IEditorOptions; /** Acquire types via type acquisition */ acquireTypes: boolean; /** Support twoslash compiler options */ supportTwoslashCompilerOptions: boolean; /** Get the text via query params and local storage, useful when the editor is the main experience */ - suppressAutomaticallyGettingDefaultText?: true | undefined; + suppressAutomaticallyGettingDefaultText?: true; /** Suppress setting compiler options from the compiler flags from query params */ - suppressAutomaticallyGettingCompilerFlags?: true | undefined; + suppressAutomaticallyGettingCompilerFlags?: true; /** Optional path to TypeScript worker wrapper class script, see https://github.com/microsoft/monaco-typescript/pull/65 */ - customTypeScriptWorkerPath?: string | undefined; + customTypeScriptWorkerPath?: string; /** Logging system */ logger: { log: (...args: any[]) => void; @@ -94,29 +94,52 @@ export declare const createTypeScriptSandbox: ( ts: typeof ts, ) => { /** The same config you passed in */ - config: { - text: string; - useJavaScript?: boolean | undefined; - filetype: 'js' | 'ts' | 'd.ts'; - compilerOptions: CompilerOptions; - monacoSettings?: MonacoEditor.editor.IEditorOptions | undefined; - acquireTypes: boolean; - supportTwoslashCompilerOptions: boolean; - suppressAutomaticallyGettingDefaultText?: true | undefined; - suppressAutomaticallyGettingCompilerFlags?: true | undefined; - customTypeScriptWorkerPath?: string | undefined; - logger: { - log: (...args: any[]) => void; - error: (...args: any[]) => void; - groupCollapsed: (...args: any[]) => void; - groupEnd: (...args: any[]) => void; - }; - domID: string; - }; + config: + | { + text: string; + useJavaScript?: boolean; + filetype: 'js' | 'ts' | 'd.ts'; + compilerOptions: CompilerOptions; + monacoSettings?: MonacoEditor.editor.IEditorOptions; + acquireTypes: boolean; + supportTwoslashCompilerOptions: boolean; + suppressAutomaticallyGettingDefaultText?: true; + suppressAutomaticallyGettingCompilerFlags?: true; + customTypeScriptWorkerPath?: string; + logger: { + log: (...args: any[]) => void; + error: (...args: any[]) => void; + groupCollapsed: (...args: any[]) => void; + groupEnd: (...args: any[]) => void; + }; + domID: string; + } + | { + text: string; + useJavaScript?: boolean; + filetype: 'js' | 'ts' | 'd.ts'; + compilerOptions: CompilerOptions; + monacoSettings?: MonacoEditor.editor.IEditorOptions; + acquireTypes: boolean; + supportTwoslashCompilerOptions: boolean; + suppressAutomaticallyGettingDefaultText?: true; + suppressAutomaticallyGettingCompilerFlags?: true; + customTypeScriptWorkerPath?: string; + logger: { + log: (...args: any[]) => void; + error: (...args: any[]) => void; + groupCollapsed: (...args: any[]) => void; + groupEnd: (...args: any[]) => void; + }; + elementToAppend?: HTMLElement | undefined; + domID: string; + }; /** A list of TypeScript versions you can use with the TypeScript sandbox */ supportedVersions: readonly [ - '5.2.1-rc', - '5.2.0-beta', + '5.5.3', + '5.4.5', + '5.3.3', + '5.2.2', '5.1.6', '5.0.4', '4.9.5', @@ -152,7 +175,10 @@ export declare const createTypeScriptSandbox: ( /** A copy of require("@typescript/vfs") this can be used to quickly set up an in-memory compiler runs for ASTs, or to get complex language server results (anything above has to be serialized when passed)*/ tsvfs: typeof tsvfs; /** Get all the different emitted files after TypeScript is run */ - getEmitResult: () => Promise; + getEmitResult: ( + emitOnlyDtsFiles?: boolean, + forceDtsEmit?: boolean, + ) => Promise; /** Gets just the JavaScript for your sandbox, will transpile if in TS only */ getRunnableJS: () => Promise; /** Gets the DTS output of the main code in the editor */ @@ -188,6 +214,7 @@ export declare const createTypeScriptSandbox: ( host: { compilerHost: ts.CompilerHost; updateFile: (sourceFile: ts.SourceFile) => boolean; + deleteFile: (sourceFile: ts.SourceFile) => boolean; }; fsMap: Map; }>; @@ -196,87 +223,85 @@ export declare const createTypeScriptSandbox: ( /** The Sandbox's default compiler options */ compilerDefaults: { [x: string]: MonacoEditor.languages.typescript.CompilerOptionsValue; - allowJs?: boolean | undefined; - allowSyntheticDefaultImports?: boolean | undefined; - allowUmdGlobalAccess?: boolean | undefined; - allowUnreachableCode?: boolean | undefined; - allowUnusedLabels?: boolean | undefined; - alwaysStrict?: boolean | undefined; - baseUrl?: string | undefined; - charset?: string | undefined; - checkJs?: boolean | undefined; - declaration?: boolean | undefined; - declarationMap?: boolean | undefined; - emitDeclarationOnly?: boolean | undefined; - declarationDir?: string | undefined; - disableSizeLimit?: boolean | undefined; - disableSourceOfProjectReferenceRedirect?: boolean | undefined; - downlevelIteration?: boolean | undefined; - emitBOM?: boolean | undefined; - emitDecoratorMetadata?: boolean | undefined; - experimentalDecorators?: boolean | undefined; - forceConsistentCasingInFileNames?: boolean | undefined; - importHelpers?: boolean | undefined; - inlineSourceMap?: boolean | undefined; - inlineSources?: boolean | undefined; - isolatedModules?: boolean | undefined; - jsx?: MonacoEditor.languages.typescript.JsxEmit | undefined; - keyofStringsOnly?: boolean | undefined; - lib?: string[] | undefined; - locale?: string | undefined; - mapRoot?: string | undefined; - maxNodeModuleJsDepth?: number | undefined; - module?: MonacoEditor.languages.typescript.ModuleKind | undefined; - moduleResolution?: - | MonacoEditor.languages.typescript.ModuleResolutionKind - | undefined; - newLine?: MonacoEditor.languages.typescript.NewLineKind | undefined; - noEmit?: boolean | undefined; - noEmitHelpers?: boolean | undefined; - noEmitOnError?: boolean | undefined; - noErrorTruncation?: boolean | undefined; - noFallthroughCasesInSwitch?: boolean | undefined; - noImplicitAny?: boolean | undefined; - noImplicitReturns?: boolean | undefined; - noImplicitThis?: boolean | undefined; - noStrictGenericChecks?: boolean | undefined; - noUnusedLocals?: boolean | undefined; - noUnusedParameters?: boolean | undefined; - noImplicitUseStrict?: boolean | undefined; - noLib?: boolean | undefined; - noResolve?: boolean | undefined; - out?: string | undefined; - outDir?: string | undefined; - outFile?: string | undefined; - paths?: MonacoEditor.languages.typescript.MapLike | undefined; - preserveConstEnums?: boolean | undefined; - preserveSymlinks?: boolean | undefined; - project?: string | undefined; - reactNamespace?: string | undefined; - jsxFactory?: string | undefined; - composite?: boolean | undefined; - removeComments?: boolean | undefined; - rootDir?: string | undefined; - rootDirs?: string[] | undefined; - skipLibCheck?: boolean | undefined; - skipDefaultLibCheck?: boolean | undefined; - sourceMap?: boolean | undefined; - sourceRoot?: string | undefined; - strict?: boolean | undefined; - strictFunctionTypes?: boolean | undefined; - strictBindCallApply?: boolean | undefined; - strictNullChecks?: boolean | undefined; - strictPropertyInitialization?: boolean | undefined; - stripInternal?: boolean | undefined; - suppressExcessPropertyErrors?: boolean | undefined; - suppressImplicitAnyIndexErrors?: boolean | undefined; - target?: MonacoEditor.languages.typescript.ScriptTarget | undefined; - traceResolution?: boolean | undefined; - resolveJsonModule?: boolean | undefined; - types?: string[] | undefined; - typeRoots?: string[] | undefined; - esModuleInterop?: boolean | undefined; - useDefineForClassFields?: boolean | undefined; + allowJs?: boolean; + allowSyntheticDefaultImports?: boolean; + allowUmdGlobalAccess?: boolean; + allowUnreachableCode?: boolean; + allowUnusedLabels?: boolean; + alwaysStrict?: boolean; + baseUrl?: string; + charset?: string; + checkJs?: boolean; + declaration?: boolean; + declarationMap?: boolean; + emitDeclarationOnly?: boolean; + declarationDir?: string; + disableSizeLimit?: boolean; + disableSourceOfProjectReferenceRedirect?: boolean; + downlevelIteration?: boolean; + emitBOM?: boolean; + emitDecoratorMetadata?: boolean; + experimentalDecorators?: boolean; + forceConsistentCasingInFileNames?: boolean; + importHelpers?: boolean; + inlineSourceMap?: boolean; + inlineSources?: boolean; + isolatedModules?: boolean; + jsx?: MonacoEditor.languages.typescript.JsxEmit; + keyofStringsOnly?: boolean; + lib?: string[]; + locale?: string; + mapRoot?: string; + maxNodeModuleJsDepth?: number; + module?: MonacoEditor.languages.typescript.ModuleKind; + moduleResolution?: MonacoEditor.languages.typescript.ModuleResolutionKind; + newLine?: MonacoEditor.languages.typescript.NewLineKind; + noEmit?: boolean; + noEmitHelpers?: boolean; + noEmitOnError?: boolean; + noErrorTruncation?: boolean; + noFallthroughCasesInSwitch?: boolean; + noImplicitAny?: boolean; + noImplicitReturns?: boolean; + noImplicitThis?: boolean; + noStrictGenericChecks?: boolean; + noUnusedLocals?: boolean; + noUnusedParameters?: boolean; + noImplicitUseStrict?: boolean; + noLib?: boolean; + noResolve?: boolean; + out?: string; + outDir?: string; + outFile?: string; + paths?: MonacoEditor.languages.typescript.MapLike; + preserveConstEnums?: boolean; + preserveSymlinks?: boolean; + project?: string; + reactNamespace?: string; + jsxFactory?: string; + composite?: boolean; + removeComments?: boolean; + rootDir?: string; + rootDirs?: string[]; + skipLibCheck?: boolean; + skipDefaultLibCheck?: boolean; + sourceMap?: boolean; + sourceRoot?: string; + strict?: boolean; + strictFunctionTypes?: boolean; + strictBindCallApply?: boolean; + strictNullChecks?: boolean; + strictPropertyInitialization?: boolean; + stripInternal?: boolean; + suppressExcessPropertyErrors?: boolean; + suppressImplicitAnyIndexErrors?: boolean; + target?: MonacoEditor.languages.typescript.ScriptTarget; + traceResolution?: boolean; + resolveJsonModule?: boolean; + types?: string[]; + typeRoots?: string[]; + esModuleInterop?: boolean; + useDefineForClassFields?: boolean; }; /** The Sandbox's current compiler options */ getCompilerOptions: () => MonacoEditor.languages.typescript.CompilerOptions; diff --git a/packages/website/src/vendor/typescript-vfs.d.ts b/packages/website/src/vendor/typescript-vfs.d.ts index e6c7df7b4391..8ce45dbdade9 100644 --- a/packages/website/src/vendor/typescript-vfs.d.ts +++ b/packages/website/src/vendor/typescript-vfs.d.ts @@ -16,6 +16,15 @@ type LanguageServiceHost = ts.LanguageServiceHost; type CompilerHost = ts.CompilerHost; type SourceFile = ts.SourceFile; type TS = typeof ts; +type FetchLike = (url: string) => Promise<{ + json(): Promise; + text(): Promise; +}>; +interface LocalStorageLike { + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; +} export interface VirtualTypeScriptEnvironment { sys: System; languageService: ts.LanguageService; @@ -26,6 +35,7 @@ export interface VirtualTypeScriptEnvironment { content: string, replaceTextSpan?: ts.TextSpan, ) => void; + deleteFile: (fileName: string) => void; } /** * Makes a virtual copy of the TypeScript environment. This is the main API you want to be using with @@ -79,6 +89,10 @@ export declare const addAllFilesFromFolder: ( export declare const addFilesForTypesIntoFolder: ( map: Map, ) => void; +export interface LZString { + compressToUTF16(input: string): string; + decompressFromUTF16(compressed: string): string; +} /** * Create a virtual FS Map with the lib files from a particular TypeScript * version based on the target, Always includes dom ATM. @@ -96,9 +110,9 @@ export declare const createDefaultMapFromCDN: ( version: string, cache: boolean, ts: TS, - lzstring?: typeof import('lz-string'), - fetcher?: typeof fetch, - storer?: typeof localStorage, + lzstring?: LZString, + fetcher?: FetchLike, + storer?: LocalStorageLike, ) => Promise>; /** * Creates an in-memory System object which can be used in a TypeScript program, this @@ -128,6 +142,7 @@ export declare function createVirtualCompilerHost( ): { compilerHost: CompilerHost; updateFile: (sourceFile: SourceFile) => boolean; + deleteFile: (sourceFile: SourceFile) => boolean; }; /** * Creates an object which can host a language service against the virtual file-system @@ -141,5 +156,6 @@ export declare function createVirtualLanguageServiceHost( ): { languageServiceHost: LanguageServiceHost; updateFile: (sourceFile: ts.SourceFile) => void; + deleteFile: (sourceFile: ts.SourceFile) => void; }; export {}; diff --git a/yarn.lock b/yarn.lock index fe86e0dd5ba3..dedcdd500a07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2306,29 +2306,29 @@ __metadata: languageName: node linkType: hard -"@csstools/css-parser-algorithms@npm:^2.6.3": - version: 2.6.3 - resolution: "@csstools/css-parser-algorithms@npm:2.6.3" +"@csstools/css-parser-algorithms@npm:^2.7.1": + version: 2.7.1 + resolution: "@csstools/css-parser-algorithms@npm:2.7.1" peerDependencies: - "@csstools/css-tokenizer": ^2.3.1 - checksum: f1bfcb6680c801f4201c30b77827e000b838e5a45b42f146ec352cd008b51ebb31b8aae00364369765c24e938391e221e37f4063e3a6151316d6990c498da103 + "@csstools/css-tokenizer": ^2.4.1 + checksum: 304e6f92e583042c310e368a82b694af563a395e5c55911caefe52765c5acb000b9daa17356ea8a4dd37d4d50132b76de48ced75159b169b53e134ff78b362ba languageName: node linkType: hard -"@csstools/css-tokenizer@npm:^2.3.1": - version: 2.3.1 - resolution: "@csstools/css-tokenizer@npm:2.3.1" - checksum: a5fe22faed5673b5d19e64aa7f4730b48711d0946470551376bc3125d831511070c94addfdfc6a62634e968955050ef2c99c92ff8cb294d9bf70ebc1f3ac22a8 +"@csstools/css-tokenizer@npm:^2.4.1": + version: 2.4.1 + resolution: "@csstools/css-tokenizer@npm:2.4.1" + checksum: 395c51f8724ddc4851d836f484346bb3ea6a67af936dde12cbf9a57ae321372e79dee717cbe4823599eb0e6fd2d5405cf8873450e986c2fca6e6ed82e7b10219 languageName: node linkType: hard -"@csstools/media-query-list-parser@npm:^2.1.11": - version: 2.1.11 - resolution: "@csstools/media-query-list-parser@npm:2.1.11" +"@csstools/media-query-list-parser@npm:^2.1.13": + version: 2.1.13 + resolution: "@csstools/media-query-list-parser@npm:2.1.13" peerDependencies: - "@csstools/css-parser-algorithms": ^2.6.3 - "@csstools/css-tokenizer": ^2.3.1 - checksum: e338eff90b43ab31b3d33c55c792f7760d556feaeaa042e12b8e3f873293b72f1140df1bbbfa1c9dcc16c58afb777f1e218e17afdcd0ab8228d2a8ea22bcbe61 + "@csstools/css-parser-algorithms": ^2.7.1 + "@csstools/css-tokenizer": ^2.4.1 + checksum: 7754b4b9fcc749a51a2bcd34a167ad16e7227ff087f6c4e15b3593d3342413446b72dad37f1adb99c62538730c77e3e47842987ce453fbb3849d329a39ba9ad7 languageName: node linkType: hard @@ -5570,17 +5570,17 @@ __metadata: dependencies: "@jest/types": 29.6.3 "@prettier/sync": ^0.5.1 - "@typescript-eslint/rule-tester": 7.16.1 - "@typescript-eslint/scope-manager": 7.16.1 - "@typescript-eslint/type-utils": 7.16.1 - "@typescript-eslint/utils": 7.16.1 + "@typescript-eslint/rule-tester": 7.17.0 + "@typescript-eslint/scope-manager": 7.17.0 + "@typescript-eslint/type-utils": 7.17.0 + "@typescript-eslint/utils": 7.17.0 jest: 29.7.0 prettier: ^3.2.5 rimraf: "*" languageName: unknown linkType: soft -"@typescript-eslint/eslint-plugin@7.16.1, @typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin": +"@typescript-eslint/eslint-plugin@7.17.0, @typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin": version: 0.0.0-use.local resolution: "@typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin" dependencies: @@ -5589,12 +5589,12 @@ __metadata: "@types/marked": ^5.0.2 "@types/mdast": ^4.0.3 "@types/natural-compare": "*" - "@typescript-eslint/rule-schema-to-typescript-types": 7.16.1 - "@typescript-eslint/rule-tester": 7.16.1 - "@typescript-eslint/scope-manager": 7.16.1 - "@typescript-eslint/type-utils": 7.16.1 - "@typescript-eslint/utils": 7.16.1 - "@typescript-eslint/visitor-keys": 7.16.1 + "@typescript-eslint/rule-schema-to-typescript-types": 7.17.0 + "@typescript-eslint/rule-tester": 7.17.0 + "@typescript-eslint/scope-manager": 7.17.0 + "@typescript-eslint/type-utils": 7.17.0 + "@typescript-eslint/utils": 7.17.0 + "@typescript-eslint/visitor-keys": 7.17.0 ajv: ^6.12.6 cross-env: ^7.0.3 cross-fetch: "*" @@ -5639,16 +5639,16 @@ __metadata: languageName: unknown linkType: soft -"@typescript-eslint/parser@7.16.1, @typescript-eslint/parser@workspace:packages/parser": +"@typescript-eslint/parser@7.17.0, @typescript-eslint/parser@workspace:packages/parser": version: 0.0.0-use.local resolution: "@typescript-eslint/parser@workspace:packages/parser" dependencies: "@jest/types": 29.6.3 "@types/glob": "*" - "@typescript-eslint/scope-manager": 7.16.1 - "@typescript-eslint/types": 7.16.1 - "@typescript-eslint/typescript-estree": 7.16.1 - "@typescript-eslint/visitor-keys": 7.16.1 + "@typescript-eslint/scope-manager": 7.17.0 + "@typescript-eslint/types": 7.17.0 + "@typescript-eslint/typescript-estree": 7.17.0 + "@typescript-eslint/visitor-keys": 7.17.0 debug: ^4.3.4 downlevel-dts: "*" glob: "*" @@ -5670,11 +5670,11 @@ __metadata: dependencies: "@jest/types": 29.6.3 "@nx/devkit": "*" - "@typescript-eslint/eslint-plugin": 7.16.1 - "@typescript-eslint/scope-manager": 7.16.1 - "@typescript-eslint/types": 7.16.1 - "@typescript-eslint/typescript-estree": 7.16.1 - "@typescript-eslint/utils": 7.16.1 + "@typescript-eslint/eslint-plugin": 7.17.0 + "@typescript-eslint/scope-manager": 7.17.0 + "@typescript-eslint/types": 7.17.0 + "@typescript-eslint/typescript-estree": 7.17.0 + "@typescript-eslint/utils": 7.17.0 cross-fetch: "*" execa: "*" prettier: ^3.2.5 @@ -5684,28 +5684,28 @@ __metadata: languageName: unknown linkType: soft -"@typescript-eslint/rule-schema-to-typescript-types@7.16.1, @typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types": +"@typescript-eslint/rule-schema-to-typescript-types@7.17.0, @typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types": version: 0.0.0-use.local resolution: "@typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types" dependencies: "@jest/types": 29.6.3 - "@typescript-eslint/type-utils": 7.16.1 - "@typescript-eslint/utils": 7.16.1 + "@typescript-eslint/type-utils": 7.17.0 + "@typescript-eslint/utils": 7.17.0 natural-compare: ^1.4.0 prettier: ^3.2.5 languageName: unknown linkType: soft -"@typescript-eslint/rule-tester@7.16.1, @typescript-eslint/rule-tester@workspace:packages/rule-tester": +"@typescript-eslint/rule-tester@7.17.0, @typescript-eslint/rule-tester@workspace:packages/rule-tester": version: 0.0.0-use.local resolution: "@typescript-eslint/rule-tester@workspace:packages/rule-tester" dependencies: "@jest/types": 29.6.3 "@types/json-stable-stringify-without-jsonify": ^1.0.2 "@types/lodash.merge": 4.6.9 - "@typescript-eslint/parser": 7.16.1 - "@typescript-eslint/typescript-estree": 7.16.1 - "@typescript-eslint/utils": 7.16.1 + "@typescript-eslint/parser": 7.17.0 + "@typescript-eslint/typescript-estree": 7.17.0 + "@typescript-eslint/utils": 7.17.0 ajv: ^6.12.6 chai: ^4.4.1 eslint-visitor-keys: ^4.0.0 @@ -5724,15 +5724,15 @@ __metadata: languageName: unknown linkType: soft -"@typescript-eslint/scope-manager@7.16.1, @typescript-eslint/scope-manager@workspace:packages/scope-manager": +"@typescript-eslint/scope-manager@7.17.0, @typescript-eslint/scope-manager@workspace:packages/scope-manager": version: 0.0.0-use.local resolution: "@typescript-eslint/scope-manager@workspace:packages/scope-manager" dependencies: "@jest/types": 29.6.3 "@types/glob": "*" - "@typescript-eslint/types": 7.16.1 - "@typescript-eslint/typescript-estree": 7.16.1 - "@typescript-eslint/visitor-keys": 7.16.1 + "@typescript-eslint/types": 7.17.0 + "@typescript-eslint/typescript-estree": 7.17.0 + "@typescript-eslint/visitor-keys": 7.17.0 glob: "*" jest-specific-snapshot: "*" make-dir: "*" @@ -5761,14 +5761,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@7.16.1, @typescript-eslint/type-utils@workspace:packages/type-utils": +"@typescript-eslint/type-utils@7.17.0, @typescript-eslint/type-utils@workspace:packages/type-utils": version: 0.0.0-use.local resolution: "@typescript-eslint/type-utils@workspace:packages/type-utils" dependencies: "@jest/types": 29.6.3 - "@typescript-eslint/parser": 7.16.1 - "@typescript-eslint/typescript-estree": 7.16.1 - "@typescript-eslint/utils": 7.16.1 + "@typescript-eslint/parser": 7.17.0 + "@typescript-eslint/typescript-estree": 7.17.0 + "@typescript-eslint/utils": 7.17.0 ajv: ^6.12.6 debug: ^4.3.4 downlevel-dts: "*" @@ -5783,7 +5783,7 @@ __metadata: languageName: unknown linkType: soft -"@typescript-eslint/types@7.16.1, @typescript-eslint/types@workspace:packages/types": +"@typescript-eslint/types@7.17.0, @typescript-eslint/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@typescript-eslint/types@workspace:packages/types" dependencies: @@ -5883,13 +5883,13 @@ __metadata: languageName: unknown linkType: soft -"@typescript-eslint/typescript-estree@7.16.1, @typescript-eslint/typescript-estree@workspace:packages/typescript-estree": +"@typescript-eslint/typescript-estree@7.17.0, @typescript-eslint/typescript-estree@workspace:packages/typescript-estree": version: 0.0.0-use.local resolution: "@typescript-eslint/typescript-estree@workspace:packages/typescript-estree" dependencies: "@jest/types": 29.6.3 - "@typescript-eslint/types": 7.16.1 - "@typescript-eslint/visitor-keys": 7.16.1 + "@typescript-eslint/types": 7.17.0 + "@typescript-eslint/visitor-keys": 7.17.0 debug: ^4.3.4 glob: "*" globby: ^11.1.0 @@ -5945,14 +5945,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@7.16.1, @typescript-eslint/utils@workspace:packages/utils": +"@typescript-eslint/utils@7.17.0, @typescript-eslint/utils@workspace:packages/utils": version: 0.0.0-use.local resolution: "@typescript-eslint/utils@workspace:packages/utils" dependencies: "@eslint-community/eslint-utils": ^4.4.0 - "@typescript-eslint/scope-manager": 7.16.1 - "@typescript-eslint/types": 7.16.1 - "@typescript-eslint/typescript-estree": 7.16.1 + "@typescript-eslint/scope-manager": 7.17.0 + "@typescript-eslint/types": 7.17.0 + "@typescript-eslint/typescript-estree": 7.17.0 downlevel-dts: "*" jest: 29.7.0 prettier: ^3.2.5 @@ -5998,13 +5998,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@7.16.1, @typescript-eslint/visitor-keys@workspace:packages/visitor-keys": +"@typescript-eslint/visitor-keys@7.17.0, @typescript-eslint/visitor-keys@workspace:packages/visitor-keys": version: 0.0.0-use.local resolution: "@typescript-eslint/visitor-keys@workspace:packages/visitor-keys" dependencies: "@jest/types": 29.6.3 "@types/eslint-visitor-keys": "*" - "@typescript-eslint/types": 7.16.1 + "@typescript-eslint/types": 7.17.0 downlevel-dts: "*" eslint-visitor-keys: ^3.4.3 jest: 29.7.0 @@ -6034,16 +6034,16 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/website-eslint@7.16.1, @typescript-eslint/website-eslint@workspace:packages/website-eslint": +"@typescript-eslint/website-eslint@7.17.0, @typescript-eslint/website-eslint@workspace:packages/website-eslint": version: 0.0.0-use.local resolution: "@typescript-eslint/website-eslint@workspace:packages/website-eslint" dependencies: "@eslint/js": "*" - "@typescript-eslint/eslint-plugin": 7.16.1 - "@typescript-eslint/parser": 7.16.1 - "@typescript-eslint/scope-manager": 7.16.1 - "@typescript-eslint/typescript-estree": 7.16.1 - "@typescript-eslint/visitor-keys": 7.16.1 + "@typescript-eslint/eslint-plugin": 7.17.0 + "@typescript-eslint/parser": 7.17.0 + "@typescript-eslint/scope-manager": 7.17.0 + "@typescript-eslint/typescript-estree": 7.17.0 + "@typescript-eslint/visitor-keys": 7.17.0 esbuild: ~0.23.0 eslint: "*" esquery: "*" @@ -13467,8 +13467,8 @@ __metadata: linkType: hard "knip@npm:^5.9.4": - version: 5.25.1 - resolution: "knip@npm:5.25.1" + version: 5.26.0 + resolution: "knip@npm:5.26.0" dependencies: "@nodelib/fs.walk": 1.2.8 "@snyk/github-codeowners": 1.1.0 @@ -13484,7 +13484,6 @@ __metadata: smol-toml: ^1.1.4 strip-json-comments: 5.0.1 summary: 2.1.0 - tsconfig-paths: ^4.2.0 zod: ^3.22.4 zod-validation-error: ^3.0.3 peerDependencies: @@ -13493,14 +13492,14 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: a8b2955d162668535ba23090042387335bda9b77364e6c86bbf8dcc6ea077cdce02dbe9dc8b46fe707fffc7b1e0babf87552315fa9c011c256c8950a58c9b25a + checksum: 83f66c3077c3aa56142925e2095b22283cac0f0b09703c43973f3940e657156787895960736594e4d2acfe9def310398ae53d3f6e7360833759d4903cad169ea languageName: node linkType: hard -"known-css-properties@npm:^0.31.0": - version: 0.31.0 - resolution: "known-css-properties@npm:0.31.0" - checksum: 814979212974f229f33ece048a54c3da5b689cbf1275fcfda83b9317a3826605c5d85b28ed68d9b62300a8eb9db61708a662ef6ec2ec12029f3139d117ebb2a1 +"known-css-properties@npm:^0.34.0": + version: 0.34.0 + resolution: "known-css-properties@npm:0.34.0" + checksum: 2f1c562767164672442949c44310cf8bbd0cc8a1fbf76b2437a0a2ed364876f40b03d18ffee3de2490e789e916be6c635823e4137fcb3c2f690a96e79bd66a8c languageName: node linkType: hard @@ -16664,14 +16663,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.26, postcss@npm:^8.4.32, postcss@npm:^8.4.33, postcss@npm:^8.4.38": - version: 8.4.38 - resolution: "postcss@npm:8.4.38" +"postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.26, postcss@npm:^8.4.32, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.4.39": + version: 8.4.39 + resolution: "postcss@npm:8.4.39" dependencies: nanoid: ^3.3.7 - picocolors: ^1.0.0 + picocolors: ^1.0.1 source-map-js: ^1.2.0 - checksum: 649f9e60a763ca4b5a7bbec446a069edf07f057f6d780a5a0070576b841538d1ecf7dd888f2fbfd1f76200e26c969e405aeeae66332e6927dbdc8bdcb90b9451 + checksum: 14b130c90f165961772bdaf99c67f907f3d16494adf0868e57ef68baa67e0d1f6762db9d41ab0f4d09bab6fb7888588dba3596afd1a235fd5c2d43fba7006ac6 languageName: node linkType: hard @@ -18862,12 +18861,12 @@ __metadata: linkType: hard "stylelint@npm:^16.3.1": - version: 16.6.1 - resolution: "stylelint@npm:16.6.1" + version: 16.7.0 + resolution: "stylelint@npm:16.7.0" dependencies: - "@csstools/css-parser-algorithms": ^2.6.3 - "@csstools/css-tokenizer": ^2.3.1 - "@csstools/media-query-list-parser": ^2.1.11 + "@csstools/css-parser-algorithms": ^2.7.1 + "@csstools/css-tokenizer": ^2.4.1 + "@csstools/media-query-list-parser": ^2.1.13 "@csstools/selector-specificity": ^3.1.1 "@dual-bundle/import-meta-resolve": ^4.1.0 balanced-match: ^2.0.0 @@ -18875,7 +18874,7 @@ __metadata: cosmiconfig: ^9.0.0 css-functions-list: ^3.2.2 css-tree: ^2.3.1 - debug: ^4.3.4 + debug: ^4.3.5 fast-glob: ^3.3.2 fastest-levenshtein: ^1.0.16 file-entry-cache: ^9.0.0 @@ -18886,13 +18885,13 @@ __metadata: ignore: ^5.3.1 imurmurhash: ^0.1.4 is-plain-object: ^5.0.0 - known-css-properties: ^0.31.0 + known-css-properties: ^0.34.0 mathml-tag-names: ^2.1.3 meow: ^13.2.0 micromatch: ^4.0.7 normalize-path: ^3.0.0 picocolors: ^1.0.1 - postcss: ^8.4.38 + postcss: ^8.4.39 postcss-resolve-nested-selector: ^0.1.1 postcss-safe-parser: ^7.0.0 postcss-selector-parser: ^6.1.0 @@ -18906,7 +18905,7 @@ __metadata: write-file-atomic: ^5.0.1 bin: stylelint: bin/stylelint.mjs - checksum: eae2d11e1b7c1d0b8e242e2ce5f6098cdc303b0210b676dd2692c68c9c9458938e8a7e29af22f8f3f0907874456191c293e6677b55bdeb8eb8c6e5a9e6e34491 + checksum: b6b3b2772abbdbff02ed9357eb355c6b3056c806301fba0aa62a45eebb93d3e87ea969f03cc8f802de3a50b3b3b008de70536cff91edc09287e640960dd8b5d9 languageName: node linkType: hard @@ -19291,7 +19290,7 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^4.1.2, tsconfig-paths@npm:^4.2.0": +"tsconfig-paths@npm:^4.1.2": version: 4.2.0 resolution: "tsconfig-paths@npm:4.2.0" dependencies: @@ -19498,9 +19497,9 @@ __metadata: resolution: "typescript-eslint@workspace:packages/typescript-eslint" dependencies: "@jest/types": 29.6.3 - "@typescript-eslint/eslint-plugin": 7.16.1 - "@typescript-eslint/parser": 7.16.1 - "@typescript-eslint/utils": 7.16.1 + "@typescript-eslint/eslint-plugin": 7.17.0 + "@typescript-eslint/parser": 7.17.0 + "@typescript-eslint/utils": 7.17.0 downlevel-dts: "*" jest: 29.7.0 prettier: ^3.2.5 @@ -20129,8 +20128,8 @@ __metadata: linkType: hard "webpack@npm:^5.88.1, webpack@npm:^5.91.0": - version: 5.92.0 - resolution: "webpack@npm:5.92.0" + version: 5.93.0 + resolution: "webpack@npm:5.93.0" dependencies: "@types/eslint-scope": ^3.7.3 "@types/estree": ^1.0.5 @@ -20161,7 +20160,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: b020102549d2bdbc59902003140808601a4f85800c3efcb8292d4239a71a44786d0b4e2412cfa840a75c2e60276e7e55ea3b77b4e1850a915024cab2a57e90ef + checksum: c93bd73d9e1ab49b07e139582187f1c3760ee2cf0163b6288fab2ae210e39e59240a26284e7e5d29bec851255ef4b43c51642c882fa5a94e16ce7cb906deeb47 languageName: node linkType: hard @@ -20194,14 +20193,14 @@ __metadata: "@types/mdast": ^4.0.3 "@types/react": "*" "@types/unist": ^3.0.2 - "@typescript-eslint/eslint-plugin": 7.16.1 - "@typescript-eslint/parser": 7.16.1 - "@typescript-eslint/rule-schema-to-typescript-types": 7.16.1 - "@typescript-eslint/scope-manager": 7.16.1 - "@typescript-eslint/types": 7.16.1 - "@typescript-eslint/typescript-estree": 7.16.1 - "@typescript-eslint/utils": 7.16.1 - "@typescript-eslint/website-eslint": 7.16.1 + "@typescript-eslint/eslint-plugin": 7.17.0 + "@typescript-eslint/parser": 7.17.0 + "@typescript-eslint/rule-schema-to-typescript-types": 7.17.0 + "@typescript-eslint/scope-manager": 7.17.0 + "@typescript-eslint/types": 7.17.0 + "@typescript-eslint/typescript-estree": 7.17.0 + "@typescript-eslint/utils": 7.17.0 + "@typescript-eslint/website-eslint": 7.17.0 "@uiw/react-shields": 2.0.1 clsx: ^2.1.0 copy-webpack-plugin: ^12.0.0 From c7181b91d2abeec46c35615374528f67f1fbe34a Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Tue, 23 Jul 2024 19:46:10 -0600 Subject: [PATCH 14/19] test(typescript-estree): cleans up PR --- .../src/create-program/useProvidedPrograms.ts | 2 +- .../project-a/src/index.ts | 0 .../project-a/tsconfig.json | 5 --- .../project-a/tsconfig.src.json | 9 ----- .../project-b/src/index.ts | 0 .../project-b/tsconfig.json | 5 --- .../project-b/tsconfig.src.json | 9 ----- .../projectServicesComplex/tsconfig.base.json | 9 ----- .../projectServicesComplex/tsconfig.json | 8 ---- .../tsconfig.overridden.json | 3 -- .../tsconfig.overrides.json | 5 --- .../tests/lib/createProjectService.test.ts | 1 + .../tests/lib/getParsedConfigFile.test.ts | 37 +++++-------------- 13 files changed, 11 insertions(+), 82 deletions(-) delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/src/index.ts delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.json delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/src/index.ts delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.json delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.src.json delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.base.json delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.json delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overridden.json delete mode 100644 packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overrides.json diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index 070a014daf93..d672f07d8ad8 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -5,7 +5,7 @@ import * as ts from 'typescript'; import type { ParseSettings } from '../parseSettings'; import { getParsedConfigFile } from './getParsedConfigFile'; import type { ASTAndDefiniteProgram } from './shared'; -import { CORE_COMPILER_OPTIONS, getAstFromProgram } from './shared'; +import { getAstFromProgram } from './shared'; const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/src/index.ts b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/src/index.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.json deleted file mode 100644 index 144eca86d81d..000000000000 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "include": [], - "references": [{ "path": "./tsconfig.src.json" }] -} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json deleted file mode 100644 index 35eec1467e18..000000000000 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-a/tsconfig.src.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "include": ["./src"], - "compilerOptions": { - "paths": { - "@": ["./project-b/src/index.ts"] - } - } -} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/src/index.ts b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/src/index.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.json deleted file mode 100644 index 144eca86d81d..000000000000 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "include": [], - "references": [{ "path": "./tsconfig.src.json" }] -} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.src.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.src.json deleted file mode 100644 index 35eec1467e18..000000000000 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/project-b/tsconfig.src.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "include": ["./src"], - "compilerOptions": { - "paths": { - "@": ["./project-b/src/index.ts"] - } - } -} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.base.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.base.json deleted file mode 100644 index 1558f0441589..000000000000 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.base.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "paths": { - "@/package-a": ["./project-a/src/index.ts"], - "@/package-b": ["./project-b/src/index.ts"] - } - } -} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.json deleted file mode 100644 index f5573dac6eda..000000000000 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": [], - "references": [ - { "path": "./packages/package-a" }, - { "path": "./packages/package-b" } - ] -} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overridden.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overridden.json deleted file mode 100644 index 588708a2b74e..000000000000 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overridden.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["./tsconfig.base.json", "./tsconfig.overrides.json"] -} diff --git a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overrides.json b/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overrides.json deleted file mode 100644 index 8ad9c98014c7..000000000000 --- a/packages/typescript-estree/tests/fixtures/projectServicesComplex/tsconfig.overrides.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "compilerOptions": { - "strict": false - } -} diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 90efc15f77cd..bc68388f86d3 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -36,6 +36,7 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({ const { createProjectService, + // eslint-disable-next-line @typescript-eslint/no-require-imports } = require('../../src/create-program/createProjectService'); describe('createProjectService', () => { diff --git a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts b/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts index 8f026e6af924..7261ea7420ab 100644 --- a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts +++ b/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts @@ -1,15 +1,9 @@ -import fs from 'node:fs'; import path from 'node:path'; import * as ts from 'typescript'; import { getParsedConfigFile } from '../../src/create-program/getParsedConfigFile'; -const FIXTURES_DIR = path.resolve( - __dirname, - '../fixtures/projectServicesComplex', -); - const mockGetParsedCommandLineOfConfigFile = jest.fn(); const mockTsserver: typeof ts = { @@ -80,28 +74,15 @@ describe('getParsedConfigFile', () => { ); }); - it('uses extended compiler options when parsing a config file', () => { - mockGetParsedCommandLineOfConfigFile.mockImplementation( - ( - ...args: Parameters<(typeof ts)['getParsedCommandLineOfConfigFile']> - ) => { - return ts.getParsedCommandLineOfConfigFile(...args); - }, - ); - const base = JSON.parse( - fs.readFileSync(path.resolve(FIXTURES_DIR, 'tsconfig.base.json'), 'utf8'), - ); - const config = { - options: expect.objectContaining(base.compilerOptions), - raw: JSON.parse( - fs.readFileSync(path.resolve(FIXTURES_DIR, 'tsconfig.json'), 'utf8'), - ), + it('uses compiler options when parsing a config file succeeds', () => { + const parsedConfigFile = { + options: { strict: true }, + raw: { compilerOptions: { strict: true } }, + errors: [], }; - expect( - getParsedConfigFile( - mockTsserver, - path.resolve(FIXTURES_DIR, 'tsconfig.json'), - ), - ).toEqual(expect.objectContaining(config)); + mockGetParsedCommandLineOfConfigFile.mockReturnValue(parsedConfigFile); + expect(getParsedConfigFile(mockTsserver, 'tsconfig.json')).toEqual( + expect.objectContaining(parsedConfigFile), + ); }); }); From 45551dc2e74379e587396cb1d28748dfca395cac Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Tue, 23 Jul 2024 20:23:47 -0600 Subject: [PATCH 15/19] test(typescript-estree): fixes snapshot tests --- .../src/create-program/createProjectService.ts | 3 +-- .../src/create-program/getParsedConfigFile.ts | 2 +- .../typescript-estree/tests/lib/createProjectService.test.ts | 4 ++-- .../typescript-estree/tests/lib/getParsedConfigFile.test.ts | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index dc51c7dfaff2..4fe4032203eb 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -133,14 +133,13 @@ export function createProjectService( path.dirname(options.defaultProject), ); if (typeof configFile === 'string') { - defaultProjectError = configFile; + defaultProjectError = `Could not read default project '${options.defaultProject}': ${configFile}`; } else { service.setCompilerOptionsForInferredProjects( // NOTE: The inferred projects API is not intended for source files when a tsconfig // exists. There is no API that generates an InferredProjectCompilerOptions suggesting // it is meant for hard coded options passed in. Hard casting as a work around. // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 - configFile.options as ts.server.protocol.InferredProjectCompilerOptions, ); } diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts index 449a902b67a2..39e885278a7f 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -47,7 +47,7 @@ function getParsedConfigFile( } if (parsingError !== undefined) { - return `Could not parse config file '${configFile}': ${parsingError}`; + return parsingError; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index bc68388f86d3..8a3a7372b989 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -59,7 +59,7 @@ describe('createProjectService', () => { it('throws an error when options.defaultProject is set and getParsedConfigFile returns an error', () => { mockGetParsedConfigFile.mockReturnValue( - "Could not parse config file './tsconfig.json': ./tsconfig.json(1,1): error TS1234: Oh no!", + './tsconfig.json(1,1): error TS1234: Oh no!', ); expect(() => @@ -71,7 +71,7 @@ describe('createProjectService', () => { undefined, ), ).toThrow( - /Could not parse config file '\.\/tsconfig.json': .+ error TS1234: Oh no!/, + /Could not read default project '\.\/tsconfig.json': .+ error TS1234: Oh no!/, ); }); diff --git a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts b/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts index 7261ea7420ab..91d39fd3a03b 100644 --- a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts +++ b/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts @@ -70,7 +70,7 @@ describe('getParsedConfigFile', () => { ] satisfies ts.Diagnostic[], }); expect(getParsedConfigFile(mockTsserver, './tsconfig.json')).toMatch( - /Could not parse config file '\.\/tsconfig.json': .+ error TS1234: Oh no!/, + /.+ error TS1234: Oh no!/, ); }); From d5f06c5824aa4ef84ad748f532f6a9cb7b097b80 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Wed, 24 Jul 2024 19:09:49 +0000 Subject: [PATCH 16/19] style(typescript-estree): applies suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Brad Zacher Co-authored-by: Josh Goldberg ✨ --- .../src/create-program/getParsedConfigFile.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts index 39e885278a7f..c5ab0aecf6ed 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -23,7 +23,7 @@ function getParsedConfigFile( } const getCurrentDirectory = (): string => - (projectDirectory && path.resolve(projectDirectory)) || process.cwd(); + projectDirectory ? path.resolve(projectDirectory) : process.cwd(); let parsingError: string | undefined; @@ -46,12 +46,8 @@ function getParsedConfigFile( parsingError = formatDiagnostics(parsed.errors); } - if (parsingError !== undefined) { - return parsingError; - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return parsed!; + return parsingError ?? parsed!; function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { return tsserver.formatDiagnostics(diagnostics, { From 1dd15bf412c88e88b03907c313aafe626903ebbd Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 25 Jul 2024 08:32:41 -0600 Subject: [PATCH 17/19] style(typescript-estree): applies manual suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Brad Zacher Co-authored-by: Josh Goldberg ✨ --- .../create-program/createProjectService.ts | 30 ++++++++----------- .../src/create-program/getParsedConfigFile.ts | 17 +++++------ .../src/create-program/useProvidedPrograms.ts | 3 -- .../tests/lib/createProjectService.test.ts | 12 ++++---- .../tests/lib/getParsedConfigFile.test.ts | 28 +++++++++++++++-- 5 files changed, 53 insertions(+), 37 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 4fe4032203eb..aef05cc20ba3 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -124,31 +124,27 @@ export function createProjectService( if (options.defaultProject) { log('Enabling default project: %s', options.defaultProject); - let defaultProjectError: string | undefined; + let configFile: ts.ParsedCommandLine; try { - const configFile = getParsedConfigFile( + configFile = getParsedConfigFile( tsserver, options.defaultProject, path.dirname(options.defaultProject), ); - if (typeof configFile === 'string') { - defaultProjectError = `Could not read default project '${options.defaultProject}': ${configFile}`; - } else { - service.setCompilerOptionsForInferredProjects( - // NOTE: The inferred projects API is not intended for source files when a tsconfig - // exists. There is no API that generates an InferredProjectCompilerOptions suggesting - // it is meant for hard coded options passed in. Hard casting as a work around. - // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 - configFile.options as ts.server.protocol.InferredProjectCompilerOptions, - ); - } } catch (error) { - defaultProjectError = `Could not parse default project '${options.defaultProject}': ${(error as Error).message}`; - } - if (defaultProjectError !== undefined) { - throw new Error(defaultProjectError); + throw new Error( + `Could not read default project '${options.defaultProject}': ${(error as Error).message}`, + ); } + + service.setCompilerOptionsForInferredProjects( + // NOTE: The inferred projects API is not intended for source files when a tsconfig + // exists. There is no API that generates an InferredProjectCompilerOptions suggesting + // it is meant for hard coded options passed in. Hard casting as a work around. + // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 + configFile.options as ts.server.protocol.InferredProjectCompilerOptions, + ); } return { diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts index c5ab0aecf6ed..944920450e4e 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -14,7 +14,7 @@ function getParsedConfigFile( tsserver: typeof ts, configFile: string, projectDirectory?: string, -): ts.ParsedCommandLine | string { +): ts.ParsedCommandLine { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (tsserver.sys === undefined) { throw new Error( @@ -22,17 +22,12 @@ function getParsedConfigFile( ); } - const getCurrentDirectory = (): string => - projectDirectory ? path.resolve(projectDirectory) : process.cwd(); - - let parsingError: string | undefined; - const parsed = tsserver.getParsedCommandLineOfConfigFile( configFile, CORE_COMPILER_OPTIONS, { onUnRecoverableConfigFileDiagnostic: diag => { - parsingError = formatDiagnostics([diag]); // ensures that `parsed` is defined. + throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined. }, fileExists: fs.existsSync, getCurrentDirectory, @@ -43,11 +38,15 @@ function getParsedConfigFile( ); if (parsed?.errors.length) { - parsingError = formatDiagnostics(parsed.errors); + throw new Error(formatDiagnostics(parsed.errors)); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return parsingError ?? parsed!; + return parsed!; + + function getCurrentDirectory() { + return projectDirectory ? path.resolve(projectDirectory) : process.cwd(); + } function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined { return tsserver.formatDiagnostics(diagnostics, { diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index d672f07d8ad8..e4e03eb2d0d4 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -61,9 +61,6 @@ function createProgramFromConfigFile( projectDirectory?: string, ): ts.Program { const parsed = getParsedConfigFile(ts, configFile, projectDirectory); - if (typeof parsed === 'string') { - throw new Error(parsed); - } const host = ts.createCompilerHost(parsed.options, true); return ts.createProgram(parsed.fileNames, parsed.options, host); } diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 8a3a7372b989..cbf3d72dab9c 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -57,10 +57,10 @@ describe('createProjectService', () => { expect(settings.allowDefaultProject).toBeUndefined(); }); - it('throws an error when options.defaultProject is set and getParsedConfigFile returns an error', () => { - mockGetParsedConfigFile.mockReturnValue( - './tsconfig.json(1,1): error TS1234: Oh no!', - ); + it('throws an error when options.defaultProject is set and getParsedConfigFile throws a diagnostic error', () => { + mockGetParsedConfigFile.mockImplementation(() => { + throw new Error('./tsconfig.json(1,1): error TS1234: Oh no!'); + }); expect(() => createProjectService( @@ -75,7 +75,7 @@ describe('createProjectService', () => { ); }); - it('throws an error when options.defaultProject is set and getParsedConfigFile throws an error', () => { + it('throws an error when options.defaultProject is set and getParsedConfigFile throws an environment error', () => { mockGetParsedConfigFile.mockImplementation(() => { throw new Error( '`getParsedConfigFile` is only supported in a Node-like environment.', @@ -91,7 +91,7 @@ describe('createProjectService', () => { undefined, ), ).toThrow( - "Could not parse default project './tsconfig.json': `getParsedConfigFile` is only supported in a Node-like environment.", + "Could not read default project './tsconfig.json': `getParsedConfigFile` is only supported in a Node-like environment.", ); }); diff --git a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts b/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts index 91d39fd3a03b..a1aa1a2c04e4 100644 --- a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts +++ b/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts @@ -54,7 +54,7 @@ describe('getParsedConfigFile', () => { expect(host.getCurrentDirectory()).toBe(__dirname); }); - it('returns a diagnostic string when parsing a config file fails', () => { + it('throws a diagnostic error when getParsedCommandLineOfConfigFile returns an error', () => { mockGetParsedCommandLineOfConfigFile.mockReturnValue({ errors: [ { @@ -69,7 +69,31 @@ describe('getParsedConfigFile', () => { }, ] satisfies ts.Diagnostic[], }); - expect(getParsedConfigFile(mockTsserver, './tsconfig.json')).toMatch( + expect(() => getParsedConfigFile(mockTsserver, './tsconfig.json')).toThrow( + /.+ error TS1234: Oh no!/, + ); + }); + + it('throws a diagnostic error when getParsedCommandLineOfConfigFile throws an error', () => { + mockGetParsedCommandLineOfConfigFile.mockImplementation( + ( + ...[_configFileName, _optionsToExtend, host]: Parameters< + typeof ts.getParsedCommandLineOfConfigFile + > + ) => { + return host.onUnRecoverableConfigFileDiagnostic({ + category: ts.DiagnosticCategory.Error, + code: 1234, + file: ts.createSourceFile('./tsconfig.json', '', { + languageVersion: ts.ScriptTarget.Latest, + }), + start: 0, + length: 0, + messageText: 'Oh no!', + } satisfies ts.Diagnostic); + }, + ); + expect(() => getParsedConfigFile(mockTsserver, './tsconfig.json')).toThrow( /.+ error TS1234: Oh no!/, ); }); From ac8c9bcc59024bbf71d6a6a864b6cceca2655dd7 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 25 Jul 2024 10:15:13 -0600 Subject: [PATCH 18/19] fixes(typescript-estree): fixes linting errors --- .../typescript-estree/src/create-program/getParsedConfigFile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts index 944920450e4e..703b8500f070 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/typescript-estree/src/create-program/getParsedConfigFile.ts @@ -44,7 +44,7 @@ function getParsedConfigFile( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return parsed!; - function getCurrentDirectory() { + function getCurrentDirectory(): string { return projectDirectory ? path.resolve(projectDirectory) : process.cwd(); } From 4478aea5bac4b46f7581a3863d38c3a4144720b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Sun, 28 Jul 2024 07:42:14 -0400 Subject: [PATCH 19/19] Update packages/typescript-estree/src/create-program/createProjectService.ts --- .../src/create-program/createProjectService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index aef05cc20ba3..edf5f9f9167f 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -141,7 +141,7 @@ export function createProjectService( service.setCompilerOptionsForInferredProjects( // NOTE: The inferred projects API is not intended for source files when a tsconfig // exists. There is no API that generates an InferredProjectCompilerOptions suggesting - // it is meant for hard coded options passed in. Hard casting as a work around. + // it is meant for hard coded options passed in. Hard asserting as a work around. // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 configFile.options as ts.server.protocol.InferredProjectCompilerOptions, );