@ms-cloudpack/eslint-plugin-deprecated 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/package.json +37 -0
- package/src/__fixtures__/createInvalidCases.ts +45 -0
- package/src/__fixtures__/deprecated.ts +153 -0
- package/src/__fixtures__/file.ts +1 -0
- package/src/__fixtures__/tsconfig.json +13 -0
- package/src/index.ts +26 -0
- package/src/rules/no-deprecated.ts +271 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @ms-cloudpack/eslint-plugin-deprecated
|
|
2
|
+
|
|
3
|
+
Fork of `eslint-plugin-etc`'s [`no-deprecated` rule](https://github.com/cartant/eslint-plugin-etc/blob/main/source/rules/no-deprecated.ts) with updated dependencies.
|
|
4
|
+
|
|
5
|
+
This plugin **requires type information** (see [setup instructions](https://typescript-eslint.io/linting/typed-linting)) and only works with flat config.
|
|
6
|
+
|
|
7
|
+
## Rules
|
|
8
|
+
|
|
9
|
+
### `@ms-cloudpack/no-deprecated`
|
|
10
|
+
|
|
11
|
+
Forbids the use of deprecated APIs.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
|
|
15
|
+
- `ignoredSelectors` (optional): Ignore deprecated APIs matching these selectors, using [esquery syntax](https://github.com/estools/esquery).
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ms-cloudpack/eslint-plugin-deprecated",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "ESLint rule banning usage of deprecated APIs",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": "./src/index.ts",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.18.0"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "cloudpack-scripts build",
|
|
13
|
+
"lint": "cloudpack-scripts lint",
|
|
14
|
+
"test": "cloudpack-scripts test"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@phenomnomnominal/tsquery": "^6.1.3",
|
|
18
|
+
"@typescript-eslint/utils": "^8.0.0",
|
|
19
|
+
"tsutils": "^3.21.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
23
|
+
"typescript": ">=5.0.0",
|
|
24
|
+
"typescript-eslint": "^8.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@ms-cloudpack/scripts": "workspace:^",
|
|
28
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
29
|
+
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
30
|
+
"eslint": "^9.0.0",
|
|
31
|
+
"eslint-plugin-eslint-plugin": "^7.3.3",
|
|
32
|
+
"typescript": "~5.9.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src/**/!(*.test.*)"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heavily modified from https://github.com/cartant/eslint-etc/blob/main/source/from-fixture.ts
|
|
3
|
+
* for no-deprecated rule tests.
|
|
4
|
+
*/
|
|
5
|
+
import type { InvalidTestCase, TestCaseError } from '@typescript-eslint/rule-tester';
|
|
6
|
+
|
|
7
|
+
const markerRegexp = /^(?<indent>\s*)(?<marker>~+)$/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a string with marker comment to an invalid test case.
|
|
11
|
+
* ```
|
|
12
|
+
* let a: DeprecatedInterface;
|
|
13
|
+
* ~~~~~~~~~~~~~~~~~~~
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function createInvalidCases<MessageIds extends string, TData extends Readonly<Record<string, unknown>>>(
|
|
17
|
+
cases: Omit<InvalidTestCase<MessageIds, never>, 'code' | 'errors'> &
|
|
18
|
+
{
|
|
19
|
+
code: string;
|
|
20
|
+
errors: { messageId: MessageIds; data?: TData }[];
|
|
21
|
+
}[],
|
|
22
|
+
): InvalidTestCase<MessageIds, never>[] {
|
|
23
|
+
return cases.map(({ code, errors: partialErrors, ...testCase }) => {
|
|
24
|
+
const lines: string[] = [];
|
|
25
|
+
const errors: TestCaseError<MessageIds>[] = [];
|
|
26
|
+
|
|
27
|
+
for (const line of code.split('\n')) {
|
|
28
|
+
const match = line.match(markerRegexp);
|
|
29
|
+
if (match?.groups) {
|
|
30
|
+
const error = partialErrors[errors.length];
|
|
31
|
+
if (!error) {
|
|
32
|
+
throw new Error('More marker comments found than expected');
|
|
33
|
+
}
|
|
34
|
+
const column = match.groups.indent.length + 1;
|
|
35
|
+
const endColumn = column + match.groups.marker.length;
|
|
36
|
+
const lineNum = lines.length;
|
|
37
|
+
errors.push({ ...error, line: lineNum, column, endLine: lineNum, endColumn });
|
|
38
|
+
} else {
|
|
39
|
+
lines.push(line);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { code: lines.join('\n'), errors, ...testCase };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copied from https://github.com/cartant/eslint-plugin-etc/blob/main/tests/modules/deprecated.ts
|
|
3
|
+
* due to lack of updates for new dependency versions.
|
|
4
|
+
*/
|
|
5
|
+
/** */
|
|
6
|
+
|
|
7
|
+
export interface NotDeprecatedInterface {
|
|
8
|
+
notDeprecatedProperty: string;
|
|
9
|
+
notDeprecatedMethod(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type NotDeprecatedType = {
|
|
13
|
+
notDeprecatedProperty: string;
|
|
14
|
+
notDeprecatedMethod(): void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class NotDeprecatedClass {
|
|
18
|
+
static notDeprecatedStaticMethod(): void {}
|
|
19
|
+
notDeprecatedProperty: string = '';
|
|
20
|
+
get notDeprecatedGetter(): string {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
set notDeprecatedSetter(value: string) {}
|
|
24
|
+
notDeprecatedMethod(): void {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Note: this gets an editor diagnostics error, but is correct at actual build time
|
|
28
|
+
// @ts-expect-error -- not valid with erasable syntax, but fine for a test fixture
|
|
29
|
+
export enum NotDeprecatedEnum {
|
|
30
|
+
NotDeprecatedMember = 1,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const notDeprecatedVariable = {};
|
|
34
|
+
|
|
35
|
+
export function notDeprecatedFunction(): void {}
|
|
36
|
+
|
|
37
|
+
// Deprecated
|
|
38
|
+
|
|
39
|
+
/** @deprecated Don't use this */
|
|
40
|
+
export interface DeprecatedInterface {
|
|
41
|
+
notDeprecatedProperty: string;
|
|
42
|
+
notDeprecatedMethod(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @deprecated Don't use this */
|
|
46
|
+
export type DeprecatedType = {
|
|
47
|
+
notDeprecatedProperty: string;
|
|
48
|
+
notDeprecatedMethod(): void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** @deprecated Don't use this */
|
|
52
|
+
export class DeprecatedClass {
|
|
53
|
+
static notDeprecatedStaticMethod(): void {}
|
|
54
|
+
notDeprecatedProperty: string = '';
|
|
55
|
+
get notDeprecatedGetter(): string {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
set notDeprecatedSetter(value: string) {}
|
|
59
|
+
notDeprecatedMethod(): void {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @deprecated Don't use this */
|
|
63
|
+
// @ts-expect-error -- not valid with erasable syntax, but fine for a test fixture
|
|
64
|
+
export enum DeprecatedEnum {
|
|
65
|
+
NotDeprecatedMember = 1,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @deprecated Don't use this */
|
|
69
|
+
export const deprecatedVariable = {};
|
|
70
|
+
|
|
71
|
+
/** @deprecated Don't use this */
|
|
72
|
+
export function deprecatedFunction(): void {}
|
|
73
|
+
|
|
74
|
+
// Some proeprties/methods deprecated
|
|
75
|
+
|
|
76
|
+
export interface SomeDeprecatedInterface {
|
|
77
|
+
/** @deprecated Don't use this */
|
|
78
|
+
deprecatedProperty: string;
|
|
79
|
+
notDeprecatedProperty: string;
|
|
80
|
+
/** @deprecated Don't use this */
|
|
81
|
+
deprecatedMethod(): void;
|
|
82
|
+
notDeprecatedMethod(): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type SomeDeprecatedType = {
|
|
86
|
+
/** @deprecated Don't use this */
|
|
87
|
+
deprecatedProperty: string;
|
|
88
|
+
notDeprecatedProperty: string;
|
|
89
|
+
/** @deprecated Don't use this */
|
|
90
|
+
deprecatedMethod(): void;
|
|
91
|
+
notDeprecatedMethod(): void;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export class SomeDeprecatedClass {
|
|
95
|
+
/** @deprecated Don't use this */
|
|
96
|
+
static deprecatedStaticMethod(): void {}
|
|
97
|
+
static notDeprecatedStaticMethod(): void {}
|
|
98
|
+
/** @deprecated Don't use this */
|
|
99
|
+
deprecatedProperty: string = '';
|
|
100
|
+
notDeprecatedProperty: string = '';
|
|
101
|
+
/** @deprecated Don't use this */
|
|
102
|
+
get deprecatedGetter(): string {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
get notDeprecatedGetter(): string {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
/** @deprecated Don't use this */
|
|
109
|
+
set deprecatedSetter(value: string) {}
|
|
110
|
+
set notDeprecatedSetter(value: string) {}
|
|
111
|
+
/** @deprecated Don't use this */
|
|
112
|
+
deprecatedMethod(): void {}
|
|
113
|
+
notDeprecatedMethod(): void {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// @ts-expect-error -- not valid with erasable syntax, but fine for a test fixture
|
|
117
|
+
export enum SomeDeprecatedEnum {
|
|
118
|
+
/** @deprecated Don't use this */
|
|
119
|
+
DeprecatedMember = 1,
|
|
120
|
+
NotDeprecatedMember = 2,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Some deprecated signatures
|
|
124
|
+
|
|
125
|
+
export class DeprecatedSignatureClass {
|
|
126
|
+
static deprecatedSignatureStaticMethod(value: number): void;
|
|
127
|
+
/** @deprecated Don't use this */
|
|
128
|
+
static deprecatedSignatureStaticMethod(value: string): void;
|
|
129
|
+
static deprecatedSignatureStaticMethod(_value: unknown): void {}
|
|
130
|
+
deprecatedSignatureMethod(value: number): void;
|
|
131
|
+
/** @deprecated Don't use this */
|
|
132
|
+
deprecatedSignatureMethod(value: string): void;
|
|
133
|
+
deprecatedSignatureMethod(_value: unknown): void {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function deprecatedSignatureFunction(value: number): void;
|
|
137
|
+
/** @deprecated Don't use this */
|
|
138
|
+
export function deprecatedSignatureFunction(value: string): void;
|
|
139
|
+
export function deprecatedSignatureFunction(_value: unknown): void {}
|
|
140
|
+
|
|
141
|
+
// Deprecated constructor
|
|
142
|
+
|
|
143
|
+
export class DeprecatedConstructorClass {
|
|
144
|
+
/** @deprecated Don't use this */
|
|
145
|
+
constructor() {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class DeprecatedConstructorSignatureClass {
|
|
149
|
+
constructor(value: number);
|
|
150
|
+
/** @deprecated Don't use this */
|
|
151
|
+
constructor(value: string);
|
|
152
|
+
constructor(_value: unknown) {}
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// placeholder for tests (must exist on disk, but contents will be virtualized)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// tsconfig used as `project` for no-deprecated tests
|
|
2
|
+
{
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"lib": ["esnext"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"target": "esnext",
|
|
9
|
+
// very important for test performance!
|
|
10
|
+
"types": []
|
|
11
|
+
},
|
|
12
|
+
"include": ["file.ts", "deprecated.ts"]
|
|
13
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
4
|
+
import noDeprecated from './rules/no-deprecated.ts';
|
|
5
|
+
|
|
6
|
+
const { name, version } = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, '../package.json'), 'utf8')) as {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const plugin = {
|
|
12
|
+
meta: { name, version, namespace: '@ms-cloudpack' },
|
|
13
|
+
configs: {
|
|
14
|
+
recommended: {
|
|
15
|
+
rules: {
|
|
16
|
+
'@ms-cloudpack/no-deprecated': 'error',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
'@ms-cloudpack/no-deprecated': noDeprecated,
|
|
22
|
+
},
|
|
23
|
+
// Use `satisfies` to preserve the original property names
|
|
24
|
+
} satisfies TSESLint.FlatConfig.Plugin;
|
|
25
|
+
|
|
26
|
+
export default plugin;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { tsquery } from '@phenomnomnominal/tsquery';
|
|
5
|
+
import tsutils from 'tsutils';
|
|
6
|
+
|
|
7
|
+
export type MessageIds = 'forbidden';
|
|
8
|
+
export type MessageData = { name: string; comment: string };
|
|
9
|
+
type Options = { ignoredSelectors?: string[] };
|
|
10
|
+
|
|
11
|
+
const deprecatedNamesByProgram = new WeakMap<ts.Program, Set<string>>();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Copied from https://github.com/cartant/eslint-plugin-etc/blob/main/source/rules/no-deprecated.ts
|
|
15
|
+
* due to lack of updates for new dependency versions.
|
|
16
|
+
* Also added a new `ignoredSelectors` option.
|
|
17
|
+
*
|
|
18
|
+
* License for `eslint-plugin-etc` (excluding new `ignoredSelectors` option):
|
|
19
|
+
*
|
|
20
|
+
* MIT License
|
|
21
|
+
*
|
|
22
|
+
* Copyright (c) 2019-2021 Nicholas Jamieson and contributors
|
|
23
|
+
*
|
|
24
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
25
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
26
|
+
* in the Software without restriction, including without limitation the rights
|
|
27
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
28
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
29
|
+
* furnished to do so, subject to the following conditions:
|
|
30
|
+
*
|
|
31
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
32
|
+
* copies or substantial portions of the Software.
|
|
33
|
+
*
|
|
34
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
35
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
36
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
37
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
38
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
39
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
40
|
+
* SOFTWARE.
|
|
41
|
+
*/
|
|
42
|
+
const rule: TSESLint.RuleModule<MessageIds, [Options?]> = {
|
|
43
|
+
defaultOptions: [],
|
|
44
|
+
meta: {
|
|
45
|
+
docs: {
|
|
46
|
+
description: 'disallow the use of deprecated APIs',
|
|
47
|
+
},
|
|
48
|
+
messages: {
|
|
49
|
+
forbidden: `"{{name}}" is deprecated: {{comment}}`,
|
|
50
|
+
},
|
|
51
|
+
schema: [
|
|
52
|
+
{
|
|
53
|
+
properties: {
|
|
54
|
+
ignoredSelectors: {
|
|
55
|
+
description: 'Selectors for nodes to ignore',
|
|
56
|
+
type: 'array',
|
|
57
|
+
items: { type: 'string' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
type: 'object',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
defaultOptions: [{}],
|
|
64
|
+
type: 'problem',
|
|
65
|
+
},
|
|
66
|
+
create: (context) => {
|
|
67
|
+
const [{ ignoredSelectors } = {}] = context.options;
|
|
68
|
+
let ignoredNodes: ts.Node[] | undefined;
|
|
69
|
+
|
|
70
|
+
const { esTreeNodeToTSNodeMap, program } = ESLintUtils.getParserServices(context);
|
|
71
|
+
const typeChecker = program.getTypeChecker();
|
|
72
|
+
|
|
73
|
+
let deprecatedNames = deprecatedNamesByProgram.get(program);
|
|
74
|
+
if (!deprecatedNames) {
|
|
75
|
+
deprecatedNames = findTaggedNames('deprecated', program);
|
|
76
|
+
deprecatedNamesByProgram.set(program, deprecatedNames);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
Identifier: (node) => {
|
|
81
|
+
const parentType = node.parent?.type;
|
|
82
|
+
if (
|
|
83
|
+
parentType === 'ExportSpecifier' ||
|
|
84
|
+
parentType === 'ImportDefaultSpecifier' ||
|
|
85
|
+
parentType === 'ImportNamespaceSpecifier' ||
|
|
86
|
+
parentType === 'ImportSpecifier'
|
|
87
|
+
) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const identifier = esTreeNodeToTSNodeMap.get(node) as ts.Identifier;
|
|
91
|
+
if (!deprecatedNames.has(identifier.text) || isDeclaration(identifier)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// NEW: check if this node matches any ignored selectors
|
|
96
|
+
if (ignoredSelectors?.length) {
|
|
97
|
+
ignoredNodes ??= tsquery(identifier.getSourceFile(), ignoredSelectors.join(', '));
|
|
98
|
+
if (ignoredNodes.includes(identifier)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const tags = getTags('deprecated', identifier, typeChecker);
|
|
104
|
+
for (const tag of tags) {
|
|
105
|
+
context.report({
|
|
106
|
+
data: {
|
|
107
|
+
comment: tag.trim().replace(/\s+/g, ' '),
|
|
108
|
+
name: identifier.text,
|
|
109
|
+
},
|
|
110
|
+
messageId: 'forbidden',
|
|
111
|
+
node,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default rule;
|
|
120
|
+
|
|
121
|
+
function isDeclaration(identifier: ts.Identifier): boolean {
|
|
122
|
+
const parent = identifier.parent;
|
|
123
|
+
switch (parent.kind) {
|
|
124
|
+
case ts.SyntaxKind.ClassDeclaration:
|
|
125
|
+
case ts.SyntaxKind.ClassExpression:
|
|
126
|
+
case ts.SyntaxKind.InterfaceDeclaration:
|
|
127
|
+
case ts.SyntaxKind.TypeParameter:
|
|
128
|
+
case ts.SyntaxKind.FunctionExpression:
|
|
129
|
+
case ts.SyntaxKind.FunctionDeclaration:
|
|
130
|
+
case ts.SyntaxKind.LabeledStatement:
|
|
131
|
+
case ts.SyntaxKind.JsxAttribute:
|
|
132
|
+
case ts.SyntaxKind.MethodDeclaration:
|
|
133
|
+
case ts.SyntaxKind.MethodSignature:
|
|
134
|
+
case ts.SyntaxKind.PropertySignature:
|
|
135
|
+
case ts.SyntaxKind.TypeAliasDeclaration:
|
|
136
|
+
case ts.SyntaxKind.GetAccessor:
|
|
137
|
+
case ts.SyntaxKind.SetAccessor:
|
|
138
|
+
case ts.SyntaxKind.EnumDeclaration:
|
|
139
|
+
case ts.SyntaxKind.ModuleDeclaration:
|
|
140
|
+
return true;
|
|
141
|
+
case ts.SyntaxKind.VariableDeclaration:
|
|
142
|
+
case ts.SyntaxKind.Parameter:
|
|
143
|
+
case ts.SyntaxKind.PropertyDeclaration:
|
|
144
|
+
case ts.SyntaxKind.EnumMember:
|
|
145
|
+
case ts.SyntaxKind.ImportEqualsDeclaration:
|
|
146
|
+
return (parent as ts.NamedDeclaration).name === identifier;
|
|
147
|
+
case ts.SyntaxKind.PropertyAssignment:
|
|
148
|
+
return (
|
|
149
|
+
(parent as ts.PropertyAssignment).name === identifier &&
|
|
150
|
+
!tsutils.isReassignmentTarget(identifier.parent.parent as ts.ObjectLiteralExpression)
|
|
151
|
+
);
|
|
152
|
+
case ts.SyntaxKind.BindingElement:
|
|
153
|
+
// return true for `b` in `const {a: b} = obj`
|
|
154
|
+
return (
|
|
155
|
+
(parent as ts.BindingElement).name === identifier && (parent as ts.BindingElement).propertyName !== undefined
|
|
156
|
+
);
|
|
157
|
+
default:
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get all identifiers with jsdoc tag `@tagName` in the program.
|
|
164
|
+
*/
|
|
165
|
+
function findTaggedNames(tagName: string, program: ts.Program): Set<string> {
|
|
166
|
+
const taggedNames = new Set<string>();
|
|
167
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
168
|
+
if (!sourceFile.text.includes(`@${tagName}`)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const nodes = tsquery(
|
|
172
|
+
sourceFile,
|
|
173
|
+
`ClassDeclaration, Constructor, EnumDeclaration, EnumMember, FunctionDeclaration, GetAccessor, ` +
|
|
174
|
+
`InterfaceDeclaration, MethodDeclaration, MethodSignature, PropertyDeclaration, PropertySignature, ` +
|
|
175
|
+
`SetAccessor, TypeAliasDeclaration, VariableDeclaration`,
|
|
176
|
+
);
|
|
177
|
+
for (const node of nodes) {
|
|
178
|
+
const tags = ts.getJSDocTags(node);
|
|
179
|
+
if (tags.some((tag) => tag.tagName.text === tagName)) {
|
|
180
|
+
const name = ts.isConstructorDeclaration(node)
|
|
181
|
+
? node.parent.name
|
|
182
|
+
: (node as ts.Node & { name?: ts.Identifier }).name;
|
|
183
|
+
if (name?.text) {
|
|
184
|
+
taggedNames.add(name.text);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return taggedNames;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get the text of all tags of `node`'s type matching `tagName`.
|
|
194
|
+
*/
|
|
195
|
+
function getTags(tagName: string, node: ts.Identifier, tc: ts.TypeChecker): string[] {
|
|
196
|
+
const callExpression = getCallExpression(node);
|
|
197
|
+
if (callExpression) {
|
|
198
|
+
const signature = tc.getResolvedSignature(callExpression);
|
|
199
|
+
const tags = signature && findTags(tagName, signature.getJsDocTags());
|
|
200
|
+
if (tags?.length) {
|
|
201
|
+
return tags;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let symbol: ts.Symbol | undefined;
|
|
206
|
+
const parent = node.parent;
|
|
207
|
+
if (parent.kind === ts.SyntaxKind.BindingElement) {
|
|
208
|
+
symbol = tc.getTypeAtLocation(parent.parent).getProperty(node.text);
|
|
209
|
+
} else if (
|
|
210
|
+
(tsutils.isPropertyAssignment(parent) && parent.name === node) ||
|
|
211
|
+
(tsutils.isShorthandPropertyAssignment(parent) && parent.name === node && tsutils.isReassignmentTarget(node))
|
|
212
|
+
) {
|
|
213
|
+
symbol = tc.getPropertySymbolOfDestructuringAssignment(node);
|
|
214
|
+
} else {
|
|
215
|
+
symbol = tc.getSymbolAtLocation(node);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (symbol && tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias)) {
|
|
219
|
+
symbol = tc.getAliasedSymbol(symbol);
|
|
220
|
+
}
|
|
221
|
+
if (
|
|
222
|
+
!symbol ||
|
|
223
|
+
// if this is a CallExpression and the declaration is a function or method,
|
|
224
|
+
// stop here to avoid collecting JsDoc of all overload signatures
|
|
225
|
+
(callExpression && isFunctionOrMethod(symbol.declarations))
|
|
226
|
+
) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
return findTags(tagName, symbol.getJsDocTags());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get the text of all `tags` matching `tagName`.
|
|
234
|
+
*/
|
|
235
|
+
function findTags(tagName: string, tags: ts.JSDocTagInfo[]): string[] {
|
|
236
|
+
const result: string[] = [];
|
|
237
|
+
for (const tag of tags) {
|
|
238
|
+
if (tag.name === tagName) {
|
|
239
|
+
const { text = '' } = tag;
|
|
240
|
+
if (typeof text === 'string') {
|
|
241
|
+
result.push(text);
|
|
242
|
+
} else {
|
|
243
|
+
result.push(text.reduce((t, part) => t + part.text, ''));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getCallExpression(node: ts.Expression): ts.CallLikeExpression | undefined {
|
|
251
|
+
let parent = node.parent;
|
|
252
|
+
if (tsutils.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
253
|
+
parent = parent.parent;
|
|
254
|
+
}
|
|
255
|
+
return tsutils.isCallLikeExpression(parent) ? parent : undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isFunctionOrMethod(declarations: ts.Declaration[] | undefined): boolean {
|
|
259
|
+
if (!declarations?.length) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
switch (declarations[0].kind) {
|
|
263
|
+
case ts.SyntaxKind.MethodDeclaration:
|
|
264
|
+
case ts.SyntaxKind.FunctionDeclaration:
|
|
265
|
+
case ts.SyntaxKind.FunctionExpression:
|
|
266
|
+
case ts.SyntaxKind.MethodSignature:
|
|
267
|
+
return true;
|
|
268
|
+
default:
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|