Skip to content

Commit 37fb402

Browse files
initial commit
0 parents  commit 37fb402

17 files changed

+2200
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.tsbuildinfo/
2+
node_modules/

README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# example-typescript-eslint-bug
2+
3+
This repo demonstrates two bugs with `typescript-eslint` using `EXPERIMENTAL_useProjectService`.
4+
5+
1. `extends` is not used in the default project's `tsconfig.json`.
6+
2. `parserOptions.extraFileExtensions` is not working as expected to setup
7+
`tsserver` for type checking using project references.
8+
9+
pnpm is required to test the patch in this repo which contains a (non-rigorous)
10+
fix to resolve both issues.
11+
12+
## Expected Errors With Patch
13+
14+
Using the patch, these errors are expected and correctly reported.
15+
16+
```sh
17+
pnpm i
18+
pnpm eslint .
19+
20+
# <REDACTED>/packages/package-a/src/test-component.vue
21+
# 8:7 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
22+
# 8:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
23+
#
24+
# <REDACTED>/packages/package-a/src/test.ts
25+
# 7:14 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
26+
# 7:41 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
27+
```
28+
29+
## Errors Without Patch
30+
31+
These errors are not expected using `typescript-eslint` as is and reveals the extends
32+
in `tsconfig.json` is not followed.
33+
34+
```sh
35+
# remove from root package.json
36+
#
37+
# "patchedDependencies": {
38+
# "@typescript-eslint/[email protected]": "patches/@[email protected]"
39+
# },
40+
41+
pnpm i
42+
pnpm eslint .
43+
44+
# <REDACTED>/packages/package-a/src/test-component.vue
45+
# 0:0 error Parsing error: Cannot read properties of undefined (reading 'target')
46+
#
47+
# <REDACTED>/packages/package-a/src/test.ts
48+
# 0:0 error Parsing error: Cannot read properties of undefined (reading 'target')
49+
```
50+
51+
## Errors Without Patch With Workaround
52+
53+
This attempts to work around the issue by using a project-level `tsconfig.eslint.json`
54+
that does not extend.
55+
56+
It reveals a new issue on line 4 where `test-component.vue` is not associated
57+
with `packages/package-a/src/tsconfig.src.json` by following the project references
58+
which is expected to work with `extraFileExtensions`.
59+
60+
```sh
61+
# remove from root package.json (if not already done)
62+
#
63+
# "patchedDependencies": {
64+
# "@typescript-eslint/[email protected]": "patches/@[email protected]"
65+
# },
66+
67+
pnpm eslint -c eslint.config2.js .
68+
69+
# <REDACTED>/packages/package-a/src/test-component.vue
70+
# 4:7 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
71+
# 8:7 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
72+
# 8:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
73+
#
74+
# <REDACTED>/packages/package-a/src/test.ts
75+
# 7:14 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
76+
# 7:41 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
77+
```

eslint.config.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import tseslint from 'typescript-eslint';
2+
import vueParser from 'vue-eslint-parser';
3+
import vuePlugin from 'eslint-plugin-vue';
4+
5+
const overrides = {
6+
files: [
7+
"packages/*/src/**/*.{ts,vue}",
8+
],
9+
languageOptions: {
10+
parser: vueParser,
11+
parserOptions: {
12+
parser: tseslint.parser,
13+
ecmaVersion: 'latest',
14+
sourceType: 'module',
15+
warnOnUnsupportedTypeScriptVersion: false,
16+
extraFileExtensions: ['.vue'],
17+
tsconfigRootDir: import.meta.dirname,
18+
EXPERIMENTAL_useProjectService: {
19+
defaultProject: './tsconfig.json'
20+
}
21+
}
22+
},
23+
plugins: {
24+
vue: vuePlugin,
25+
"@typescript-eslint": tseslint.plugin,
26+
},
27+
settings: {
28+
"import/parsers": {"@typescript-eslint/parser": [".ts"]},
29+
"import/resolver": {
30+
"typescript": {"alwaysTryTypes": true},
31+
}
32+
}};
33+
34+
export default [
35+
...vuePlugin.configs['flat/recommended'].map(config => ({...config, ...overrides})),
36+
...tseslint.configs.recommended.map(config => ({...config, ...overrides})),
37+
...tseslint.configs.recommendedTypeChecked.map(config => ({...config, ...overrides})),
38+
...tseslint.configs.stylisticTypeChecked.map(config => ({...config, ...overrides}))
39+
]

eslint.config2.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import tseslint from 'typescript-eslint';
2+
import vueParser from 'vue-eslint-parser';
3+
import vuePlugin from 'eslint-plugin-vue';
4+
5+
const overrides = {
6+
files: [
7+
"packages/*/src/**/*.{ts,vue}",
8+
],
9+
languageOptions: {
10+
parser: vueParser,
11+
parserOptions: {
12+
parser: tseslint.parser,
13+
ecmaVersion: 'latest',
14+
sourceType: 'module',
15+
warnOnUnsupportedTypeScriptVersion: false,
16+
extraFileExtensions: ['.vue'],
17+
tsconfigRootDir: import.meta.dirname,
18+
EXPERIMENTAL_useProjectService: {
19+
defaultProject: './tsconfig.eslint.json'
20+
}
21+
}
22+
},
23+
plugins: {
24+
vue: vuePlugin,
25+
"@typescript-eslint": tseslint.plugin,
26+
},
27+
settings: {
28+
"import/parsers": {"@typescript-eslint/parser": [".ts"]},
29+
"import/resolver": {
30+
"typescript": {"alwaysTryTypes": true},
31+
}
32+
}};
33+
34+
export default [
35+
...vuePlugin.configs['flat/recommended'].map(config => ({...config, ...overrides})),
36+
...tseslint.configs.recommended.map(config => ({...config, ...overrides})),
37+
...tseslint.configs.recommendedTypeChecked.map(config => ({...config, ...overrides})),
38+
...tseslint.configs.stylisticTypeChecked.map(config => ({...config, ...overrides}))
39+
]

package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "example-typescript-eslint-bug",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"workspaces": [
7+
"packages/*"
8+
],
9+
"pnpm": {
10+
"patchedDependencies": {
11+
"@typescript-eslint/[email protected]": "patches/@[email protected]"
12+
},
13+
"overrides": {
14+
"@typescript-eslint/eslint-plugin": "$typescript-eslint",
15+
"@typescript-eslint/parser": "$typescript-eslint",
16+
"@typescript-eslint/scope-manager": "$typescript-eslint",
17+
"@typescript-eslint/type-utils": "$typescript-eslint",
18+
"@typescript-eslint/types": "$typescript-eslint",
19+
"@typescript-eslint/typescript-estree": "$typescript-eslint",
20+
"@typescript-eslint/utils": "$typescript-eslint",
21+
"@typescript-eslint/sitor-keys": "$typescript-eslint"
22+
}
23+
},
24+
"devDependencies": {
25+
"eslint": "^8.57.0",
26+
"eslint-plugin-vue": "^9.26.0",
27+
"typescript": "^5.4.5",
28+
"typescript-eslint": "^7.11.0",
29+
"vue-eslint-parser": "^9.4.3",
30+
"vue-tsc": "^2.0.19"
31+
}
32+
}

packages/package-a/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "package-a",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"dependencies": {
7+
"@types/jsdom": "^21.1.6",
8+
"@types/node": "^20.11.10",
9+
"vite": "^5.0.11",
10+
"vue": "^3.4.15"
11+
}
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import icon from './test.png';
3+
4+
const shouldPass = icon;
5+
6+
const shouldFailTsc: string = icon as number;
7+
8+
const shouldFailEslint = icon as any;
9+
10+
</script>
11+
12+
<template>
13+
<v-img
14+
:src="shouldPass"
15+
/>
16+
<v-img
17+
:src="shouldFailTsc"
18+
/>
19+
<v-img
20+
:src="shouldFailEslint"
21+
/>
22+
</template>

packages/package-a/src/test.png

Loading

packages/package-a/src/test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import icon from './test.png';
2+
3+
export const shouldPass = icon;
4+
5+
export const shouldFailTsc: string = icon as number;
6+
7+
export const shouldFailEslint = icon as any;

packages/package-a/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"include": [],
4+
"references": [
5+
{
6+
"path": "./tsconfig.src.json"
7+
}
8+
]
9+
}

packages/package-a/tsconfig.src.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"include": ["./src"],
4+
"references": [],
5+
"compilerOptions": {
6+
"jsx": "preserve",
7+
"jsxImportSource": "vue",
8+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
9+
"noEmit": true,
10+
"tsBuildInfoFile": "./.tsbuildinfo/src.tsbuildinfo",
11+
"types": ["jsdom", "vite/client"]
12+
}
13+
}

patches/@[email protected]

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
diff --git a/dist/create-program/createProjectService.js b/dist/create-program/createProjectService.js
2+
index cf4185511027b727ac8f53dc2504df9abf6d657f..05094e87c65f2c1ec6603a3efaafbb1be515eeca 100644
3+
--- a/dist/create-program/createProjectService.js
4+
+++ b/dist/create-program/createProjectService.js
5+
@@ -12,12 +12,14 @@ const doNothing = () => { };
6+
const createStubFileWatcher = () => ({
7+
close: doNothing,
8+
});
9+
-function createProjectService(optionsRaw, jsDocParsingMode) {
10+
+function createProjectService(optionsRaw, jsDocParsingMode, extraFileExtensions) {
11+
const options = typeof optionsRaw === 'object' ? optionsRaw : {};
12+
(0, validateDefaultProjectForFilesGlob_1.validateDefaultProjectForFilesGlob)(options);
13+
// We import this lazily to avoid its cost for users who don't use the service
14+
// TODO: Once we drop support for TS<5.3 we can import from "typescript" directly
15+
+ const fs = require('fs');
16+
const tsserver = require('typescript/lib/tsserverlibrary');
17+
+ const { CORE_COMPILER_OPTIONS } = require('./shared');
18+
// TODO: see getWatchProgramsForProjects
19+
// We don't watch the disk, we just refer to these when ESLint calls us
20+
// there's a whole separate update pass in maybeInvalidateProgram at the bottom of getWatchProgramsForProjects
21+
@@ -50,22 +52,46 @@ function createProjectService(optionsRaw, jsDocParsingMode) {
22+
session: undefined,
23+
jsDocParsingMode,
24+
});
25+
+ service.setHostConfiguration({
26+
+ extraFileExtensions: extraFileExtensions.map(
27+
+ (extension) => ({
28+
+ extension,
29+
+ isMixedContent: false,
30+
+ scriptKind: tsserver.ScriptKind.Deferred
31+
+ })
32+
+ )
33+
+ })
34+
if (options.defaultProject) {
35+
let configRead;
36+
try {
37+
- configRead = tsserver.readConfigFile(options.defaultProject, system.readFile);
38+
+ // https://github.com/typescript-eslint/typescript-eslint/blob/5ca7f6e563779157cac1ac1592e2f1b82068715a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts#L66
39+
+ configRead = tsserver.getParsedCommandLineOfConfigFile(
40+
+ options.defaultProject,
41+
+ CORE_COMPILER_OPTIONS,
42+
+ {
43+
+ onUnRecoverableConfigFileDiagnostic: diag => {
44+
+ throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined.
45+
+ },
46+
+ fileExists: fs.existsSync,
47+
+ getCurrentDirectory: () =>
48+
+ system.getCurrentDirectory(),
49+
+ readDirectory: system.readDirectory,
50+
+ readFile: file => fs.readFileSync(file, 'utf-8'),
51+
+ useCaseSensitiveFileNames: system.useCaseSensitiveFileNames,
52+
+ },
53+
+ );
54+
}
55+
catch (error) {
56+
throw new Error(`Could not parse default project '${options.defaultProject}': ${error.message}`);
57+
}
58+
- if (configRead.error) {
59+
- throw new Error(`Could not read default project '${options.defaultProject}': ${tsserver.formatDiagnostic(configRead.error, {
60+
- getCurrentDirectory: system.getCurrentDirectory,
61+
- getCanonicalFileName: fileName => fileName,
62+
- getNewLine: () => node_os_1.default.EOL,
63+
- })}`);
64+
- }
65+
- service.setCompilerOptionsForInferredProjects(configRead.config.compilerOptions);
66+
+ // if (configRead.error) {
67+
+ // throw new Error(`Could not read default project '${options.defaultProject}': ${tsserver.formatDiagnostic(configRead.error, {
68+
+ // getCurrentDirectory: system.getCurrentDirectory,
69+
+ // getCanonicalFileName: fileName => fileName,
70+
+ // getNewLine: () => node_os_1.default.EOL,
71+
+ // })}`);
72+
+ // }
73+
+ service.setCompilerOptionsForInferredProjects(configRead.options);
74+
}
75+
return {
76+
allowDefaultProjectForFiles: options.allowDefaultProjectForFiles,
77+
diff --git a/dist/parseSettings/createParseSettings.js b/dist/parseSettings/createParseSettings.js
78+
index 9aaf36f12699fd92a699b8704fc8574a8d6688fa..f3ee1b4f971d653bb3555ff9b43de696bf25c68b 100644
79+
--- a/dist/parseSettings/createParseSettings.js
80+
+++ b/dist/parseSettings/createParseSettings.js
81+
@@ -76,7 +76,7 @@ function createParseSettings(code, options = {}) {
82+
codeFullText,
83+
comment: options.comment === true,
84+
comments: [],
85+
- DEPRECATED__createDefaultProgram:
86+
+ DEPRECATED__createDefaultProgram:
87+
// eslint-disable-next-line deprecation/deprecation -- will be cleaned up with the next major
88+
options.DEPRECATED__createDefaultProgram === true,
89+
debugLevel: options.debugLevel === true
90+
@@ -90,7 +90,7 @@ function createParseSettings(code, options = {}) {
91+
(options.project &&
92+
options.EXPERIMENTAL_useProjectService !== false &&
93+
process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true')
94+
- ? (TSSERVER_PROJECT_SERVICE ??= (0, createProjectService_1.createProjectService)(options.EXPERIMENTAL_useProjectService, jsDocParsingMode))
95+
+ ? (TSSERVER_PROJECT_SERVICE ??= (0, createProjectService_1.createProjectService)(options.EXPERIMENTAL_useProjectService, jsDocParsingMode, options.extraFileExtensions))
96+
: undefined,
97+
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true,
98+
extraFileExtensions: Array.isArray(options.extraFileExtensions) &&

0 commit comments

Comments
 (0)