Skip to content

Commit d1388fc

Browse files
fix(eslint-plugin): [no-deprecated] should allow ignoring of deprecated value (typescript-eslint#10670)
* feat: create valueMatchesSomeSpecifier function * fix: ignore deprecated value * test: add test * test: add test * refactor: use AST node narrowing * feat: support package * test: add test * test: add test * test: add test for typeMatchesSomeSpecifier function * test: add test for valueMatchesSomeSpecifier function * refactor: remove default empty array from specifiers * Revert "refactor: remove default empty array from specifiers" This reverts commit 3673d21. * test: add test case with undefined argument * fix: sync workspace * feat: support PrivateIdentifier * feat: support literal case * feat: support dynamic import * test: add no-deprecated test * fix: sync workspace * refactor: use named functions instead of string descriptions in describe blocks * fix: lint fix --------- Co-authored-by: Josh Goldberg <git@joshuakgoldberg.com>
1 parent 5e3288a commit d1388fc

File tree

4 files changed

+493
-2
lines changed

4 files changed

+493
-2
lines changed

packages/eslint-plugin/src/rules/no-deprecated.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
nullThrows,
1313
typeOrValueSpecifiersSchema,
1414
typeMatchesSomeSpecifier,
15+
valueMatchesSomeSpecifier,
1516
} from '../util';
1617

1718
type IdentifierLike =
@@ -375,7 +376,10 @@ export default createRule<Options, MessageIds>({
375376
}
376377

377378
const type = services.getTypeAtLocation(node);
378-
if (typeMatchesSomeSpecifier(type, allow, services.program)) {
379+
if (
380+
typeMatchesSomeSpecifier(type, allow, services.program) ||
381+
valueMatchesSomeSpecifier(node, allow, services.program, type)
382+
) {
379383
return;
380384
}
381385

packages/eslint-plugin/tests/rules/no-deprecated.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,21 @@ ruleTester.run('no-deprecated', rule, {
332332
{
333333
code: `
334334
/** @deprecated */
335+
function A() {
336+
return <div />;
337+
}
338+
339+
const a = <A></A>;
340+
`,
341+
options: [
342+
{
343+
allow: [{ from: 'file', name: 'A' }],
344+
},
345+
],
346+
},
347+
{
348+
code: `
349+
/** @deprecated */
335350
declare class A {}
336351
337352
new A();
@@ -344,7 +359,62 @@ new A();
344359
},
345360
{
346361
code: `
362+
/** @deprecated */
363+
const deprecatedValue = 45;
364+
const bar = deprecatedValue;
365+
`,
366+
options: [
367+
{
368+
allow: [{ from: 'file', name: 'deprecatedValue' }],
369+
},
370+
],
371+
},
372+
{
373+
code: `
374+
class MyClass {
375+
/** @deprecated */
376+
#privateProp = 42;
377+
value = this.#privateProp;
378+
}
379+
`,
380+
options: [
381+
{
382+
allow: [{ from: 'file', name: 'privateProp' }],
383+
},
384+
],
385+
},
386+
{
387+
code: `
388+
/** @deprecated */
389+
const deprecatedValue = 45;
390+
const bar = deprecatedValue;
391+
`,
392+
options: [
393+
{
394+
allow: ['deprecatedValue'],
395+
},
396+
],
397+
},
398+
{
399+
code: `
347400
import { exists } from 'fs';
401+
exists('/foo');
402+
`,
403+
options: [
404+
{
405+
allow: [
406+
{
407+
from: 'package',
408+
name: 'exists',
409+
package: 'fs',
410+
},
411+
],
412+
},
413+
],
414+
},
415+
{
416+
code: `
417+
const { exists } = import('fs');
348418
exists('/foo');
349419
`,
350420
options: [
@@ -2915,6 +2985,37 @@ class B extends A {
29152985
},
29162986
],
29172987
},
2988+
{
2989+
code: `
2990+
import { exists } from 'fs';
2991+
exists('/foo');
2992+
`,
2993+
errors: [
2994+
{
2995+
column: 1,
2996+
data: {
2997+
name: 'exists',
2998+
reason:
2999+
'Since v1.0.0 - Use {@link stat} or {@link access} instead.',
3000+
},
3001+
endColumn: 7,
3002+
endLine: 3,
3003+
line: 3,
3004+
messageId: 'deprecatedWithReason',
3005+
},
3006+
],
3007+
options: [
3008+
{
3009+
allow: [
3010+
{
3011+
from: 'package',
3012+
name: 'exists',
3013+
package: 'hoge',
3014+
},
3015+
],
3016+
},
3017+
],
3018+
},
29183019
{
29193020
code: `
29203021
declare class A {

packages/type-utils/src/TypeOrValueSpecifier.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { TSESTree } from '@typescript-eslint/types';
12
import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema';
23
import type * as ts from 'typescript';
34

5+
import { AST_NODE_TYPES } from '@typescript-eslint/types';
46
import * as tsutils from 'ts-api-utils';
57

68
import { specifierNameMatches } from './typeOrValueSpecifiers/specifierNameMatches';
@@ -219,3 +221,69 @@ export const typeMatchesSomeSpecifier = (
219221
program: ts.Program,
220222
): boolean =>
221223
specifiers.some(specifier => typeMatchesSpecifier(type, specifier, program));
224+
225+
const getSpecifierNames = (specifierName: string | string[]): string[] => {
226+
return typeof specifierName === 'string' ? [specifierName] : specifierName;
227+
};
228+
229+
const getStaticName = (node: TSESTree.Node): string | undefined => {
230+
if (
231+
node.type === AST_NODE_TYPES.Identifier ||
232+
node.type === AST_NODE_TYPES.JSXIdentifier ||
233+
node.type === AST_NODE_TYPES.PrivateIdentifier
234+
) {
235+
return node.name;
236+
}
237+
238+
if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') {
239+
return node.value;
240+
}
241+
242+
return undefined;
243+
};
244+
245+
export function valueMatchesSpecifier(
246+
node: TSESTree.Node,
247+
specifier: TypeOrValueSpecifier,
248+
program: ts.Program,
249+
type: ts.Type,
250+
): boolean {
251+
const staticName = getStaticName(node);
252+
if (!staticName) {
253+
return false;
254+
}
255+
256+
if (typeof specifier === 'string') {
257+
return specifier === staticName;
258+
}
259+
260+
if (!getSpecifierNames(specifier.name).includes(staticName)) {
261+
return false;
262+
}
263+
264+
if (specifier.from === 'package') {
265+
const symbol = type.getSymbol() ?? type.aliasSymbol;
266+
const declarations = symbol?.getDeclarations() ?? [];
267+
const declarationFiles = declarations.map(declaration =>
268+
declaration.getSourceFile(),
269+
);
270+
return typeDeclaredInPackageDeclarationFile(
271+
specifier.package,
272+
declarations,
273+
declarationFiles,
274+
program,
275+
);
276+
}
277+
278+
return true;
279+
}
280+
281+
export const valueMatchesSomeSpecifier = (
282+
node: TSESTree.Node,
283+
specifiers: TypeOrValueSpecifier[] = [],
284+
program: ts.Program,
285+
type: ts.Type,
286+
): boolean =>
287+
specifiers.some(specifier =>
288+
valueMatchesSpecifier(node, specifier, program, type),
289+
);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy