@eagleoutice/eslint-config-flowr 2.1.8 → 2.1.10
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 +11 -1
- package/index.js +2 -1
- package/package.json +1 -1
- package/pattern-type.d.ts +2 -0
- package/patterns.d.ts +1 -3
- package/patterns.js +488 -209
- package/rules/replacement-pattern.js +26 -7
- package/rules/ts-types.d.ts +7 -0
- package/rules/use-instead.js +26 -15
- package/rules/util.d.ts +13 -1
- package/rules/util.js +111 -6
|
@@ -28,6 +28,20 @@ function render(template, captures, node, sourceCode) {
|
|
|
28
28
|
});
|
|
29
29
|
return failed ? undefined : filled;
|
|
30
30
|
}
|
|
31
|
+
/** per program, whether one of its files matches a `helperIn` path; every file of a run shares the program */
|
|
32
|
+
const helperFiles = new WeakMap();
|
|
33
|
+
function programHas(program, path) {
|
|
34
|
+
let known = helperFiles.get(program);
|
|
35
|
+
if (known === undefined) {
|
|
36
|
+
helperFiles.set(program, known = new Map());
|
|
37
|
+
}
|
|
38
|
+
let has = known.get(path);
|
|
39
|
+
if (has === undefined) {
|
|
40
|
+
has = program.getSourceFiles().some(file => file.fileName.replaceAll('\\', '/').includes(path));
|
|
41
|
+
known.set(path, has);
|
|
42
|
+
}
|
|
43
|
+
return has;
|
|
44
|
+
}
|
|
31
45
|
const rule = {
|
|
32
46
|
meta: {
|
|
33
47
|
type: 'suggestion',
|
|
@@ -56,6 +70,8 @@ const rule = {
|
|
|
56
70
|
replace: { type: 'string' },
|
|
57
71
|
/** path to a substring of the file its symbol must be declared in, this is what keeps the pattern flowR-specific */
|
|
58
72
|
declaredIn: { type: 'object', additionalProperties: { type: 'string' } },
|
|
73
|
+
/** path to a substring of the file the replacement's helper lives in, the pattern is silent in a program without it */
|
|
74
|
+
helperIn: { type: 'string' },
|
|
59
75
|
/** pairs of paths that have to spell the same code, esquery has no back-references */
|
|
60
76
|
sameText: { type: 'array', items: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 2 } },
|
|
61
77
|
/** `true` forces the autofix, `false` forces a suggestion; by default it is fixed when the replacement is already in scope */
|
|
@@ -89,12 +105,12 @@ const rule = {
|
|
|
89
105
|
bySelector.set(pattern.selector, [pattern]);
|
|
90
106
|
}
|
|
91
107
|
}
|
|
92
|
-
/**
|
|
93
|
-
* The match only counts if every named node resolves to a declaration of the expected flowR file.
|
|
94
|
-
* The declaring file itself is exempt, it is where the helper is built.
|
|
95
|
-
*/
|
|
108
|
+
/** The match only counts if every named node resolves to a declaration of the expected flowR file. */
|
|
96
109
|
function isFlowr(node, pattern) {
|
|
97
|
-
if (!pattern.
|
|
110
|
+
if (pattern.helperIn !== undefined && (!services || !programHas(services.program, pattern.helperIn))) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
else if (!pattern.declaredIn) {
|
|
98
114
|
return true;
|
|
99
115
|
}
|
|
100
116
|
else if (!services || !checker) {
|
|
@@ -102,7 +118,7 @@ const rule = {
|
|
|
102
118
|
}
|
|
103
119
|
return Object.entries(pattern.declaredIn).every(([path, expected]) => {
|
|
104
120
|
const file = (0, util_1.declarationFile)((0, util_1.symbolOf)(resolvePath(node, path), services, checker));
|
|
105
|
-
return file !== undefined && file
|
|
121
|
+
return file !== undefined && file.replaceAll('\\', '/').includes(expected);
|
|
106
122
|
});
|
|
107
123
|
}
|
|
108
124
|
/** esquery cannot demand that two sub-nodes are the same, so the pattern names the pairs itself */
|
|
@@ -121,7 +137,10 @@ const rule = {
|
|
|
121
137
|
const replacement = render(pattern.replace, captures, node, sourceCode);
|
|
122
138
|
const message = (pattern.message === undefined ? undefined : render(pattern.message, captures, node, sourceCode))
|
|
123
139
|
?? (replacement ? `Use \`${replacement}\` here.` : `\`${pattern.id ?? pattern.selector}\` has a helper replacement.`);
|
|
124
|
-
if (replacement
|
|
140
|
+
if (replacement !== undefined && (0, util_1.insideReplacement)(node, replacement, sourceCode, services)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
else if (replacement === undefined) {
|
|
125
144
|
context.report({ node, messageId: 'replacement', data: { message } });
|
|
126
145
|
return;
|
|
127
146
|
}
|
package/rules/ts-types.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface TsSourceFile {
|
|
|
7
7
|
}
|
|
8
8
|
export interface TsDeclaration {
|
|
9
9
|
getSourceFile(): TsSourceFile | undefined;
|
|
10
|
+
/** what a property or variable is set to, `frames: framesOf` binds `framesOf` */
|
|
11
|
+
initializer?: unknown;
|
|
10
12
|
}
|
|
11
13
|
export interface TsJsDocTag {
|
|
12
14
|
name: string;
|
|
@@ -27,11 +29,16 @@ export interface TsTypeChecker {
|
|
|
27
29
|
getSymbolAtLocation(node: unknown): TsSymbol | undefined;
|
|
28
30
|
getAliasedSymbol(symbol: TsSymbol): TsSymbol;
|
|
29
31
|
getTypeOfSymbolAtLocation(symbol: TsSymbol, location: TsDeclaration): TsType;
|
|
32
|
+
/** what a module exports, its symbol taken from the source file */
|
|
33
|
+
getExportsOfModule(module: TsSymbol): readonly TsSymbol[];
|
|
34
|
+
/** the variable a shorthand property `{ framesOf }` stands for */
|
|
35
|
+
getShorthandAssignmentValueSymbol(declaration: TsDeclaration): TsSymbol | undefined;
|
|
30
36
|
}
|
|
31
37
|
/** What typescript-eslint hands to a type-aware rule. */
|
|
32
38
|
export interface TypeServices {
|
|
33
39
|
program: {
|
|
34
40
|
getTypeChecker(): TsTypeChecker;
|
|
41
|
+
getSourceFiles(): readonly TsSourceFile[];
|
|
35
42
|
};
|
|
36
43
|
esTreeNodeToTSNodeMap: {
|
|
37
44
|
get(node: unknown): unknown;
|
package/rules/use-instead.js
CHANGED
|
@@ -35,10 +35,16 @@ function cachedMemberTags(symbol, checker) {
|
|
|
35
35
|
memberCache.set(symbol, tags);
|
|
36
36
|
return tags;
|
|
37
37
|
}
|
|
38
|
-
/** `
|
|
38
|
+
/** `X.member` around a reference to `X`, with the member it reads */
|
|
39
|
+
function memberRead(node) {
|
|
40
|
+
const parent = node.parent;
|
|
41
|
+
return parent?.type === 'MemberExpression' && parent.object === node && !parent.computed && parent.property.type === 'Identifier'
|
|
42
|
+
? { node: parent, member: parent.property.name } : undefined;
|
|
43
|
+
}
|
|
44
|
+
/** `{@link Resolve.toValue}` becomes `Resolve.toValue`. */
|
|
39
45
|
function parseTarget(tag) {
|
|
40
46
|
const display = tag.replace(/^\{\s*@link\s*/, '').replace(/\s*\|.*$/, '').replace(/\s*\}\s*$/, '').trim();
|
|
41
|
-
return { display
|
|
47
|
+
return { display };
|
|
42
48
|
}
|
|
43
49
|
/** The references that build the replacement in the first place: helper-object wiring and re-exports. */
|
|
44
50
|
function isWiring(identifier) {
|
|
@@ -74,6 +80,7 @@ const rule = {
|
|
|
74
80
|
messages: {
|
|
75
81
|
useInstead: 'Do not use `{{name}}` directly, use `{{replacement}}` instead.',
|
|
76
82
|
useInsteadPlain: 'Do not use `{{name}}` directly, see its documentation for the replacement.',
|
|
83
|
+
useInsteadMissing: 'Do not use `{{name}}` directly, use `{{replacement}}` instead, which has no counterpart of `{{member}}` yet.',
|
|
77
84
|
suggest: 'Replace with `{{replacement}}`.'
|
|
78
85
|
}
|
|
79
86
|
},
|
|
@@ -87,21 +94,28 @@ const rule = {
|
|
|
87
94
|
/* helper objects are PascalCase, so member checks stay cheap */
|
|
88
95
|
const helperObjectPattern = new RegExp(context.options[0]?.helperObjectPattern ?? '^[A-Z]');
|
|
89
96
|
const candidates = new Map();
|
|
90
|
-
/** the helper objects declared here, a file may use what it wraps */
|
|
91
|
-
const owned = new Set();
|
|
92
97
|
function report(node, name, tag) {
|
|
93
|
-
const { display
|
|
94
|
-
|
|
98
|
+
const { display } = parseTarget(tag);
|
|
99
|
+
/* only the wrapper itself may use what it wraps, anywhere else the replacement holds */
|
|
100
|
+
if ((0, util_1.insideReplacement)(node, display, sourceCode, services) || (0, util_1.suppressed)(context, node, ['use-instead'])) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
/* `Old.member` moves to the member of the replacement that takes its place, which may be named differently */
|
|
104
|
+
const access = memberRead(node);
|
|
105
|
+
const counterpart = display === '' || access === undefined || !services ? null : (0, util_1.counterpartOf)(node, access.member, display, sourceCode, services);
|
|
106
|
+
if (counterpart === undefined) {
|
|
107
|
+
context.report({ node, messageId: 'useInsteadMissing', data: { name, replacement: display, member: access?.member ?? '' } });
|
|
95
108
|
return;
|
|
96
109
|
}
|
|
97
|
-
const
|
|
110
|
+
const [target, text] = counterpart === null || access === undefined ? [node, display] : [access.node, `${display}.${counterpart}`];
|
|
111
|
+
const fix = (fixer) => fixer.replaceText(target, text);
|
|
98
112
|
/* fixed on the spot where the helper is already in scope, offered as a suggestion otherwise */
|
|
99
113
|
const applicable = display !== '' && (0, util_1.isApplicable)(context, node, display);
|
|
100
114
|
context.report({
|
|
101
115
|
node,
|
|
102
116
|
messageId: display ? 'useInstead' : 'useInsteadPlain',
|
|
103
|
-
data: { name, replacement:
|
|
104
|
-
...display === '' ? {} : applicable ? { fix } : { suggest: [{ messageId: 'suggest', data: { replacement:
|
|
117
|
+
data: { name, replacement: text },
|
|
118
|
+
...display === '' ? {} : applicable ? { fix } : { suggest: [{ messageId: 'suggest', data: { replacement: text }, fix }] }
|
|
105
119
|
});
|
|
106
120
|
}
|
|
107
121
|
return {
|
|
@@ -111,18 +125,15 @@ const rule = {
|
|
|
111
125
|
const foreign = [];
|
|
112
126
|
for (const variable of module?.variables ?? []) {
|
|
113
127
|
const declaration = variable.defs[0]?.name;
|
|
114
|
-
|
|
128
|
+
/* only what is imported is someone else's to replace, the file's own declarations may use themselves;
|
|
129
|
+
asked of the code rather than of file paths, which differ between systems */
|
|
130
|
+
if (declaration?.type !== 'Identifier' || variable.defs[0]?.type !== 'ImportBinding') {
|
|
115
131
|
continue;
|
|
116
132
|
}
|
|
117
133
|
const symbol = (0, util_1.symbolOf)(declaration, services, checker);
|
|
118
134
|
if (!symbol) {
|
|
119
135
|
continue;
|
|
120
136
|
}
|
|
121
|
-
else if ((0, util_1.declarationFile)(symbol) === context.filename) {
|
|
122
|
-
/* a declaration may always use itself */
|
|
123
|
-
owned.add(variable.name);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
137
|
if (helperObjectPattern.test(variable.name)) {
|
|
127
138
|
candidates.set(variable.name, symbol);
|
|
128
139
|
}
|
package/rules/util.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared helpers of the flowR rules.
|
|
3
3
|
* Tags: `@useInstead <target>` names a replacement, `@lintIgnore [ids]` silences the rules.
|
|
4
4
|
*/
|
|
5
|
-
import type { Rule } from 'eslint';
|
|
5
|
+
import type { Rule, SourceCode } from 'eslint';
|
|
6
6
|
import type { Node } from 'estree';
|
|
7
7
|
import type { TsSymbol, TsTypeChecker, TypeServices } from './ts-types';
|
|
8
8
|
/** The type-aware services, or `undefined` if the file is parsed without a program. */
|
|
@@ -14,6 +14,18 @@ export declare function typeServices(context: Rule.RuleContext): TypeServices |
|
|
|
14
14
|
export declare function suppressed(context: Rule.RuleContext, node: Rule.Node, ids: readonly string[]): boolean;
|
|
15
15
|
/** The symbol an ESTree node resolves to, with import aliases followed. */
|
|
16
16
|
export declare function symbolOf(node: Node | undefined, services: TypeServices, checker: TsTypeChecker): TsSymbol | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The member of `replacement` that takes the place of `member` of `original`: the one of the same name, else the one
|
|
19
|
+
* bound to the same function under another name. `undefined` if the replacement has none, `null` if it cannot be told.
|
|
20
|
+
*/
|
|
21
|
+
export declare function counterpartOf(original: Node, member: string, replacement: string, sourceCode: SourceCode, services: TypeServices): string | undefined | null;
|
|
22
|
+
/**
|
|
23
|
+
* Whether `node` sits in a function the replacement would call, where putting the replacement in would make that
|
|
24
|
+
* function call itself. A replacement can only call what it names: a function is excluded if it is declared or bound
|
|
25
|
+
* under one of those names, or, with types, if one of them resolves to it, wired up under another name or in another
|
|
26
|
+
* file. Both ask the code alone, never file paths, which differ between systems and how a file was opened.
|
|
27
|
+
*/
|
|
28
|
+
export declare function insideReplacement(node: Node, replacement: string, sourceCode: SourceCode, services?: TypeServices): boolean;
|
|
17
29
|
/** The file a symbol is declared in. */
|
|
18
30
|
export declare function declarationFile(symbol: TsSymbol | undefined): string | undefined;
|
|
19
31
|
/** The text of the `@useInstead` tag of a symbol, `undefined` if it carries none. */
|
package/rules/util.js
CHANGED
|
@@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.typeServices = typeServices;
|
|
4
4
|
exports.suppressed = suppressed;
|
|
5
5
|
exports.symbolOf = symbolOf;
|
|
6
|
+
exports.counterpartOf = counterpartOf;
|
|
7
|
+
exports.insideReplacement = insideReplacement;
|
|
6
8
|
exports.declarationFile = declarationFile;
|
|
7
9
|
exports.useInsteadOf = useInsteadOf;
|
|
8
10
|
exports.isApplicable = isApplicable;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
+
const UseInsteadTag = 'useInstead';
|
|
12
|
+
const SuppressTag = '@lintIgnore';
|
|
11
13
|
/* ts.SymbolFlags.Alias, spelled out to keep typescript out of the dependencies */
|
|
12
|
-
const
|
|
14
|
+
const AliasFlag = 1 << 21;
|
|
13
15
|
/** The type-aware services, or `undefined` if the file is parsed without a program. */
|
|
14
16
|
function typeServices(context) {
|
|
15
17
|
const services = context.sourceCode.parserServices;
|
|
@@ -36,7 +38,7 @@ function silences(comment, ids) {
|
|
|
36
38
|
*/
|
|
37
39
|
function suppressed(context, node, ids) {
|
|
38
40
|
const sourceCode = context.sourceCode;
|
|
39
|
-
if (!sourceCode.text.includes(
|
|
41
|
+
if (!sourceCode.text.includes(SuppressTag)) {
|
|
40
42
|
return false;
|
|
41
43
|
}
|
|
42
44
|
/* a header comment marks the whole file: above the imports, or separated by a blank line */
|
|
@@ -59,7 +61,7 @@ function suppressed(context, node, ids) {
|
|
|
59
61
|
}
|
|
60
62
|
/** Resolves import aliases so the documentation of the original declaration is read. */
|
|
61
63
|
function resolveAlias(symbol, checker) {
|
|
62
|
-
return (symbol.flags &
|
|
64
|
+
return (symbol.flags & AliasFlag) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
|
|
63
65
|
}
|
|
64
66
|
/** The symbol an ESTree node resolves to, with import aliases followed. */
|
|
65
67
|
function symbolOf(node, services, checker) {
|
|
@@ -67,6 +69,109 @@ function symbolOf(node, services, checker) {
|
|
|
67
69
|
const symbol = tsNode ? checker.getSymbolAtLocation(tsNode) : undefined;
|
|
68
70
|
return symbol ? resolveAlias(symbol, checker) : undefined;
|
|
69
71
|
}
|
|
72
|
+
/** the name node a function is declared or bound under: a declaration, a member of an object or class, a variable, or an assignment */
|
|
73
|
+
function nameOf(fn, parent) {
|
|
74
|
+
if ((fn.type === 'FunctionDeclaration' || fn.type === 'FunctionExpression') && fn.id) {
|
|
75
|
+
return fn.id;
|
|
76
|
+
}
|
|
77
|
+
switch (parent?.type) {
|
|
78
|
+
case 'Property':
|
|
79
|
+
case 'MethodDefinition':
|
|
80
|
+
case 'PropertyDefinition':
|
|
81
|
+
return parent.key;
|
|
82
|
+
case 'VariableDeclarator':
|
|
83
|
+
return parent.id;
|
|
84
|
+
case 'AssignmentExpression':
|
|
85
|
+
return parent.left.type === 'MemberExpression' ? parent.left.property : parent.left;
|
|
86
|
+
default:
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** a symbol together with what it is bound to, so `frames` of `{ frames: framesOf }` also stands for `framesOf` */
|
|
91
|
+
function boundTo(symbol, checker) {
|
|
92
|
+
if (symbol === undefined) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const declaration = symbol.declarations?.[0];
|
|
96
|
+
const value = declaration?.initializer === undefined ? declaration && checker.getShorthandAssignmentValueSymbol(declaration) : checker.getSymbolAtLocation(declaration.initializer);
|
|
97
|
+
return value === undefined ? [symbol] : [symbol, resolveAlias(value, checker)];
|
|
98
|
+
}
|
|
99
|
+
/* per program, what its modules export under a name, for a replacement the file does not import yet */
|
|
100
|
+
const exportedNames = new WeakMap();
|
|
101
|
+
/** what `name` stands for at `node`: what is in scope there, else what a module of the program exports under it */
|
|
102
|
+
function rootSymbol(name, node, sourceCode, services) {
|
|
103
|
+
const checker = services.program.getTypeChecker();
|
|
104
|
+
let scope = sourceCode.getScope(node);
|
|
105
|
+
while (scope && !scope.set.has(name)) {
|
|
106
|
+
scope = scope.upper;
|
|
107
|
+
}
|
|
108
|
+
if (scope) {
|
|
109
|
+
return symbolOf(scope.set.get(name)?.defs[0]?.name, services, checker);
|
|
110
|
+
}
|
|
111
|
+
let known = exportedNames.get(services.program);
|
|
112
|
+
if (known === undefined) {
|
|
113
|
+
exportedNames.set(services.program, known = new Map());
|
|
114
|
+
}
|
|
115
|
+
if (!known.has(name)) {
|
|
116
|
+
const modules = services.program.getSourceFiles().map(file => checker.getSymbolAtLocation(file)).filter(m => m !== undefined);
|
|
117
|
+
known.set(name, modules.flatMap(m => checker.getExportsOfModule(m)).find(e => e.getName() === name));
|
|
118
|
+
}
|
|
119
|
+
return known.get(name);
|
|
120
|
+
}
|
|
121
|
+
/** the properties of what a symbol holds */
|
|
122
|
+
function propertiesOf(symbol, checker) {
|
|
123
|
+
const declaration = symbol?.declarations?.[0];
|
|
124
|
+
return symbol === undefined || declaration === undefined ? [] : checker.getTypeOfSymbolAtLocation(symbol, declaration).getProperties();
|
|
125
|
+
}
|
|
126
|
+
/** each link of a dotted name, `Dataflow.origin`, as the symbols it resolves to: the name and what it is bound to */
|
|
127
|
+
function resolveChain(chain, node, sourceCode, services) {
|
|
128
|
+
const checker = services.program.getTypeChecker();
|
|
129
|
+
const [root, ...members] = chain.split('.');
|
|
130
|
+
const links = [boundTo(rootSymbol(root, node, sourceCode, services), checker)];
|
|
131
|
+
for (const member of members) {
|
|
132
|
+
links.push(boundTo(propertiesOf(links[links.length - 1][0], checker).find(p => p.getName() === member), checker));
|
|
133
|
+
}
|
|
134
|
+
return links;
|
|
135
|
+
}
|
|
136
|
+
/** Every symbol the dotted names of a replacement resolve to, `REnvironment.frames` to `REnvironment`, its `frames`, and the function that member is bound to. */
|
|
137
|
+
function symbolsNamedBy(replacement, node, sourceCode, services) {
|
|
138
|
+
return new Set((replacement.match(/[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*/g) ?? []).flatMap(chain => resolveChain(chain, node, sourceCode, services).flat()));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The member of `replacement` that takes the place of `member` of `original`: the one of the same name, else the one
|
|
142
|
+
* bound to the same function under another name. `undefined` if the replacement has none, `null` if it cannot be told.
|
|
143
|
+
*/
|
|
144
|
+
function counterpartOf(original, member, replacement, sourceCode, services) {
|
|
145
|
+
const checker = services.program.getTypeChecker();
|
|
146
|
+
const target = resolveChain(replacement, original, sourceCode, services).at(-1)?.[0];
|
|
147
|
+
if (target === undefined) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const offered = propertiesOf(target, checker);
|
|
151
|
+
const was = boundTo(propertiesOf(symbolOf(original, services, checker), checker).find(p => p.getName() === member), checker);
|
|
152
|
+
return (offered.find(p => p.getName() === member) ?? offered.find(p => boundTo(p, checker).some(s => was.includes(s))))?.getName();
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Whether `node` sits in a function the replacement would call, where putting the replacement in would make that
|
|
156
|
+
* function call itself. A replacement can only call what it names: a function is excluded if it is declared or bound
|
|
157
|
+
* under one of those names, or, with types, if one of them resolves to it, wired up under another name or in another
|
|
158
|
+
* file. Both ask the code alone, never file paths, which differ between systems and how a file was opened.
|
|
159
|
+
*/
|
|
160
|
+
function insideReplacement(node, replacement, sourceCode, services) {
|
|
161
|
+
const names = new Set(replacement.match(/[A-Za-z_$][\w$]*/g) ?? []);
|
|
162
|
+
const ancestors = sourceCode.getAncestors(node);
|
|
163
|
+
const functions = ancestors.flatMap((fn, i) => fn.type === 'FunctionDeclaration' || fn.type === 'FunctionExpression' || fn.type === 'ArrowFunctionExpression' ? [nameOf(fn, ancestors[i - 1])] : [])
|
|
164
|
+
.filter((name) => name !== undefined);
|
|
165
|
+
if (functions.some(name => name.type === 'Identifier' && names.has(name.name))) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (services === undefined || functions.length === 0) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const called = symbolsNamedBy(replacement, node, sourceCode, services);
|
|
172
|
+
const checker = services.program.getTypeChecker();
|
|
173
|
+
return functions.some(name => boundTo(symbolOf(name, services, checker), checker).some(symbol => called.has(symbol)));
|
|
174
|
+
}
|
|
70
175
|
/** The file a symbol is declared in. */
|
|
71
176
|
function declarationFile(symbol) {
|
|
72
177
|
return symbol?.declarations?.[0]?.getSourceFile()?.fileName;
|
|
@@ -74,7 +179,7 @@ function declarationFile(symbol) {
|
|
|
74
179
|
/** The text of the `@useInstead` tag of a symbol, `undefined` if it carries none. */
|
|
75
180
|
function useInsteadOf(symbol, checker) {
|
|
76
181
|
for (const tag of symbol.getJsDocTags(checker)) {
|
|
77
|
-
if (tag.name ===
|
|
182
|
+
if (tag.name === UseInsteadTag) {
|
|
78
183
|
return (tag.text ?? []).map(p => p.text).join('').trim();
|
|
79
184
|
}
|
|
80
185
|
}
|