Skip to content

Commit

Permalink
fix gts type aware by emulating them to ts files
Browse files Browse the repository at this point in the history
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
  • Loading branch information
patricklx committed Dec 12, 2023
1 parent a08f0ab commit 77fe5c8
Show file tree
Hide file tree
Showing 10 changed files with 903 additions and 522 deletions.
538 changes: 21 additions & 517 deletions lib/parsers/gjs-gts-parser.js

Large diffs are not rendered by default.

519 changes: 519 additions & 0 deletions lib/parsers/transform.js

Large diffs are not rendered by default.

117 changes: 117 additions & 0 deletions lib/parsers/ts-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const fs = require('node:fs');
const { transformForLint } = require('./transform');
const babel = require('@babel/core');
const { replaceRange } = require('./transform');

let patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser;

try {
const ts = require('typescript');
typescriptParser = require('@typescript-eslint/parser');
patchTs = function patchTs() {
const sys = { ...ts.sys };
const newSys = {
...ts.sys,
readDirectory(...args) {
const results = sys.readDirectory.call(this, ...args);
return [
...results,
...results.filter((x) => x.endsWith('.gts')).map((f) => f.replace(/\.gts$/, '.mts')),
];
},
fileExists(fileName) {
return fs.existsSync(fileName.replace(/\.mts$/, '.gts')) || fs.existsSync(fileName);
},
readFile(fname) {
let fileName = fname;
let content = '';
try {
content = fs.readFileSync(fileName).toString();
} catch {
fileName = fileName.replace(/\.mts$/, '.gts');
content = fs.readFileSync(fileName).toString();
}
if (fileName.endsWith('.gts')) {
content = transformForLint(content).output;
}
if (
(!fileName.endsWith('.d.ts') && fileName.endsWith('.ts')) ||
fileName.endsWith('.gts')
) {
content = replaceExtensions(content);
}
return content;
},
};
ts.setSys(newSys);
};

replaceExtensions = function replaceExtensions(code) {
let jsCode = code;
const babelParseResult = babel.parse(jsCode, {
parserOpts: { ranges: true, plugins: ['typescript'] },
});
const length = jsCode.length;
for (const b of babelParseResult.program.body) {
if (b.type === 'ImportDeclaration' && b.source.value.endsWith('.gts')) {
const value = b.source.value.replace(/\.gts$/, '.mts');
const strWrapper = jsCode[b.source.start];
jsCode = replaceRange(
jsCode,
b.source.start,
b.source.end,
strWrapper + value + strWrapper
);
}
}
if (length !== jsCode.length) {
throw new Error('bad replacement');
}
return jsCode;
};

/**
*
* @param program {ts.Program}
*/
syncMtsGtsSourceFiles = function syncMtsGtsSourceFiles(program) {
const sourceFiles = program.getSourceFiles();
for (const sourceFile of sourceFiles) {
// check for deleted gts files, need to remove mts as well
if (sourceFile.path.endsWith('.mts') && sourceFile.isVirtualGts) {
const gtsFile = program.getSourceFile(sourceFile.path.replace(/\.mts$/, '.gts'));
if (!gtsFile) {
sourceFile.version = null;
}
}
if (sourceFile.path.endsWith('.gts')) {
/**
* @type {ts.SourceFile}
*/
const mtsSourceFile = program.getSourceFile(sourceFile.path.replace(/\.gts$/, '.mts'));
if (mtsSourceFile) {
const keep = {
fileName: mtsSourceFile.fileName,
path: mtsSourceFile.path,
originalFileName: mtsSourceFile.originalFileName,
resolvedPath: mtsSourceFile.resolvedPath,
};
Object.assign(mtsSourceFile, sourceFile, keep);
mtsSourceFile.isVirtualGts = true;
}
}
}
};
} catch {
// typescript not available
patchTs = () => null;
replaceExtensions = (code) => code;
syncMtsGtsSourceFiles = () => null;
}

module.exports = {
patchTs,
replaceExtensions,
syncMtsGtsSourceFiles,
typescriptParser

Check failure on line 116 in lib/parsers/ts-utils.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 18.x)

Insert `,`

Check failure on line 116 in lib/parsers/ts-utils.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 20.x)

Insert `,`

Check failure on line 116 in lib/parsers/ts-utils.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 21.x)

Insert `,`
};
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
]
},
"dependencies": {
"@babel/core": "^7.23.3",
"@babel/eslint-parser": "^7.22.15",
"@ember-data/rfc395-data": "^0.0.4",
"@glimmer/syntax": "^0.85.12",
Expand Down Expand Up @@ -116,7 +117,13 @@
"typescript": "^5.2.2"
},
"peerDependencies": {
"eslint": ">= 8"
"eslint": ">= 8",
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"engines": {
"node": "18.* || 20.* || >= 21"
Expand Down
5 changes: 5 additions & 0 deletions tests/lib/rules-preprocessor/ember_ts/bar.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const fortyTwoFromGTS = '42';

<template>
{{fortyTwoFromGTS}}
</template>
1 change: 1 addition & 0 deletions tests/lib/rules-preprocessor/ember_ts/baz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const fortyTwoFromTS = '42';
14 changes: 14 additions & 0 deletions tests/lib/rules-preprocessor/ember_ts/foo.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { fortyTwoFromGTS } from './bar.gts';
import { fortyTwoFromTS } from './baz.ts';

export const fortyTwoLocal = '42';

const helloWorldFromTS = fortyTwoFromTS[0] === '4' ? 'hello' : 'world';
const helloWorldFromGTS = fortyTwoFromGTS[0] === '4' ? 'hello' : 'world';
const helloWorld = fortyTwoLocal[0] === '4' ? 'hello' : 'world';
//
<template>
{{helloWorldFromGTS}}
{{helloWorldFromTS}}
{{helloWorld}}
</template>
145 changes: 143 additions & 2 deletions tests/lib/rules-preprocessor/gjs-gts-parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

const { ESLint } = require('eslint');
const plugin = require('../../../lib');
const { writeFileSync, readFileSync, unlinkSync } = require('node:fs');
const { join } = require('node:path');

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

Expand Down Expand Up @@ -388,18 +390,28 @@ const invalid = [
code: `
import Component from '@glimmer/component';
const foo: any = '';
export default class MyComponent extends Component {
foo = 'bar';
<template>
<div></div>${' '}
{{foo}}
</template>
}`,
errors: [
{
message: 'Unexpected any. Specify a different type.',
line: 4,
endLine: 4,
column: 18,
endColumn: 21,
},
{
message: 'Trailing spaces not allowed.',
line: 8,
endLine: 8,
line: 10,
endLine: 10,
column: 22,
endColumn: 24,
},
Expand Down Expand Up @@ -765,4 +777,133 @@ describe('multiple tokens in same file', () => {
expect(resultErrors[2].message).toBe("'bar' is not defined.");
expect(resultErrors[2].line).toBe(17);
});

it('lints while being type aware', async () => {
const eslint = new ESLint({
ignore: false,
useEslintrc: false,
plugins: { ember: plugin },
overrideConfig: {
root: true,
env: {
browser: true,
},
plugins: ['ember'],
extends: ['plugin:ember/recommended'],
overrides: [
{
files: ['**/*.gts'],
parser: 'eslint-plugin-ember/gjs-gts-parser',
parserOptions: {
project: './tsconfig.eslint.json',
tsconfigRootDir: __dirname,
extraFileExtensions: ['.gts'],
},
extends: [
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:ember/recommended',
],
rules: {
'no-trailing-spaces': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
},
},
{
files: ['**/*.ts'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.eslint.json',
tsconfigRootDir: __dirname,
extraFileExtensions: ['.gts'],
},
extends: [
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:ember/recommended',
],
rules: {
'no-trailing-spaces': 'error',
},
},
],
rules: {
quotes: ['error', 'single'],
semi: ['error', 'always'],
'object-curly-spacing': ['error', 'always'],
'lines-between-class-members': 'error',
'no-undef': 'error',
'no-unused-vars': 'error',
'ember/no-get': 'off',
'ember/no-array-prototype-extensions': 'error',
'ember/no-unused-services': 'error',
},
},
});

let results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);

let resultErrors = results.flatMap((result) => result.messages);
expect(resultErrors).toHaveLength(3);

expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
expect(resultErrors[0].line).toBe(6);

expect(resultErrors[1].line).toBe(7);
expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");

expect(resultErrors[2].line).toBe(8);
expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead.");

const filePath = join(__dirname, 'ember_ts', 'bar.gts');
const content = readFileSync(filePath).toString();
try {
writeFileSync(filePath, content.replace("'42'", '42'));

results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);

resultErrors = results.flatMap((result) => result.messages);
expect(resultErrors).toHaveLength(2);

expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
expect(resultErrors[0].line).toBe(6);

expect(resultErrors[1].line).toBe(8);
expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
} finally {
writeFileSync(filePath, content);
}

results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);

resultErrors = results.flatMap((result) => result.messages);
expect(resultErrors).toHaveLength(3);

expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
expect(resultErrors[0].line).toBe(6);

expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
expect(resultErrors[1].line).toBe(7);

expect(resultErrors[2].line).toBe(8);
expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead.");

try {
unlinkSync(filePath);
await new Promise(resolve => setTimeout(resolve, 100));

Check failure on line 891 in tests/lib/rules-preprocessor/gjs-gts-parser-test.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 18.x)

Replace `resolve·` with `(resolve)`

Check failure on line 891 in tests/lib/rules-preprocessor/gjs-gts-parser-test.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 20.x)

Replace `resolve·` with `(resolve)`

Check failure on line 891 in tests/lib/rules-preprocessor/gjs-gts-parser-test.js

View workflow job for this annotation

GitHub Actions / build (ubuntu, 21.x)

Replace `resolve·` with `(resolve)`
results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);

resultErrors = results.flatMap((result) => result.messages);
expect(resultErrors).toHaveLength(3);

expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
expect(resultErrors[0].line).toBe(6);

expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
expect(resultErrors[1].line).toBe(7);

expect(resultErrors[2].line).toBe(8);
expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead.");
} finally {
writeFileSync(filePath, content);
}
});
});
5 changes: 3 additions & 2 deletions tests/lib/rules-preprocessor/tsconfig.eslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"strictNullChecks": true
},
"include": [
"*"
]
"**/*.ts",
"**/*.gts"
],
}
Loading

0 comments on commit 77fe5c8

Please sign in to comment.