Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[gjs-gts-parser] fix type aware linting when using ts+gts files #1996

Merged
merged 1 commit into from
Dec 12, 2023

Conversation

patricklx
Copy link
Contributor

@patricklx patricklx commented Nov 13, 2023

this passes our own ts.sys to typescript, which emulates gts files as mts files.
We use mts extension, so that we keep the same length of import statements and so not move offsets when replacing the extension in imports.

fixes #1995

also cleaned up the code as the file was getting too big.

@patricklx patricklx changed the title attempt fix type aware fix type aware linting with when using ts+gts files Nov 14, 2023
@patricklx patricklx force-pushed the fix-type-aware branch 2 times, most recently from 5efa7b2 to 486d505 Compare November 14, 2023 09:50
},
{
files: ['**/*.ts'],
parser: '@typescript-eslint/parser',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patricklx don't we need to leave *.ts files parsing to eslint-plugin-ember/gjs-gts-parser also?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't use it then I assume we will again have errors if we try to import from a .gts file inside a regular .ts file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though this means that we will need to recommend ppl use eslint-plugin-ember/gjs-gts-parser for all files js/ts/gjs/gts in order to have it work properly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i decided now to just overwrite the TS.sys functions, typescript-eslint already handles those cases and with this we keep it like that

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aha I see, so basically we just monkey patch ts directly. Might be a bit brittle but it will work :D.
And we don't need to worry about creating our own TS program nor involving glint :D
Solid idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem i'm facing now is how to replace .gts without breaking some eslint rule...
especially the stylistic ones.
it would shift all code by 1 character... but adding something after it breaks some whitespace rules.
So now i use the extension .mts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though this means that we will need to recommend ppl use eslint-plugin-ember/gjs-gts-parser for all files js/ts/gjs/gts in order to have it work properly

Maybe, now ts.sys is overwritten as soon as gjs-gts-parser is loaded. Which is loaded during config loading.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patricklx yes you are right, with ts patch approach we can leave ts files to typescript-eslint/parser.
Again solid idea patching this :)

@patricklx patricklx marked this pull request as ready for review November 14, 2023 13:44
@patricklx patricklx changed the title fix type aware linting with when using ts+gts files [gjs-gts-parser] fix type aware linting with when using ts+gts files Nov 14, 2023
const resultErrors = results.flatMap((result) => result.messages);
expect(resultErrors).toHaveLength(3);

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patricklx We should probably remove this comment now

package.json Outdated Show resolved Hide resolved
@bmish bmish added the bug label Nov 16, 2023
return content;
},
};
ts.setSys(newSys);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found this, now a bit better than monkey patching

@vstefanovic97
Copy link

@patricklx this seems to be working ok now as far as I can tell :D

@patricklx patricklx changed the title [gjs-gts-parser] fix type aware linting with when using ts+gts files [gjs-gts-parser] fix type aware linting when using ts+gts files Nov 24, 2023
@patricklx
Copy link
Contributor Author

@bmish I squashed the commits now. this is ready

@vstefanovic97
Copy link

Will we be able to merge this soon?
Ping @bmish

@patricklx
Copy link
Contributor Author

rebased


module.exports.patchTs = function patchTs() {
const readDirectory = ts.sys.readDirectory;
ts.sys.readDirectory = function (...args) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be overriding TypeScript functions with our own version, correct? Couldn't this affect other code using TypeScript in the user's application/repository? I don't think it's safe nor acceptable to be modifying dependencies like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only inside eslint. It should work normally as we also call the original function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's how I understand these sorts of modifications, too

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you find some evidence for that? I'm not convinced that's the case. TypeScript is likely installed just once in the user's repository, assuming everyone requests v5. eslint-plugin-ember doesn't necessarily have its own copy of TypeScript. So I suspect you are modifying TypeScript used by others.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to mention, this is not a supported API for adjusting TypeScript behavior, and presumably could break with any future version update...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this code is to be removed from eslint-plugin-ember to ember-eslint-parser (and then that dep added back here), and it's not the type of patch you think it is.

Happy to hop on a call if you like!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its actually not monkey patching anymore.
Its passing our own sys to typescript via ts.setSys() if that makes any difference.
https://github.com/ember-cli/eslint-plugin-ember/pull/1996/files#diff-e244765e7358fb9a23e94c9b7d23917d4ed0172bb7550fbc1ff88359691859feR12.
It's similar to how it's done on the ts.program object in typescript-eslint

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be PRing the removal of all this parser code shortly, and then be collabin with Patrick to reintroduce via single dep

Copy link
Member

@bmish bmish Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like setSys is not public.

Anyways, I'll go ahead and release another alpha version shortly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the beginning of the extraction #2028

}
}
// block params do not have location information
// add our own nodes so we can reference them
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we do anything with block params? the whole glimmer AST should not be needed for gjs/gts parsing (as far as linting is concerned)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reorganized the files as parser file was getting to big

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but that doesn't answer the question 😅

Copy link
Contributor Author

@patricklx patricklx Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it's to have the rules no-undef and no-unused work in gjs/gts templates. Like this we have the exact location of no-undef.
And it also works for block params. Marking them unused when not used.
And js vars, that are in use in the template will not be marked as unused

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see it in thte tests for the parser

Copy link
Contributor

@NullVoxPopuli NullVoxPopuli Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I guess, how much code is there just to support no-undef and no-unused having exact locations?

maybe it's no big deal -- but also maybe @glimmer/syntax should support having offsets passed to it for all the AST nodes to reduce the Math being done in eslint-plugin-ember 🤔

maybe content-tag, or another library should be responsible for stitching together the concepts of:

  • content-tag (split <template> from js/ts)
  • parse AST for JS/TS
  • parse AST for the <template>s
  • stitch all the ASTs together with correct offsets for the file
  • somehow have methods to allow writing back to the file for any given sub-AST range

(I see now why there is so much code involved with all of this, that is a lot of steps, and I feel like most folks overwhelmed by all this code (including me, maaaaybe @ef4 (ie: put everything in content-tag), didn't understand the full scope of the problem)

Copy link
Contributor Author

@patricklx patricklx Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we only have this configuration for gjs/gts in template-lint: https://github.com/ember-template-lint/ember-template-lint/blob/master/lib/config/recommended.js#L98

Right, template-lint doesn't handle js scope. Here it can handle both.

It also is more code to supporting other rules that are token based

Copy link
Contributor Author

@patricklx patricklx Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the code could be simpler if glimmer/syntax provides ranges for block params and for the html tag parts split by .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will happily take PRs on that repo that improve the parser!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit behind on this PR, so apologies while I ask a bunch of stupid questions 😅

I was looking at eslint-plugin-svelte, and they have something like this:

 {
      files: ['*.svelte'],
      parser: 'svelte-eslint-parser',
      parserOptions: {
        parser: {
          // Specify a parser for each lang.
          ts: '@typescript-eslint/parser',
          js: 'espree',
          typescript: '@typescript-eslint/parser'
        }
      }
    }

would that work for us / simplify this code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we set a program, we also have to handle program updates... ts-eslint has a bunch of code fir that.
which is ehy i opted to pass a custom sys to ts.setSys.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, thanks!!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well wait -- what do you mean by program updates?

Copy link

@vstefanovic97 vstefanovic97 Dec 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NullVoxPopuli either way it couldn't hurt to covert the code to ESM.
If you are up for it I can try to spend some time next couple of days and convert the codebase to ESM if you guys would be willing to review.
After that we can try Glint approach more easily and see if it will work? Wdyt? cc: @patricklx, @bmish

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally, I think since we're preparing for a major, now is a great time to convert to ESM -- (esp as that conversion is much easier to land than type-aware linting, which we still have lots to document and talk about (to spread knowledge, context, document issues, document with the TS team, etc))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NullVoxPopuli I think we actually can't convert to ESM, because eslint package uses cjs so it won't be able to import parseForEslint function as well as rules if we converted them to ESM...
So I think unless glint packages start publishing cjs only approach we have is trying the dynamic import.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. I tried to make an esm plugin for eslint once and it only works with the new configuration files.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting to ESM may be a large effort best saved for the next major version (not the in-progress v12 release which is already packed with breaking changes) and after ESLint v9 is out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting another unrelated thread, with some historical artefacts:

So, given this, I am not surprised making TS work with gjs/gts is a fair bit of work. And I have a hunch this is why the Vue ecosystem has optional support for jsx/tsx, because TS built in jsx/tsx into TS parsing with no option for other syntax plugins

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is one such way to parse custom files: https://github.com/ota-meshi/typescript-eslint-parser-for-extra-files

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like none of this matters tho, because svelte seems to have fixed it all,
their config:

/** @type { import("eslint").Linter.FlatConfig } */
module.exports = {
	root: true,
	extends: [
		'eslint:recommended',
		'plugin:@typescript-eslint/recommended',
		'plugin:svelte/recommended',
		'prettier'
	],
	parser: '@typescript-eslint/parser',
	plugins: ['@typescript-eslint'],
	parserOptions: {
		sourceType: 'module',
		ecmaVersion: 2020,
		extraFileExtensions: ['.svelte']
	},
	env: {
		browser: true,
		es2017: true,
		node: true
	},
	overrides: [
		{
			files: ['*.svelte'],
			parser: 'svelte-eslint-parser',
			parserOptions: {
				parser: '@typescript-eslint/parser'
			}
		}
	]
};

Copy link
Contributor

@NullVoxPopuli NullVoxPopuli Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though, this requires generating a `.svelte-kit` folder before it can work for some reason

old comment, because they break general expectation of eslint and "just working on source files":

oh nevermind, they seem to actually be further behind: sveltejs/kit#11209

const glimmer = require('@glimmer/syntax');
const DocumentLines = require('../utils/document');
const { visitorKeys: glimmerVisitorKeys } = require('@glimmer/syntax');
const TypescriptScope = require('@typescript-eslint/scope-manager');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another separate thread:

typescript-eslint/typescript-eslint#3174 (comment)

OOOOOOFFFF

@patricklx patricklx force-pushed the fix-type-aware branch 2 times, most recently from 6f21d2f to 35c39d3 Compare December 8, 2023 14:31
extraFileExtensions: ['.gts'],
},
extends: [
'plugin:@typescript-eslint/recommended-requiring-type-checking',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this config? I didn't see it listed here: https://typescript-eslint.io/linting/configs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be an old config name but it's for type-aware linting: https://typescript-eslint.io/linting/typed-linting/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'plugin:@typescript-eslint/recommended-type-checked',

vs

'plugin:@typescript-eslint/recommended-requiring-type-checking',

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also add a test for a failure case for a type-aware lint?

Like, any of

'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',

Copy link
Contributor

@NullVoxPopuli NullVoxPopuli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these changes are looking really good, and I super appreciate the work you've all been doing on figuring this out.
(It seems that @patricklx and @vstefanovic97 are some of the first folks to figure out type-aware linting for non-js/x syntaxes!!!!)

I'm approving to move things forward, though I'd like to see a type-aware error test added before merge.

After merging, How do ya'll feel about extracting the parser to a separate package (so it can have its own tests, lots more tests, etc, and be conceptually separated from "lint configuration", which is what the spirit of eslint-plugin-ember is?)

Then, eslint-plugin-ember can bundle that extracted package as a dependencies entry, so consumers don't need to worry about installing anything separate.

I have a feeling this would put many of the lint-package maintainers at ease, as parsing and line-offset-altering is intense, and not for the faint of heart!

(It also allows us to iterate on the parser without re-releasing eslint-plugin-ember (thanks semver!))

edit: Made the repo here, and added @patricklx and @vstefanovic97 as collaborators on it
https://github.com/NullVoxPopuli/ember-eslint-parser

@patricklx
Copy link
Contributor Author

patricklx commented Dec 12, 2023

@NullVoxPopuli found a way to sync the mts & gts internal source files.
also, ts is now handled optional

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
@patricklx
Copy link
Contributor Author

@NullVoxPopuli there are already type aware tests on the string with the [0] check. Lint needs to know that it is actually a string. Otherwise it will not show that error

@NullVoxPopuli NullVoxPopuli merged commit 49f2690 into ember-cli:master Dec 12, 2023
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support "Type aware" linting for gts files
4 participants