Skip to content

Commit

Permalink
fix type aware
Browse files Browse the repository at this point in the history
  • Loading branch information
patricklx committed Nov 14, 2023
1 parent 90d1fc4 commit 412bbfd
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 4 deletions.
75 changes: 73 additions & 2 deletions lib/parsers/gjs-gts-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ const DocumentLines = require('../utils/document');
const { visitorKeys: glimmerVisitorKeys } = require('@glimmer/syntax');
const babelParser = require('@babel/eslint-parser');
const typescriptParser = require('@typescript-eslint/parser');
const ts = require('typescript');
const TypescriptScope = require('@typescript-eslint/scope-manager');
const { Reference, Scope, Variable, Definition } = require('eslint-scope');
const { registerParsedFile } = require('../preprocessors/noop');
const htmlTags = require('html-tags');
const path = require('node:path');
const fs = require('node:fs');

/**
* finds the nearest node scope
Expand Down Expand Up @@ -460,9 +463,10 @@ function replaceRange(s, start, end, substitute) {
return s.slice(0, start) + substitute + s.slice(end);
}

const processor = new ContentTag.Preprocessor();

function transformForLint(code) {
let jsCode = code;
const processor = new ContentTag.Preprocessor();
/**
*
* @type {{
Expand Down Expand Up @@ -516,6 +520,8 @@ function transformForLint(code) {
};
}

const TsProgramMap = {};

/**
* implements https://eslint.org/docs/latest/extend/custom-parsers
* 1. transforms gts/gjs files into parseable ts/js without changing the offsets and locations around it
Expand All @@ -533,12 +539,77 @@ module.exports = {
let jsCode = code;
const info = transformForLint(code);
jsCode = info.output;
jsCode = jsCode.replaceAll(/\.gts(["'])/g, '.ts$1 ');

const isTypescript = options.filePath.endsWith('.gts');

let result = null;

const projectFile = path.resolve(options.tsconfigRootDir, options.project);

if (!TsProgramMap[projectFile]) {
const config = ts.getParsedCommandLineOfConfigFile(
projectFile,
{},
{
...ts.sys,
readDirectory(dir) {
const results = ts.sys.readDirectory(dir);
return results.map((f) => f.replace(/\.gts$/, '.ts'));
},
onUnRecoverableConfigFileDiagnostic(diagnostic) {
let { messageText } = diagnostic;
if (typeof messageText !== 'string') {
messageText = messageText.messageText;
}

throw new Error(messageText);
},
}
);

const host = ts.createCompilerHost(config.options);
host.fileExists = function (fileName) {
return fs.existsSync(fileName.replace(/\.ts$/, '.gts')) || fs.existsSync(fileName);
};

const readDirectory = host.readDirectory;
host.readDirectory = function(dir) {
const results = readDirectory.call(this, dir);
return results.map((f) => f.replace(/\.gts$/, '.ts'));
};
host.readFile = function(fname) {
let fileName = fname;
if (fileName === options.filePath.replace(/\\/g, '/').replace(/\.gts$/, '.ts')) {
return jsCode;
}
let content = '';
try {
content = fs.readFileSync(fileName).toString();
} catch {
fileName = fileName.replace(/\.ts$/, '.gts');
content = fs.readFileSync(fileName).toString();
}
if (fileName.endsWith('.gts')) {
content = transformForLint(content).output;
}
content = content.replaceAll(/\.gts(["'])/g, '.ts$1 ');
return content;
};

TsProgramMap[projectFile] = ts.createProgram({
rootNames: config.fileNames,
options: config.options,
host,
});
}

const program = TsProgramMap[projectFile];

options.filePath = options.filePath.replace(/\.gts$/, '.ts');

result = isTypescript
? typescriptParser.parseForESLint(jsCode, { ...options, ranges: true })
? typescriptParser.parseForESLint(jsCode, { ...options, ranges: true, programs: [program] })
: babelParser.parseForESLint(jsCode, { ...options, ranges: true });
if (!info.templateInfos?.length) {
return result;
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>
76 changes: 76 additions & 0 deletions tests/lib/rules-preprocessor/gjs-gts-parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,4 +761,80 @@ 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',
},
},
});

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

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

expect(resultErrors[0].line).toBe(6);
expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead."); // Actual result is "Unsafe member access [0] on an `any` value."

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.");
});
});
4 changes: 2 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,6 @@
"strictNullChecks": true
},
"include": [
"*"
]
"**/*"
],
}

0 comments on commit 412bbfd

Please sign in to comment.