Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(eslint-plugin): [no-duplicate-type-constituents] prevent unnecessary| undefined for optional parameters#9479

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
24 commits
Select commitHold shift + click to select a range
fa911e8
make the feature
abrahamguoJul 2, 2024
1db600a
small refactor
abrahamguoJul 2, 2024
5735723
update docs
abrahamguoJul 2, 2024
502d684
apply lint rule
abrahamguoJul 2, 2024
5ee073c
add issue comment
abrahamguoJul 2, 2024
1ce2f45
update snapshot
abrahamguoJul 2, 2024
180dd88
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into u…
abrahamguoJul 4, 2024
02097a4
revert accidental formatting changes
abrahamguoJul 4, 2024
d79de67
handle first constituent of union
abrahamguoJul 4, 2024
4b7743d
lint
abrahamguoJul 5, 2024
e776b26
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into u…
abrahamguoJul 5, 2024
eabd62c
use nullThrows
abrahamguoJul 5, 2024
f84d9f3
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into u…
abrahamguoJul 28, 2024
e73a29d
add failing test
abrahamguoJul 28, 2024
0490020
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into u…
abrahamguoJul 30, 2024
24e6d21
WIP
abrahamguoJul 30, 2024
d5d3af5
finish refactor
abrahamguoJul 31, 2024
e2253c5
cleanup
abrahamguoJul 31, 2024
6f2058a
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into u…
abrahamguoAug 13, 2024
bb88e90
clean up nits
abrahamguoAug 13, 2024
b47b88f
add more node types
abrahamguoAug 13, 2024
15e9e42
remove only
abrahamguoAug 13, 2024
6024089
Merge branch 'main' of github.com:abrahamguo/typescript-eslint into u…
abrahamguoAug 27, 2024
63d6400
remove now-unnecessary check
abrahamguoAug 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ This rule disallows duplicate union or intersection constituents.
We consider types to be duplicate if they evaluate to the same result in the type system.
For example, given `type A = string` and `type T = string | A`, this rule would flag that `A` is the same type as `string`.

This rule also disallows explicitly listing `undefined` in a type union when a function parameter is marked as optional.
Doing so is unnecessary.

<Tabs>
<TabItem value="❌ Incorrect">

Expand All@@ -32,6 +35,8 @@ type T4 = [1, 2, 3] | [1, 2, 3];
type StringA = string;
type StringB = string;
type T5 = StringA | StringB;

const fn = (a?: string | undefined) => {};
```

</TabItem>
Expand All@@ -49,6 +54,8 @@ type T4 = [1, 2, 3] | [1, 2, 3, 4];
type StringA = string;
type NumberB = number;
type T5 = StringA | NumberB;

const fn = (a?: string) => {};
```

</TabItem>
Expand Down
190 changes: 115 additions & 75 deletionspackages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,15 @@ import type { TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import * as tsutils from 'ts-api-utils';
import type { Type } from 'typescript';
import * as ts from 'typescript';

import { createRule, getParserServices } from '../util';
import {
createRule,
getParserServices,
isFunctionOrFunctionType,
nullThrows,
NullThrowsReasons,
} from '../util';

export type Options = [
{
Expand All@@ -12,7 +19,7 @@ export type Options = [
},
];

export type MessageIds = 'duplicate';
export type MessageIds = 'duplicate' | 'unnecessary';

const astIgnoreKeys = new Set(['range', 'loc', 'parent']);

Expand DownExpand Up@@ -79,6 +86,8 @@ export default createRule<Options, MessageIds>({
fixable: 'code',
messages: {
duplicate: '{{type}} type constituent is duplicated with {{previous}}.',
unnecessary:
'Explicit undefined is unnecessary on an optional parameter.',
},
schema: [
{
Expand All@@ -103,9 +112,14 @@ export default createRule<Options, MessageIds>({
],
create(context, [{ ignoreIntersections, ignoreUnions }]) {
const parserServices = getParserServices(context);
const { sourceCode } = context;

function checkDuplicate(
node: TSESTree.TSIntersectionType | TSESTree.TSUnionType,
forEachNodeType?: (
constituentNodeType: Type,
report: (messageId: MessageIds) => void,
) => void,
): void {
const cachedTypeMap = new Map<Type, TSESTree.TypeNode>();
node.types.reduce<TSESTree.TypeNode[]>(
Expand All@@ -116,94 +130,120 @@ export default createRule<Options, MessageIds>({
return uniqueConstituents;
}

const duplicatedPreviousConstituentInAst = uniqueConstituents.find(
ele => isSameAstNode(ele, constituentNode),
);
if (duplicatedPreviousConstituentInAst) {
reportDuplicate(
{
duplicated: constituentNode,
duplicatePrevious: duplicatedPreviousConstituentInAst,
},
node,
const report = (
messageId: MessageIds,
data?: Record<string, unknown>,
): void => {
const getUnionOrIntersectionToken = (
where: 'Before' | 'After',
at: number,
): TSESTree.Token | undefined =>
sourceCode[`getTokens${where}`](constituentNode, {
filter: token => ['|', '&'].includes(token.value),
}).at(at);

const beforeUnionOrIntersectionToken = getUnionOrIntersectionToken(
'Before',
-1,
);
return uniqueConstituents;
}
const duplicatedPreviousConstituentInType =
cachedTypeMap.get(constituentNodeType);
if (duplicatedPreviousConstituentInType) {
reportDuplicate(
{
duplicated: constituentNode,
duplicatePrevious: duplicatedPreviousConstituentInType,
let afterUnionOrIntersectionToken: TSESTree.Token | undefined;
let bracketBeforeTokens;
let bracketAfterTokens;
if (beforeUnionOrIntersectionToken) {
bracketBeforeTokens = sourceCode.getTokensBetween(
beforeUnionOrIntersectionToken,
constituentNode,
);
bracketAfterTokens = sourceCode.getTokensAfter(constituentNode, {
count: bracketBeforeTokens.length,
});
} else {
afterUnionOrIntersectionToken = nullThrows(
getUnionOrIntersectionToken('After', 0),
NullThrowsReasons.MissingToken(
'union or intersection token',
'duplicate type constituent',
),
);
bracketAfterTokens = sourceCode.getTokensBetween(
constituentNode,
afterUnionOrIntersectionToken,
);
bracketBeforeTokens = sourceCode.getTokensBefore(
constituentNode,
{
count: bracketAfterTokens.length,
},
);
}
context.report({
data,
messageId,
node: constituentNode,
loc: {
start: constituentNode.loc.start,
end: (bracketAfterTokens.at(-1) ?? constituentNode).loc.end,
},
node,
);
fix: fixer =>
[
beforeUnionOrIntersectionToken,
...bracketBeforeTokens,
constituentNode,
...bracketAfterTokens,
afterUnionOrIntersectionToken,
].flatMap(token => (token ? fixer.remove(token) : [])),
});
};
const duplicatePrevious =
uniqueConstituents.find(ele =>
isSameAstNode(ele, constituentNode),
) ?? cachedTypeMap.get(constituentNodeType);
if (duplicatePrevious) {
report('duplicate', {
type:
node.type === AST_NODE_TYPES.TSIntersectionType
? 'Intersection'
: 'Union',
previous: sourceCode.getText(duplicatePrevious),
});
return uniqueConstituents;
}
forEachNodeType?.(constituentNodeType, report);
cachedTypeMap.set(constituentNodeType, constituentNode);
return [...uniqueConstituents, constituentNode];
},
[],
);
}
function reportDuplicate(
duplicateConstituent: {
duplicated: TSESTree.TypeNode;
duplicatePrevious: TSESTree.TypeNode;
},
parentNode: TSESTree.TSIntersectionType | TSESTree.TSUnionType,
): void {
const beforeTokens = context.sourceCode.getTokensBefore(
duplicateConstituent.duplicated,
{ filter: token => token.value === '|' || token.value === '&' },
);
const beforeUnionOrIntersectionToken =
beforeTokens[beforeTokens.length - 1];
const bracketBeforeTokens = context.sourceCode.getTokensBetween(
beforeUnionOrIntersectionToken,
duplicateConstituent.duplicated,
);
const bracketAfterTokens = context.sourceCode.getTokensAfter(
duplicateConstituent.duplicated,
{ count: bracketBeforeTokens.length },
);
const reportLocation: TSESTree.SourceLocation = {
start: duplicateConstituent.duplicated.loc.start,
end:
bracketAfterTokens.length > 0
? bracketAfterTokens[bracketAfterTokens.length - 1].loc.end
: duplicateConstituent.duplicated.loc.end,
};
context.report({
data: {
type:
parentNode.type === AST_NODE_TYPES.TSIntersectionType
? 'Intersection'
: 'Union',
previous: context.sourceCode.getText(
duplicateConstituent.duplicatePrevious,
),
},
messageId: 'duplicate',
node: duplicateConstituent.duplicated,
loc: reportLocation,
fix: fixer => {
return [
beforeUnionOrIntersectionToken,
...bracketBeforeTokens,
duplicateConstituent.duplicated,
...bracketAfterTokens,
].map(token => fixer.remove(token));
},
});
}

return {
...(!ignoreIntersections && {
TSIntersectionType: checkDuplicate,
}),
...(!ignoreUnions && {
TSUnionType: checkDuplicate,
TSUnionType: (node): void =>
checkDuplicate(node, (constituentNodeType, report) => {
const maybeTypeAnnotation = node.parent;
if (maybeTypeAnnotation.type === AST_NODE_TYPES.TSTypeAnnotation) {
const maybeIdentifier = maybeTypeAnnotation.parent;
if (
maybeIdentifier.type === AST_NODE_TYPES.Identifier &&
maybeIdentifier.optional
) {
const maybeFunction = maybeIdentifier.parent;
if (
isFunctionOrFunctionType(maybeFunction) &&
maybeFunction.params.includes(maybeIdentifier) &&
tsutils.isTypeFlagSet(
constituentNodeType,
ts.TypeFlags.Undefined,
)
) {
report('unnecessary');
}
}
}
}),
}),
};
},
Expand Down
2 changes: 1 addition & 1 deletionpackages/eslint-plugin/src/rules/prefer-find.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -332,7 +332,7 @@ function isStaticMemberAccessOfValue(
| TSESTree.MemberExpressionComputedName
| TSESTree.MemberExpressionNonComputedName,
value: string,
scope?: Scope.Scope | undefined,
scope?: Scope.Scope,
): boolean {
if (!memberExpression.computed) {
// x.memberName case.
Expand Down

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

Loading

[8]ページ先頭

©2009-2025 Movatter.jp