Skip to content

Commit

Permalink
Merge pull request #1996 from patricklx/fix-type-aware
Browse files Browse the repository at this point in the history
[gjs-gts-parser] fix type aware linting when using ts+gts files
  • Loading branch information
NullVoxPopuli authored Dec 12, 2023
2 parents 186376c + a93ea4b commit 49f2690
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 @@ -119,7 +120,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 49f2690

Please sign in to comment.