@blumintinc/eslint-plugin-blumint 1.20.95 → 1.20.97
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 +1 -1
- package/lib/index.js +1 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +38 -3
- package/lib/rules/enforce-css-media-queries.d.ts +2 -1
- package/lib/rules/enforce-css-media-queries.js +203 -8
- package/lib/rules/enforce-exported-function-types.js +209 -6
- package/lib/rules/enforce-firestore-doc-ref-generic.js +71 -18
- package/lib/rules/enforce-mui-rounded-icons.d.ts +2 -1
- package/lib/rules/enforce-mui-rounded-icons.js +194 -13
- package/lib/rules/enforce-positive-naming.js +112 -24
- package/lib/rules/generic-starts-with-t.js +49 -0
- package/lib/rules/global-const-style.js +101 -51
- package/lib/rules/no-always-true-false-conditions.js +231 -62
- package/lib/rules/no-unnecessary-verb-suffix.js +52 -5
- package/lib/rules/parallelize-loop-awaits.d.ts +1 -0
- package/lib/rules/parallelize-loop-awaits.js +233 -43
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.js +259 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +63 -4
- package/lib/rules/require-https-error.js +6 -2
- package/lib/rules/use-custom-link.js +45 -1
- package/lib/rules/use-custom-memo.js +30 -0
- package/lib/rules/use-custom-router.js +32 -2
- package/lib/utils/harvestRuleTesterCases.d.ts +48 -0
- package/lib/utils/harvestRuleTesterCases.js +168 -0
- package/package.json +1 -1
- package/release-manifest.json +151 -0
|
@@ -55,13 +55,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
55
55
|
if (!node.members || node.members.length === 0) {
|
|
56
56
|
return true;
|
|
57
57
|
}
|
|
58
|
-
return node.members
|
|
59
|
-
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
60
|
-
member.typeAnnotation) {
|
|
61
|
-
return hasInvalidType(member.typeAnnotation.typeAnnotation);
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
64
|
-
});
|
|
58
|
+
return membersHaveInvalidType(node.members);
|
|
65
59
|
case utils_1.AST_NODE_TYPES.TSTypeReference:
|
|
66
60
|
if (node.typeParameters) {
|
|
67
61
|
return node.typeParameters.params.some(hasInvalidType);
|
|
@@ -74,17 +68,9 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
74
68
|
}
|
|
75
69
|
// Prevent infinite recursion
|
|
76
70
|
typeCache.set(typeName, false);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (interfaceDecl) {
|
|
81
|
-
const result = interfaceDecl.body.body.some((member) => {
|
|
82
|
-
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
83
|
-
member.typeAnnotation) {
|
|
84
|
-
return hasInvalidType(member.typeAnnotation.typeAnnotation);
|
|
85
|
-
}
|
|
86
|
-
return false;
|
|
87
|
-
});
|
|
71
|
+
const members = declaredMembersOf(typeName);
|
|
72
|
+
if (members) {
|
|
73
|
+
const result = membersHaveInvalidType(members);
|
|
88
74
|
typeCache.set(typeName, result);
|
|
89
75
|
return result;
|
|
90
76
|
}
|
|
@@ -120,6 +106,73 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
120
106
|
return false;
|
|
121
107
|
}
|
|
122
108
|
}
|
|
109
|
+
function membersHaveInvalidType(members) {
|
|
110
|
+
return members.some((member) => {
|
|
111
|
+
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
112
|
+
member.typeAnnotation) {
|
|
113
|
+
return hasInvalidType(member.typeAnnotation.typeAnnotation);
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Wrappers an alias may place around its type literal without changing the
|
|
120
|
+
* fields the document declares. `Readonly<{...}>` written inline at the
|
|
121
|
+
* reference is already looked through by the type-argument recursion in
|
|
122
|
+
* `hasInvalidType`, so reading it here keeps the two spellings in agreement.
|
|
123
|
+
* A wrapper that drops fields, such as `Omit`, is excluded: its members are
|
|
124
|
+
* not the document's members, and checking them invents reports.
|
|
125
|
+
*/
|
|
126
|
+
const FIELD_PRESERVING_WRAPPERS = new Set(['Readonly']);
|
|
127
|
+
/**
|
|
128
|
+
* Reads the type literal an alias declares, looking through at most one
|
|
129
|
+
* field-preserving wrapper. Anything else — a union, an intersection, a
|
|
130
|
+
* mapped type, a reference to another named or imported type — has no
|
|
131
|
+
* members this rule can read syntactically, and guessing at them is how
|
|
132
|
+
* false positives arrive, so it stays unresolved.
|
|
133
|
+
*/
|
|
134
|
+
function aliasedTypeLiteral(typeNode) {
|
|
135
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
136
|
+
return typeNode;
|
|
137
|
+
}
|
|
138
|
+
if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference ||
|
|
139
|
+
typeNode.typeName.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
140
|
+
!FIELD_PRESERVING_WRAPPERS.has(typeNode.typeName.name)) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const wrapperArguments = typeNode.typeParameters?.params;
|
|
144
|
+
if (!wrapperArguments || wrapperArguments.length !== 1) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const [wrapped] = wrapperArguments;
|
|
148
|
+
return wrapped.type === utils_1.AST_NODE_TYPES.TSTypeLiteral
|
|
149
|
+
? wrapped
|
|
150
|
+
: undefined;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolves a named generic to the members its declaration lists, reading an
|
|
154
|
+
* interface and a type alias alike.
|
|
155
|
+
*
|
|
156
|
+
* The alias spelling is not an extra convenience: `prefer-type-over-interface`
|
|
157
|
+
* ships in the same recommended config and is fixable, so a single
|
|
158
|
+
* `eslint --fix` pass rewrites every interface into a type alias. A lookup
|
|
159
|
+
* that reads interfaces alone therefore resolves nothing on a codebase that
|
|
160
|
+
* has run the config, and a nested `any` in a document schema goes
|
|
161
|
+
* unreported.
|
|
162
|
+
*/
|
|
163
|
+
function declaredMembersOf(typeName) {
|
|
164
|
+
for (const statement of context.sourceCode.ast.body) {
|
|
165
|
+
if (statement.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
|
|
166
|
+
statement.id.name === typeName) {
|
|
167
|
+
return statement.body.body;
|
|
168
|
+
}
|
|
169
|
+
if (statement.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
|
|
170
|
+
statement.id.name === typeName) {
|
|
171
|
+
return aliasedTypeLiteral(statement.typeAnnotation)?.members;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
123
176
|
function hasTypeAnnotation(node) {
|
|
124
177
|
if (nodeCache.has(node)) {
|
|
125
178
|
return nodeCache.get(node);
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const enforceMuiRoundedIcons: TSESLint.RuleModule<"enforceRoundedVariant", [], TSESLint.RuleListener>;
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceMuiRoundedIcons = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
const renameFixes_1 = require("../utils/renameFixes");
|
|
7
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
8
|
const MUI_ICONS_BARREL = '@mui/icons-material';
|
|
7
9
|
const MUI_ICONS_DEEP_PREFIX = `${MUI_ICONS_BARREL}/`;
|
|
8
10
|
/**
|
|
@@ -66,6 +68,52 @@ const toRoundedIconName = (iconName) => {
|
|
|
66
68
|
}
|
|
67
69
|
return `${baseIconName}Rounded`;
|
|
68
70
|
};
|
|
71
|
+
/** The icon a deep module path names, e.g. `AddReactionOutlined`. */
|
|
72
|
+
const deepImportIconName = (source) => source.split('/').pop();
|
|
73
|
+
/** The binding a deep import introduces for the icon its path names. */
|
|
74
|
+
const deepImportBinding = (node) => {
|
|
75
|
+
const defaultSpecifier = node.specifiers.find((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
|
|
76
|
+
return defaultSpecifier ? defaultSpecifier.local : null;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The Rounded names this declaration would give to *bindings* if its fix ran.
|
|
80
|
+
*
|
|
81
|
+
* Only a binding whose name is the icon name being replaced is ever renamed, so
|
|
82
|
+
* an aliased specifier — whose fix touches the imported name alone — claims no
|
|
83
|
+
* name here. Collected across the file so two variants of one icon
|
|
84
|
+
* (`PersonOutlined` and `PersonSharp`, both retargeting to `PersonRounded`) can
|
|
85
|
+
* be spotted before either fix runs: renaming both bindings would declare the
|
|
86
|
+
* same name twice.
|
|
87
|
+
*/
|
|
88
|
+
const claimedRoundedNames = (node) => {
|
|
89
|
+
if (node.source.type !== utils_1.AST_NODE_TYPES.Literal ||
|
|
90
|
+
typeof node.source.value !== 'string') {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const source = node.source.value;
|
|
94
|
+
if (source.startsWith(MUI_ICONS_DEEP_PREFIX)) {
|
|
95
|
+
const iconName = deepImportIconName(source);
|
|
96
|
+
const roundedVariant = iconName ? toRoundedIconName(iconName) : null;
|
|
97
|
+
const local = deepImportBinding(node);
|
|
98
|
+
return roundedVariant && local?.name === iconName ? [roundedVariant] : [];
|
|
99
|
+
}
|
|
100
|
+
if (source !== MUI_ICONS_BARREL || node.importKind === 'type') {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const claimed = [];
|
|
104
|
+
for (const specifier of node.specifiers) {
|
|
105
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
106
|
+
specifier.importKind === 'type' ||
|
|
107
|
+
specifier.local.name !== specifier.imported.name) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const roundedVariant = toRoundedIconName(specifier.imported.name);
|
|
111
|
+
if (roundedVariant) {
|
|
112
|
+
claimed.push(roundedVariant);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return claimed;
|
|
116
|
+
};
|
|
69
117
|
exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
|
|
70
118
|
name: 'enforce-mui-rounded-icons',
|
|
71
119
|
meta: {
|
|
@@ -82,19 +130,124 @@ exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
|
|
|
82
130
|
},
|
|
83
131
|
defaultOptions: [],
|
|
84
132
|
create(context) {
|
|
133
|
+
const sourceCode = context.getSourceCode();
|
|
134
|
+
/**
|
|
135
|
+
* Rounded names that more than one binding in the file would take. Renaming
|
|
136
|
+
* every claimant would declare the same name twice, so no claimant of a
|
|
137
|
+
* contested name is renamed.
|
|
138
|
+
*/
|
|
139
|
+
const contestedNames = new Set();
|
|
140
|
+
/**
|
|
141
|
+
* Rewrites one reference to a renamed binding, or answers null when that
|
|
142
|
+
* site cannot be rewritten without changing more than the name.
|
|
143
|
+
*/
|
|
144
|
+
const referenceRenameFixes = (fixer, referenceId, newName) => {
|
|
145
|
+
const parent = referenceId.parent;
|
|
146
|
+
// `export { PersonOutlined }` binds the module's public export name to
|
|
147
|
+
// this token, so renaming it would change an API contract whose importers
|
|
148
|
+
// a single-file fix cannot reach.
|
|
149
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.ExportSpecifier) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// A shorthand property is one token serving as both key and value:
|
|
153
|
+
// rewriting it renames the key too, and expanding it to `key: value`
|
|
154
|
+
// reshapes source the rule has no business reshaping.
|
|
155
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.Property && parent.shorthand) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
// `<Icon.Sub />` and `<svg:icon />` name the binding through a compound
|
|
159
|
+
// tag whose closing half is not a scope reference, so the two tags cannot
|
|
160
|
+
// be kept in sync from here.
|
|
161
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.JSXMemberExpression ||
|
|
162
|
+
parent?.type === utils_1.AST_NODE_TYPES.JSXNamespacedName) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const fix = (0, renameFixes_1.renameIdentifierToken)(fixer, sourceCode, referenceId, newName);
|
|
166
|
+
if (!fix) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (parent?.type !== utils_1.AST_NODE_TYPES.JSXOpeningElement ||
|
|
170
|
+
parent.name !== referenceId) {
|
|
171
|
+
return [fix];
|
|
172
|
+
}
|
|
173
|
+
// The scope manager records the opening tag's name as a reference but not
|
|
174
|
+
// the closing tag's, so renaming the opening half alone would leave
|
|
175
|
+
// `<PersonRounded></PersonOutlined>` — code that no longer parses.
|
|
176
|
+
const element = parent.parent;
|
|
177
|
+
const closingName = element?.type === utils_1.AST_NODE_TYPES.JSXElement
|
|
178
|
+
? element.closingElement?.name
|
|
179
|
+
: undefined;
|
|
180
|
+
if (!closingName) {
|
|
181
|
+
return [fix];
|
|
182
|
+
}
|
|
183
|
+
// JSX requires the closing name to spell the opening one, so its first
|
|
184
|
+
// token is the same name this rename replaces.
|
|
185
|
+
const closingFix = (0, renameFixes_1.renameIdentifierToken)(fixer, sourceCode, closingName, newName);
|
|
186
|
+
return closingFix ? [fix, closingFix] : null;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Builds the rewrites that rename the binding an icon import introduces:
|
|
190
|
+
* the declaring token(s) plus every in-file reference to it.
|
|
191
|
+
*
|
|
192
|
+
* Answers null when the rename cannot be applied everywhere — the caller
|
|
193
|
+
* then keeps the retarget that leaves the binding alone, or reports without
|
|
194
|
+
* a fix where no such retarget exists.
|
|
195
|
+
*/
|
|
196
|
+
const bindingRenameFixes = (fixer, declaration, localIdentifier, declarationTokens, newName) => {
|
|
197
|
+
if (contestedNames.has(newName)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// Resolved by declaration identity rather than by name: a file may bind
|
|
201
|
+
// the same name in several scopes, and only this declaration's symbol
|
|
202
|
+
// carries the references the fix has to rewrite.
|
|
203
|
+
const variable = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, declaration).find((candidate) => candidate.defs.some((def) => def.name === localIdentifier));
|
|
204
|
+
if (!variable || (0, renameFixes_1.renameWouldCollide)(variable, newName)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const fixes = [];
|
|
208
|
+
for (const token of declarationTokens) {
|
|
209
|
+
const fix = (0, renameFixes_1.renameIdentifierToken)(fixer, sourceCode, token, newName);
|
|
210
|
+
if (!fix) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
fixes.push(fix);
|
|
214
|
+
}
|
|
215
|
+
for (const reference of variable.references) {
|
|
216
|
+
if (reference.identifier === localIdentifier) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const referenceFixes = referenceRenameFixes(fixer, reference.identifier, newName);
|
|
220
|
+
if (!referenceFixes) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
fixes.push(...referenceFixes);
|
|
224
|
+
}
|
|
225
|
+
return fixes;
|
|
226
|
+
};
|
|
85
227
|
/** `import LogoutIcon from '@mui/icons-material/Logout'` — the icon is named by the module path. */
|
|
86
228
|
const checkDeepImport = (node, iconPath) => {
|
|
87
|
-
const iconName = iconPath
|
|
229
|
+
const iconName = deepImportIconName(iconPath);
|
|
88
230
|
const roundedVariant = iconName ? toRoundedIconName(iconName) : null;
|
|
89
231
|
if (!roundedVariant) {
|
|
90
232
|
return;
|
|
91
233
|
}
|
|
234
|
+
const local = deepImportBinding(node);
|
|
235
|
+
// A hand-chosen alias (`import BellIcon from '.../NotificationsActive'`)
|
|
236
|
+
// names the icon's role, not its variant, so only a binding that repeats
|
|
237
|
+
// the icon name is renamed with the path.
|
|
238
|
+
const renamedBinding = local?.name === iconName ? local : null;
|
|
92
239
|
context.report({
|
|
93
240
|
node,
|
|
94
241
|
messageId: 'enforceRoundedVariant',
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
242
|
+
fix: (fixer) => {
|
|
243
|
+
const pathFix = fixer.replaceText(node.source, `'${MUI_ICONS_DEEP_PREFIX}${roundedVariant}'`);
|
|
244
|
+
const renameFixes = renamedBinding
|
|
245
|
+
? bindingRenameFixes(fixer, node, renamedBinding, [renamedBinding], roundedVariant)
|
|
246
|
+
: null;
|
|
247
|
+
// Path and binding move together: one fixer emits both, so a binding
|
|
248
|
+
// rename can never land without the retarget that motivates it.
|
|
249
|
+
return renameFixes ? [pathFix, ...renameFixes] : pathFix;
|
|
250
|
+
},
|
|
98
251
|
});
|
|
99
252
|
};
|
|
100
253
|
/** `import { Logout } from '@mui/icons-material'` — the icon is named by each specifier. */
|
|
@@ -116,22 +269,50 @@ exports.enforceMuiRoundedIcons = (0, createRule_1.createRule)({
|
|
|
116
269
|
continue;
|
|
117
270
|
}
|
|
118
271
|
// An alias keeps the local binding independent of the imported name, so
|
|
119
|
-
// swapping the imported name is a self-contained edit
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
272
|
+
// swapping the imported name is a self-contained edit that leaves the
|
|
273
|
+
// binding — and every reference to it — alone.
|
|
274
|
+
if (specifier.local.name !== importedName) {
|
|
275
|
+
context.report({
|
|
276
|
+
node: specifier,
|
|
277
|
+
messageId: 'enforceRoundedVariant',
|
|
278
|
+
fix: (fixer) => fixer.replaceText(specifier.imported, roundedVariant),
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Without an alias the imported name IS the local binding, so changing
|
|
283
|
+
// it renames the binding: the fix must own every reference to it or not
|
|
284
|
+
// exist at all. `import { Logout }` spells the imported name and the
|
|
285
|
+
// local one with a single token, which is rewritten once; the redundant
|
|
286
|
+
// `import { Logout as Logout }` spells them separately, so both move.
|
|
287
|
+
const declarationTokens = specifier.imported.range[0] === specifier.local.range[0]
|
|
288
|
+
? [specifier.local]
|
|
289
|
+
: [specifier.imported, specifier.local];
|
|
125
290
|
context.report({
|
|
126
291
|
node: specifier,
|
|
127
292
|
messageId: 'enforceRoundedVariant',
|
|
128
|
-
fix:
|
|
129
|
-
? (fixer) => fixer.replaceText(specifier.imported, roundedVariant)
|
|
130
|
-
: null,
|
|
293
|
+
fix: (fixer) => bindingRenameFixes(fixer, node, specifier.local, declarationTokens, roundedVariant),
|
|
131
294
|
});
|
|
132
295
|
}
|
|
133
296
|
};
|
|
134
297
|
return {
|
|
298
|
+
// Every import is a top-level statement, so the names the file's fixes
|
|
299
|
+
// would introduce are all knowable before the first report.
|
|
300
|
+
Program(node) {
|
|
301
|
+
const claimCounts = new Map();
|
|
302
|
+
for (const statement of node.body) {
|
|
303
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
for (const claimed of claimedRoundedNames(statement)) {
|
|
307
|
+
claimCounts.set(claimed, (claimCounts.get(claimed) ?? 0) + 1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const [name, count] of claimCounts) {
|
|
311
|
+
if (count > 1) {
|
|
312
|
+
contestedNames.add(name);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
135
316
|
ImportDeclaration(node) {
|
|
136
317
|
if (node.source.type !== utils_1.AST_NODE_TYPES.Literal ||
|
|
137
318
|
typeof node.source.value !== 'string') {
|
|
@@ -973,6 +973,78 @@ function isDefinitelyNonBooleanExpression(node) {
|
|
|
973
973
|
return false;
|
|
974
974
|
}
|
|
975
975
|
}
|
|
976
|
+
// Operators whose result is always a boolean, regardless of operand types.
|
|
977
|
+
const BOOLEAN_BINARY_OPERATORS = new Set([
|
|
978
|
+
'==',
|
|
979
|
+
'!=',
|
|
980
|
+
'===',
|
|
981
|
+
'!==',
|
|
982
|
+
'<',
|
|
983
|
+
'<=',
|
|
984
|
+
'>',
|
|
985
|
+
'>=',
|
|
986
|
+
'in',
|
|
987
|
+
'instanceof',
|
|
988
|
+
]);
|
|
989
|
+
/**
|
|
990
|
+
* Detects an expression that is definitively a boolean — a boolean literal, a
|
|
991
|
+
* negation, a comparison, or a branch/`Boolean()` call built from those. Opaque
|
|
992
|
+
* expressions (calls, identifiers, member accesses) yield no verdict here: they
|
|
993
|
+
* are the shapes a validator's body takes once its return annotation is gone,
|
|
994
|
+
* and assuming boolean for them is what produced the false positive in #1692.
|
|
995
|
+
*/
|
|
996
|
+
function isDefinitelyBooleanExpression(node) {
|
|
997
|
+
switch (node.type) {
|
|
998
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
999
|
+
return typeof node.value === 'boolean';
|
|
1000
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression:
|
|
1001
|
+
return node.operator === '!' || node.operator === 'delete';
|
|
1002
|
+
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
1003
|
+
return BOOLEAN_BINARY_OPERATORS.has(node.operator);
|
|
1004
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
1005
|
+
return (isDefinitelyBooleanExpression(node.left) &&
|
|
1006
|
+
isDefinitelyBooleanExpression(node.right));
|
|
1007
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
1008
|
+
return (isDefinitelyBooleanExpression(node.consequent) &&
|
|
1009
|
+
isDefinitelyBooleanExpression(node.alternate));
|
|
1010
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
1011
|
+
// `Boolean(x)` is the one call whose result is boolean by construction.
|
|
1012
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1013
|
+
node.callee.name === 'Boolean');
|
|
1014
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
1015
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
1016
|
+
// `x as boolean` asserts booleanness; `x as const` and other assertions
|
|
1017
|
+
// say nothing, so the asserted expression decides.
|
|
1018
|
+
return (isBooleanOnlyType(node.typeAnnotation) ||
|
|
1019
|
+
isDefinitelyBooleanExpression(node.expression));
|
|
1020
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
1021
|
+
return isDefinitelyBooleanExpression(node.expression);
|
|
1022
|
+
default:
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function classifyExpression(node) {
|
|
1027
|
+
// A definitively non-boolean shape wins over a boolean one: a validator's
|
|
1028
|
+
// `return 'Must not be blank'` proves the function is not a predicate even
|
|
1029
|
+
// though its success path returns `true`.
|
|
1030
|
+
if (isDefinitelyNonBooleanExpression(node))
|
|
1031
|
+
return 'nonBoolean';
|
|
1032
|
+
if (isDefinitelyBooleanExpression(node))
|
|
1033
|
+
return 'boolean';
|
|
1034
|
+
return 'indeterminate';
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Combines the verdicts of a body's returns under the same precedence:
|
|
1038
|
+
* non-boolean beats boolean, and boolean beats no verdict at all. An empty list
|
|
1039
|
+
* (a body with no returns) is `indeterminate`.
|
|
1040
|
+
*/
|
|
1041
|
+
function combineReturnKinds(kinds) {
|
|
1042
|
+
if (kinds.includes('nonBoolean'))
|
|
1043
|
+
return 'nonBoolean';
|
|
1044
|
+
if (kinds.includes('boolean'))
|
|
1045
|
+
return 'boolean';
|
|
1046
|
+
return 'indeterminate';
|
|
1047
|
+
}
|
|
976
1048
|
/**
|
|
977
1049
|
* Yields the immediate AST-node children of `node`, skipping the `parent`
|
|
978
1050
|
* back-reference so traversal only walks downward.
|
|
@@ -995,10 +1067,12 @@ function childNodesOf(node) {
|
|
|
995
1067
|
return children;
|
|
996
1068
|
}
|
|
997
1069
|
/**
|
|
998
|
-
*
|
|
999
|
-
* function's)
|
|
1070
|
+
* Classifies the `return` statements belonging to `fn`'s own body (not a nested
|
|
1071
|
+
* function's). A bare `return;` carries no verdict, so it neither exempts the
|
|
1072
|
+
* function nor keeps a sibling boolean return from deciding.
|
|
1000
1073
|
*/
|
|
1001
|
-
function
|
|
1074
|
+
function classifyBlockReturns(block) {
|
|
1075
|
+
const kinds = [];
|
|
1002
1076
|
const stack = [block];
|
|
1003
1077
|
while (stack.length > 0) {
|
|
1004
1078
|
const current = stack.pop();
|
|
@@ -1009,41 +1083,54 @@ function blockReturnsNonBoolean(block) {
|
|
|
1009
1083
|
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
1010
1084
|
continue;
|
|
1011
1085
|
}
|
|
1012
|
-
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
1013
|
-
current.argument
|
|
1014
|
-
isDefinitelyNonBooleanExpression(current.argument)) {
|
|
1015
|
-
return true;
|
|
1086
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement && current.argument) {
|
|
1087
|
+
kinds.push(classifyExpression(current.argument));
|
|
1016
1088
|
}
|
|
1017
1089
|
for (const child of childNodesOf(current)) {
|
|
1018
1090
|
stack.push(child);
|
|
1019
1091
|
}
|
|
1020
1092
|
}
|
|
1021
|
-
return
|
|
1093
|
+
return combineReturnKinds(kinds);
|
|
1022
1094
|
}
|
|
1023
1095
|
/**
|
|
1024
|
-
*
|
|
1025
|
-
* non-boolean value — e.g. a validator predicate returning `string | true`. An
|
|
1096
|
+
* The booleanness of a function backing an `is`/`has`-prefixed name. An
|
|
1026
1097
|
* explicit return-type annotation is authoritative; otherwise the body's own
|
|
1027
|
-
* `return` statements (or the concise-arrow expression)
|
|
1098
|
+
* `return` statements (or the concise-arrow expression) decide.
|
|
1028
1099
|
*/
|
|
1029
|
-
function
|
|
1100
|
+
function classifyFunctionReturn(fn) {
|
|
1030
1101
|
if (fn.returnType) {
|
|
1031
|
-
return
|
|
1102
|
+
return isBooleanOnlyType(fn.returnType.typeAnnotation)
|
|
1103
|
+
? 'boolean'
|
|
1104
|
+
: 'nonBoolean';
|
|
1032
1105
|
}
|
|
1033
1106
|
if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
1034
|
-
return
|
|
1107
|
+
return classifyExpression(fn.body);
|
|
1035
1108
|
}
|
|
1036
|
-
return
|
|
1109
|
+
return classifyBlockReturns(fn.body);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Whether a function backing an `is`/`has`-prefixed name must be exempt from
|
|
1113
|
+
* boolean negative-naming. Only a function proven to return a boolean is
|
|
1114
|
+
* flagged: a validator predicate returning `string | true` is exempt, and so is
|
|
1115
|
+
* one whose returns are syntactically opaque (`=> validate(value)`). That
|
|
1116
|
+
* matters because `no-explicit-return-type` deletes the very annotation that
|
|
1117
|
+
* spells the validator's non-boolean return, leaving nothing but the name to go
|
|
1118
|
+
* on — and guessing "boolean" from the name alone reports a rename that inverts
|
|
1119
|
+
* the predicate's meaning (#1692). Preferring a false negative here is the
|
|
1120
|
+
* repository's stated trade-off.
|
|
1121
|
+
*/
|
|
1122
|
+
function isExemptFromBooleanNaming(fn) {
|
|
1123
|
+
return classifyFunctionReturn(fn) !== 'boolean';
|
|
1037
1124
|
}
|
|
1038
1125
|
/**
|
|
1039
|
-
* When a declarator/property value is a function, whether that function is
|
|
1040
|
-
*
|
|
1126
|
+
* When a declarator/property value is a function, whether that function is
|
|
1127
|
+
* exempt from boolean negative-naming.
|
|
1041
1128
|
*/
|
|
1042
|
-
function
|
|
1129
|
+
function isExemptFunctionValue(node) {
|
|
1043
1130
|
return (!!node &&
|
|
1044
1131
|
(node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
1045
1132
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
1046
|
-
|
|
1133
|
+
isExemptFromBooleanNaming(node));
|
|
1047
1134
|
}
|
|
1048
1135
|
exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
1049
1136
|
name: 'enforce-positive-naming',
|
|
@@ -1280,7 +1367,7 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1280
1367
|
// with `is`/`has` but is not a boolean, so its domain-correct negation
|
|
1281
1368
|
// ("isNotBlank") must not be flagged. The name heuristic alone cannot
|
|
1282
1369
|
// tell them apart; the initializer's return shape can.
|
|
1283
|
-
if (
|
|
1370
|
+
if (isExemptFunctionValue(node.init))
|
|
1284
1371
|
return;
|
|
1285
1372
|
const variableName = node.id.name;
|
|
1286
1373
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(variableName);
|
|
@@ -1323,8 +1410,9 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1323
1410
|
if (!isBooleanLike(node.id || node))
|
|
1324
1411
|
return;
|
|
1325
1412
|
// Skip validator predicates that return a non-boolean value (e.g.
|
|
1326
|
-
// `string | true`), whose negation is the domain-correct term
|
|
1327
|
-
|
|
1413
|
+
// `string | true`), whose negation is the domain-correct term, and any
|
|
1414
|
+
// function whose returns give no syntactic verdict.
|
|
1415
|
+
if (isExemptFromBooleanNaming(node))
|
|
1328
1416
|
return;
|
|
1329
1417
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(functionName);
|
|
1330
1418
|
if (isNegative) {
|
|
@@ -1348,7 +1436,7 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1348
1436
|
if (!isBooleanLike(node.key))
|
|
1349
1437
|
return;
|
|
1350
1438
|
// Skip validator predicates returning a non-boolean value.
|
|
1351
|
-
if (
|
|
1439
|
+
if (isExemptFunctionValue(node.value))
|
|
1352
1440
|
return;
|
|
1353
1441
|
const methodName = node.key.name;
|
|
1354
1442
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(methodName);
|
|
@@ -1373,7 +1461,7 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1373
1461
|
if (!isBooleanLike(node.key))
|
|
1374
1462
|
return;
|
|
1375
1463
|
// Skip validator predicates returning a non-boolean value.
|
|
1376
|
-
if (
|
|
1464
|
+
if (isExemptFunctionValue(node.value))
|
|
1377
1465
|
return;
|
|
1378
1466
|
const propertyName = node.key.name;
|
|
1379
1467
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(propertyName);
|
|
@@ -2,10 +2,59 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.genericStartsWithT = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
/**
|
|
7
|
+
* A module augmentation targets either an external module
|
|
8
|
+
* (`declare module 'pkg'`, whose id is a string literal) or the global scope
|
|
9
|
+
* (`declare global`). TypeScript requires every declaration of a merged entity
|
|
10
|
+
* to spell its type parameters identically (TS2428, "All declarations of 'X'
|
|
11
|
+
* must have identical type parameters"), so the name inside an augmentation is
|
|
12
|
+
* fixed by the upstream declaration rather than chosen by the author. Renaming
|
|
13
|
+
* it to satisfy the convention breaks the merge.
|
|
14
|
+
*
|
|
15
|
+
* A plain `namespace X` — including the ambient `declare namespace X` and
|
|
16
|
+
* `declare module Foo`, both of which carry an identifier id — augments nothing
|
|
17
|
+
* upstream, so its type parameters stay author-owned and reportable.
|
|
18
|
+
*/
|
|
19
|
+
function isModuleAugmentation(node) {
|
|
20
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
21
|
+
typeof node.id.value === 'string') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
// `declare global` carries a dedicated flag, which also covers the bare
|
|
25
|
+
// `global { ... }` form nested inside an ambient module (that form has no
|
|
26
|
+
// `declare` of its own).
|
|
27
|
+
if (node.global === true) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
// Parser versions that predate the `global` flag spell the same block as an
|
|
31
|
+
// ambient module whose id is the `global` keyword. Requiring `declare` keeps
|
|
32
|
+
// an ordinary `namespace global { ... }` reportable.
|
|
33
|
+
return (node.declare === true &&
|
|
34
|
+
node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
35
|
+
node.id.name === 'global');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The declaration need not be a direct child of the augmentation block; it can
|
|
39
|
+
* sit inside a nested namespace, an interface member signature or any other
|
|
40
|
+
* container within it, so the whole ancestor chain is inspected.
|
|
41
|
+
*/
|
|
42
|
+
function isInsideModuleAugmentation(node) {
|
|
43
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
44
|
+
if (ancestor.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration &&
|
|
45
|
+
isModuleAugmentation(ancestor)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
5
51
|
exports.genericStartsWithT = (0, createRule_1.createRule)({
|
|
6
52
|
create(context) {
|
|
7
53
|
return {
|
|
8
54
|
TSTypeParameterDeclaration(node) {
|
|
55
|
+
if (isInsideModuleAugmentation(node)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
9
58
|
for (const param of node.params) {
|
|
10
59
|
if (typeof param.name.name === 'string' &&
|
|
11
60
|
param.name.name[0] !== 'T') {
|