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 a93ea4b
Show file tree
Hide file tree
Showing 10 changed files with 883 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 /* istanbul ignore next */ {
// typescript not available
patchTs = () => null;
replaceExtensions = (code) => code;
syncMtsGtsSourceFiles = () => null;
}

module.exports = {
patchTs,
replaceExtensions,
syncMtsGtsSourceFiles,
typescriptParser,
};
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>
125 changes: 123 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 } = 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,113 @@ 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.");
});
});
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 a93ea4b

Please sign in to comment.