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

fix: fix illegal decorator check #6723

Merged
merged 20 commits into from
Jun 17, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 0 additions & 8 deletions packages/ast-spec/tests/fixtures-with-differences-errors.shot

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 35 additions & 1 deletion packages/typescript-estree/src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
canContainDirective,
createError,
findNextToken,
getAllAccessorDeclarations,
getBinaryExpressionType,
getContainingFunction,
getDeclarationKind,
Expand All @@ -17,6 +18,7 @@ import {
getRange,
getTextForTokenKind,
getTSNodeAccessibility,
hasDecorators,
hasModifier,
isChainExpression,
isChildUnwrappableOptionalChain,
Expand All @@ -25,6 +27,7 @@ import {
isESTreeClassMember,
isOptional,
isThisInTypeQuery,
nodeCanBeDecorated,
nodeHasIllegalDecorators,
nodeIsPresent,
unescapeStringLiteralText,
Expand Down Expand Up @@ -3107,14 +3110,45 @@ export class Converter {
return;
}

// typescript<5.0.0
if (nodeHasIllegalDecorators(node)) {
this.#throwError(
node.illegalDecorators[0],
'Decorators are not valid here.',
);
}

for (const modifier of getModifiers(node) ?? []) {
// @ts-expect-error -- this is safe as it's guarded
const modifiers: ts.ModifierLike[] = node.modifiers;
Copy link
Member

Choose a reason for hiding this comment

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

instead of using .modifiers can we use getModifiers here?

Copy link
Contributor Author

@fisker fisker Mar 21, 2023

Choose a reason for hiding this comment

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

getModifiers only returns ts.Modifier, but we need ts.Decorators, unless we call getDecorators again.

Copy link
Member

Choose a reason for hiding this comment

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

In that same file there is the getDecorators util which should do what you want I think?

Copy link
Contributor Author

@fisker fisker Mar 21, 2023

Choose a reason for hiding this comment

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

I know.

unless we can call getDecorators again.

But I checked both modifiers and decorators here, and getModifiers() + getDecorators() is node.modifiers

Copy link
Member

Choose a reason for hiding this comment

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

I'm not quite sure I understand - your code below does "for each modifier, only action the decorators"

So couldn't the code be

for (const decorator of getDecorators(node)) {
  // your code
}

for (const modifier of getModifiers(node)) {
  // old code
}

Copy link
Contributor Author

@fisker fisker Apr 19, 2023

Choose a reason for hiding this comment

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

Ideally, it should work, but I still can't get why you want 4 loops when you can do it in 1 loop.

Anyway, I tried, but turns out getModifiers() + getDecorators() not equals to node.modifiers.
ts.canHaveModifiers() and ts.canHaveDecorators() are stopping me to access modifiers on some node. You can try it to see the failed tests.

Should we

  1. Remove those check
  2. Add option to skip check

?

Copy link
Member

Choose a reason for hiding this comment

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

The problem here is that they purposely removed node.modifiers from the types in TSv5 - so we definitely don't want to be accessing it directly if we can avoid it as it's clear that their intention is for it to be a truly private property.

I guess we could add an option to skip the checks?


if (!modifiers) {
return;
}

for (const modifier of modifiers) {
if (ts.isDecorator(modifier)) {
// `checkGrammarModifiers` function in typescript
if (!nodeCanBeDecorated(node)) {
if (ts.isMethodDeclaration(node) && !nodeIsPresent(node.body)) {
this.#throwError(
modifier,
'A decorator can only decorate a method implementation, not an overload.',
);
} else {
this.#throwError(modifier, 'Decorators are not valid here.');
}
} else if (ts.isSetAccessor(node) || ts.isGetAccessor(node)) {
const { firstAccessor, secondAccessor } =
getAllAccessorDeclarations(node);
if (hasDecorators(firstAccessor) && node === secondAccessor) {
this.#throwError(
modifier,
'Decorators cannot be applied to multiple get/set accessors of the same name.',
);
}
}
}

if (modifier.kind !== SyntaxKind.ReadonlyKeyword) {
if (
node.kind === SyntaxKind.PropertySignature ||
Expand Down
38 changes: 38 additions & 0 deletions packages/typescript-estree/src/node-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,3 +766,41 @@ export function getContainingFunction(
): ts.SignatureDeclaration | undefined {
return ts.findAncestor(node.parent, ts.isFunctionLike);
}

// `ts.nodeCanBeDecorated`
export function nodeCanBeDecorated(node: ts.Node): boolean {
return [true, false].some(
useLegacyDecorators =>
// @ts-expect-error -- internal api
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
ts.nodeCanBeDecorated?.(
Copy link
Member

Choose a reason for hiding this comment

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

we can't use this function, or at least not without version checks.
in versions before 5.0 this function did not take boolean as the first argument - so by calling it like this we will cause a crash.

Given it's an internal API - I think we might want to consider rewriting it for ourselves? The logic will be pretty static given the spec doesn't change much if at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in versions before 5.0 this function did not take boolean as the first argument

Didn't know that.

I think we might want to consider rewriting it for ourselves?

I can try.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added acaee6b, but I don't know the equivalent of ts.hasAmbientModifier, can you help?

Copy link
Member

Choose a reason for hiding this comment

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

hasAmbientModifier was only added in TS 4.3, and currently on v6 we are supporting 4.2
However based on the 2y lifecycle of versions that DefinitelyTyped supports (which we want to roughly align with) we will actually now be able to bump the minimum version to 4.3.

We can queue a separate PR for that then update this PR to use ts.hasAmbientModifier (#6923)

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
Member

Choose a reason for hiding this comment

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

it's still a private, internal API!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's use a fake one for now? a84fa78

Copy link
Member

Choose a reason for hiding this comment

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

If we're going with a fake () => false one then I just removed it altogether. Seems like everything is working well without it? I posted a link in microsoft/TypeScript#52727 (comment).

useLegacyDecorators,
node,
node.parent,
node.parent.parent,
) ?? true,
);
}

// `ts.hasDecorators`
export function hasDecorators(node: ts.Node | undefined): boolean {
// @ts-expect-error -- internal api
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return
return Boolean(node) && (ts.hasDecorators?.(node) ?? false);
}

// `ts.getAllAccessorDeclarations`
export function getAllAccessorDeclarations(
accessor: ts.GetAccessorDeclaration | ts.SetAccessorDeclaration,
): {
firstAccessor?: ts.Node;
secondAccessor?: ts.Node;
} {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return (
// @ts-expect-error -- internal api
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
ts.getAllAccessorDeclarations?.(accessor.parent.members ?? [], accessor) ??
{}
);
fisker marked this conversation as resolved.
Show resolved Hide resolved
}