Skip to content

Commit 97d7398

Browse files
committed
fix gts type aware by emulating them to ts files
set our own ts.sys with ts.setSys typescript-eslint has handling for a lot of scenarios for file changes and project changes etc use mts extension to keep same offsets we also sync the mts with the gts files
1 parent a08f0ab commit 97d7398

File tree

10 files changed

+883
-522
lines changed

10 files changed

+883
-522
lines changed

lib/parsers/gjs-gts-parser.js

Lines changed: 21 additions & 517 deletions
Large diffs are not rendered by default.

lib/parsers/transform.js

Lines changed: 519 additions & 0 deletions
Large diffs are not rendered by default.

lib/parsers/ts-utils.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
const fs = require('node:fs');
2+
const { transformForLint } = require('./transform');
3+
const babel = require('@babel/core');
4+
const { replaceRange } = require('./transform');
5+
6+
let patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser;
7+
8+
try {
9+
const ts = require('typescript');
10+
typescriptParser = require('@typescript-eslint/parser');
11+
patchTs = function patchTs() {
12+
const sys = { ...ts.sys };
13+
const newSys = {
14+
...ts.sys,
15+
readDirectory(...args) {
16+
const results = sys.readDirectory.call(this, ...args);
17+
return [
18+
...results,
19+
...results.filter((x) => x.endsWith('.gts')).map((f) => f.replace(/\.gts$/, '.mts')),
20+
];
21+
},
22+
fileExists(fileName) {
23+
return fs.existsSync(fileName.replace(/\.mts$/, '.gts')) || fs.existsSync(fileName);
24+
},
25+
readFile(fname) {
26+
let fileName = fname;
27+
let content = '';
28+
try {
29+
content = fs.readFileSync(fileName).toString();
30+
} catch {
31+
fileName = fileName.replace(/\.mts$/, '.gts');
32+
content = fs.readFileSync(fileName).toString();
33+
}
34+
if (fileName.endsWith('.gts')) {
35+
content = transformForLint(content).output;
36+
}
37+
if (
38+
(!fileName.endsWith('.d.ts') && fileName.endsWith('.ts')) ||
39+
fileName.endsWith('.gts')
40+
) {
41+
content = replaceExtensions(content);
42+
}
43+
return content;
44+
},
45+
};
46+
ts.setSys(newSys);
47+
};
48+
49+
replaceExtensions = function replaceExtensions(code) {
50+
let jsCode = code;
51+
const babelParseResult = babel.parse(jsCode, {
52+
parserOpts: { ranges: true, plugins: ['typescript'] },
53+
});
54+
const length = jsCode.length;
55+
for (const b of babelParseResult.program.body) {
56+
if (b.type === 'ImportDeclaration' && b.source.value.endsWith('.gts')) {
57+
const value = b.source.value.replace(/\.gts$/, '.mts');
58+
const strWrapper = jsCode[b.source.start];
59+
jsCode = replaceRange(
60+
jsCode,
61+
b.source.start,
62+
b.source.end,
63+
strWrapper + value + strWrapper
64+
);
65+
}
66+
}
67+
if (length !== jsCode.length) {
68+
throw new Error('bad replacement');
69+
}
70+
return jsCode;
71+
};
72+
73+
/**
74+
*
75+
* @param program {ts.Program}
76+
*/
77+
syncMtsGtsSourceFiles = function syncMtsGtsSourceFiles(program) {
78+
const sourceFiles = program.getSourceFiles();
79+
for (const sourceFile of sourceFiles) {
80+
// check for deleted gts files, need to remove mts as well
81+
if (sourceFile.path.endsWith('.mts') && sourceFile.isVirtualGts) {
82+
const gtsFile = program.getSourceFile(sourceFile.path.replace(/\.mts$/, '.gts'));
83+
if (!gtsFile) {
84+
sourceFile.version = null;
85+
}
86+
}
87+
if (sourceFile.path.endsWith('.gts')) {
88+
/**
89+
* @type {ts.SourceFile}
90+
*/
91+
const mtsSourceFile = program.getSourceFile(sourceFile.path.replace(/\.gts$/, '.mts'));
92+
if (mtsSourceFile) {
93+
const keep = {
94+
fileName: mtsSourceFile.fileName,
95+
path: mtsSourceFile.path,
96+
originalFileName: mtsSourceFile.originalFileName,
97+
resolvedPath: mtsSourceFile.resolvedPath,
98+
};
99+
Object.assign(mtsSourceFile, sourceFile, keep);
100+
mtsSourceFile.isVirtualGts = true;
101+
}
102+
}
103+
}
104+
};
105+
} catch {
106+
// typescript not available
107+
patchTs = () => null;
108+
replaceExtensions = (code) => code;
109+
syncMtsGtsSourceFiles = () => null;
110+
}
111+
112+
module.exports = {
113+
patchTs,
114+
replaceExtensions,
115+
syncMtsGtsSourceFiles,
116+
typescriptParser,
117+
};

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
]
6969
},
7070
"dependencies": {
71+
"@babel/core": "^7.23.3",
7172
"@babel/eslint-parser": "^7.22.15",
7273
"@ember-data/rfc395-data": "^0.0.4",
7374
"@glimmer/syntax": "^0.85.12",
@@ -116,7 +117,13 @@
116117
"typescript": "^5.2.2"
117118
},
118119
"peerDependencies": {
119-
"eslint": ">= 8"
120+
"eslint": ">= 8",
121+
"typescript": "*"
122+
},
123+
"peerDependenciesMeta": {
124+
"typescript": {
125+
"optional": true
126+
}
120127
},
121128
"engines": {
122129
"node": "18.* || 20.* || >= 21"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const fortyTwoFromGTS = '42';
2+
3+
<template>
4+
{{fortyTwoFromGTS}}
5+
</template>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const fortyTwoFromTS = '42';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { fortyTwoFromGTS } from './bar.gts';
2+
import { fortyTwoFromTS } from './baz.ts';
3+
4+
export const fortyTwoLocal = '42';
5+
6+
const helloWorldFromTS = fortyTwoFromTS[0] === '4' ? 'hello' : 'world';
7+
const helloWorldFromGTS = fortyTwoFromGTS[0] === '4' ? 'hello' : 'world';
8+
const helloWorld = fortyTwoLocal[0] === '4' ? 'hello' : 'world';
9+
//
10+
<template>
11+
{{helloWorldFromGTS}}
12+
{{helloWorldFromTS}}
13+
{{helloWorld}}
14+
</template>

tests/lib/rules-preprocessor/gjs-gts-parser-test.js

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
const { ESLint } = require('eslint');
1111
const plugin = require('../../../lib');
12+
const { writeFileSync, readFileSync } = require('node:fs');
13+
const { join } = require('node:path');
1214

1315
const gjsGtsParser = require.resolve('../../../lib/parsers/gjs-gts-parser');
1416

@@ -388,18 +390,28 @@ const invalid = [
388390
code: `
389391
import Component from '@glimmer/component';
390392
393+
const foo: any = '';
394+
391395
export default class MyComponent extends Component {
392396
foo = 'bar';
393397
394398
<template>
395399
<div></div>${' '}
400+
{{foo}}
396401
</template>
397402
}`,
398403
errors: [
404+
{
405+
message: 'Unexpected any. Specify a different type.',
406+
line: 4,
407+
endLine: 4,
408+
column: 18,
409+
endColumn: 21,
410+
},
399411
{
400412
message: 'Trailing spaces not allowed.',
401-
line: 8,
402-
endLine: 8,
413+
line: 10,
414+
endLine: 10,
403415
column: 22,
404416
endColumn: 24,
405417
},
@@ -765,4 +777,113 @@ describe('multiple tokens in same file', () => {
765777
expect(resultErrors[2].message).toBe("'bar' is not defined.");
766778
expect(resultErrors[2].line).toBe(17);
767779
});
780+
781+
it('lints while being type aware', async () => {
782+
const eslint = new ESLint({
783+
ignore: false,
784+
useEslintrc: false,
785+
plugins: { ember: plugin },
786+
overrideConfig: {
787+
root: true,
788+
env: {
789+
browser: true,
790+
},
791+
plugins: ['ember'],
792+
extends: ['plugin:ember/recommended'],
793+
overrides: [
794+
{
795+
files: ['**/*.gts'],
796+
parser: 'eslint-plugin-ember/gjs-gts-parser',
797+
parserOptions: {
798+
project: './tsconfig.eslint.json',
799+
tsconfigRootDir: __dirname,
800+
extraFileExtensions: ['.gts'],
801+
},
802+
extends: [
803+
'plugin:@typescript-eslint/recommended-requiring-type-checking',
804+
'plugin:ember/recommended',
805+
],
806+
rules: {
807+
'no-trailing-spaces': 'error',
808+
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
809+
},
810+
},
811+
{
812+
files: ['**/*.ts'],
813+
parser: '@typescript-eslint/parser',
814+
parserOptions: {
815+
project: './tsconfig.eslint.json',
816+
tsconfigRootDir: __dirname,
817+
extraFileExtensions: ['.gts'],
818+
},
819+
extends: [
820+
'plugin:@typescript-eslint/recommended-requiring-type-checking',
821+
'plugin:ember/recommended',
822+
],
823+
rules: {
824+
'no-trailing-spaces': 'error',
825+
},
826+
},
827+
],
828+
rules: {
829+
quotes: ['error', 'single'],
830+
semi: ['error', 'always'],
831+
'object-curly-spacing': ['error', 'always'],
832+
'lines-between-class-members': 'error',
833+
'no-undef': 'error',
834+
'no-unused-vars': 'error',
835+
'ember/no-get': 'off',
836+
'ember/no-array-prototype-extensions': 'error',
837+
'ember/no-unused-services': 'error',
838+
},
839+
},
840+
});
841+
842+
let results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);
843+
844+
let resultErrors = results.flatMap((result) => result.messages);
845+
expect(resultErrors).toHaveLength(3);
846+
847+
expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
848+
expect(resultErrors[0].line).toBe(6);
849+
850+
expect(resultErrors[1].line).toBe(7);
851+
expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
852+
853+
expect(resultErrors[2].line).toBe(8);
854+
expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead.");
855+
856+
const filePath = join(__dirname, 'ember_ts', 'bar.gts');
857+
const content = readFileSync(filePath).toString();
858+
try {
859+
writeFileSync(filePath, content.replace("'42'", '42'));
860+
861+
results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);
862+
863+
resultErrors = results.flatMap((result) => result.messages);
864+
expect(resultErrors).toHaveLength(2);
865+
866+
expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
867+
expect(resultErrors[0].line).toBe(6);
868+
869+
expect(resultErrors[1].line).toBe(8);
870+
expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
871+
} finally {
872+
writeFileSync(filePath, content);
873+
}
874+
875+
results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);
876+
877+
resultErrors = results.flatMap((result) => result.messages);
878+
expect(resultErrors).toHaveLength(3);
879+
880+
expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
881+
expect(resultErrors[0].line).toBe(6);
882+
883+
expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
884+
expect(resultErrors[1].line).toBe(7);
885+
886+
expect(resultErrors[2].line).toBe(8);
887+
expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead.");
888+
});
768889
});

tests/lib/rules-preprocessor/tsconfig.eslint.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"strictNullChecks": true
66
},
77
"include": [
8-
"*"
9-
]
8+
"**/*.ts",
9+
"**/*.gts"
10+
],
1011
}

0 commit comments

Comments
 (0)