@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.
@@ -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.some((member) => {
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 program = context.sourceCode.ast;
78
- const interfaceDecl = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
79
- n.id.name === typeName);
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
- export declare const enforceMuiRoundedIcons: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"enforceRoundedVariant", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
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.split('/').pop();
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
- // The local binding is untouched: only the module the icon comes from
96
- // changes.
97
- fix: (fixer) => fixer.replaceText(node.source, `'${MUI_ICONS_DEEP_PREFIX}${roundedVariant}'`),
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. Without one, a
120
- // fix would rename the binding and would have to rewrite every
121
- // reference to it (shorthand properties, export specifiers, JSX) — so
122
- // that form is reported without a fix rather than risking a corrupting
123
- // edit.
124
- const isAliased = specifier.local.name !== importedName;
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: isAliased
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
- * Whether any `return` statement belonging to `fn`'s own body (not a nested
999
- * function's) yields a definitively non-boolean value.
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 blockReturnsNonBoolean(block) {
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 false;
1093
+ return combineReturnKinds(kinds);
1022
1094
  }
1023
1095
  /**
1024
- * Whether a function backing an `is`/`has`-prefixed name actually returns a
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) are inspected.
1098
+ * `return` statements (or the concise-arrow expression) decide.
1028
1099
  */
1029
- function functionReturnsNonBoolean(fn) {
1100
+ function classifyFunctionReturn(fn) {
1030
1101
  if (fn.returnType) {
1031
- return !isBooleanOnlyType(fn.returnType.typeAnnotation);
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 isDefinitelyNonBooleanExpression(fn.body);
1107
+ return classifyExpression(fn.body);
1035
1108
  }
1036
- return blockReturnsNonBoolean(fn.body);
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 a
1040
- * non-boolean predicate that must be exempt from boolean negative-naming.
1126
+ * When a declarator/property value is a function, whether that function is
1127
+ * exempt from boolean negative-naming.
1041
1128
  */
1042
- function isNonBooleanFunctionValue(node) {
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
- functionReturnsNonBoolean(node));
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 (isNonBooleanFunctionValue(node.init))
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
- if (functionReturnsNonBoolean(node))
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 (isNonBooleanFunctionValue(node.value))
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 (isNonBooleanFunctionValue(node.value))
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') {