@blumintinc/eslint-plugin-blumint 1.20.9 → 1.20.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/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.9',
226
+ version: '1.20.10',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -739,6 +739,8 @@ function buildDestructuringGroups(callback, depTextSet, visitorKeys, sourceCode)
739
739
  declarations,
740
740
  inits,
741
741
  baseName: existingGroup?.baseName ?? baseName ?? null,
742
+ hasTypeAnnotation: Boolean(existingGroup?.hasTypeAnnotation) ||
743
+ Boolean(declarator.id.typeAnnotation),
742
744
  });
743
745
  }
744
746
  }
@@ -764,6 +766,18 @@ function validateGroupsForHoisting(groups, callback, scope, visitorKeys, reserve
764
766
  if (hasCrossGroupNameCollision(groups)) {
765
767
  return null;
766
768
  }
769
+ // The hoisted declaration re-emits the pattern from property texts against a
770
+ // `(obj) ?? {}` initializer. A declarator annotation cannot survive that: the
771
+ // `{}` fallback almost never satisfies the annotation, and widening it to one
772
+ // that does requires the type checker. Reporting without a fix is preferable
773
+ // to silently dropping the annotation or manufacturing a type error, and a
774
+ // single annotated declarator withholds the whole fix because a partial hoist
775
+ // would still rewrite the dependency array around the statement left behind.
776
+ for (const group of groups.values()) {
777
+ if (group.hasTypeAnnotation) {
778
+ return null;
779
+ }
780
+ }
767
781
  const declarationsToRemove = new Set();
768
782
  const initsToIgnore = new Set();
769
783
  for (const group of groups.values()) {
@@ -198,42 +198,193 @@ function isAllCapsViolation(text, ignoredWordsSet) {
198
198
  return !allExempt;
199
199
  }
200
200
  /**
201
- * Builds the corrected version of the text for the suggestion / fix.
202
- * ALL-CAPS: Capitalises only the first letter of the first word, lowercases
203
- * the rest (respecting acronyms and ignored words).
204
- * Title Case: Lowercases non-first words that are not acronyms / proper nouns.
201
+ * Splits a whitespace-delimited token into its leading punctuation, the bare
202
+ * word, and its trailing punctuation, so casing can be applied to the word
203
+ * without disturbing quotes/brackets/periods around it. Only *surrounding*
204
+ * punctuation is peeled off, so a possessive stays whole: `USER'S` yields the
205
+ * single core `USER'S` rather than a `USER` word and a stray `'S`.
205
206
  */
206
- function buildSuggestionText(text, ignoredWordsSet) {
207
+ function splitWordAffixes(raw) {
208
+ const lead = /^[^\w]*/.exec(raw)?.[0] ?? '';
209
+ const rest = raw.slice(lead.length);
210
+ const trail = /[^\w]*$/.exec(rest)?.[0] ?? '';
211
+ return { lead, core: rest.slice(0, rest.length - trail.length), trail };
212
+ }
213
+ /**
214
+ * Upper-cases the first alphabetic character, leaving any leading punctuation
215
+ * untouched so `(text` becomes `(Text` rather than staying lower-case.
216
+ */
217
+ function capitalizeFirstLetter(text) {
218
+ const index = text.search(/[a-zA-Z]/);
219
+ if (index === -1)
220
+ return text;
221
+ return (text.slice(0, index) +
222
+ text.charAt(index).toUpperCase() +
223
+ text.slice(index + 1));
224
+ }
225
+ /**
226
+ * True when every letter in the token is upper-case (`USER'S`, `FORM`), which
227
+ * means lower-casing just the leading character would leave a shouting tail.
228
+ */
229
+ function isAllUpperCase(text) {
230
+ const letters = text.replace(/[^a-zA-Z]/g, '');
231
+ return letters.length > 0 && letters === letters.toUpperCase();
232
+ }
233
+ /**
234
+ * Maps the lower-cased form of every ignored word to its canonical spelling so
235
+ * an ALL-CAPS occurrence (`GOOGLE`) can be restored to `Google` rather than
236
+ * flattened to `google`.
237
+ */
238
+ function buildCanonicalIgnoredWords(ignoredWordsSet) {
239
+ const canonical = new Map();
240
+ ignoredWordsSet.forEach((word) => {
241
+ const key = word.toLowerCase();
242
+ if (!canonical.has(key))
243
+ canonical.set(key, word);
244
+ });
245
+ return canonical;
246
+ }
247
+ /**
248
+ * Builds the corrected text for an ALL-CAPS violation: the entire string is
249
+ * lower-cased and each sentence's first letter is capitalised. Only words in
250
+ * the explicit acronym allowlist and ignored/proper nouns keep their casing —
251
+ * the length-based acronym heuristic cannot be used here because *every* token
252
+ * of an ALL-CAPS string looks like an acronym.
253
+ */
254
+ function buildAllCapsSuggestionText(text, ignoredWordsSet) {
255
+ const canonicalIgnoredWords = buildCanonicalIgnoredWords(ignoredWordsSet);
256
+ return splitIntoSentences(text)
257
+ .map((sentence) => sentence
258
+ .split(/\s+/)
259
+ .map((raw, index) => {
260
+ const { lead, core, trail } = splitWordAffixes(raw);
261
+ if (!core)
262
+ return raw;
263
+ const canonical = canonicalIgnoredWords.get(core.toLowerCase());
264
+ if (canonical)
265
+ return `${lead}${canonical}${trail}`;
266
+ if (ACRONYM_ALLOWLIST.has(core))
267
+ return raw;
268
+ const lowered = core.toLowerCase();
269
+ const cased = index === 0 ? capitalizeFirstLetter(lowered) : lowered;
270
+ return `${lead}${cased}${trail}`;
271
+ })
272
+ .join(' '))
273
+ .join(' ');
274
+ }
275
+ /**
276
+ * Builds the corrected text for a Title Case violation: non-first words that
277
+ * are neither acronyms nor proper nouns are lower-cased. Mixed-case tokens
278
+ * only lose their leading capital (`Name` → `name`) so intra-word casing such
279
+ * as `McDonald` survives, while shouting tokens (`CHANGES`) are lower-cased in
280
+ * full.
281
+ */
282
+ function buildTitleCaseSuggestionText(text, ignoredWordsSet) {
207
283
  const sentences = splitIntoSentences(text);
208
284
  return sentences
209
285
  .map((sentence) => {
210
286
  const words = sentence.split(/\s+/);
211
287
  return words
212
288
  .map((raw, index) => {
213
- const word = raw.replace(/^[^\w]+|[^\w]+$/g, '');
214
- if (!word)
289
+ const { lead, core, trail } = splitWordAffixes(raw);
290
+ if (!core)
215
291
  return raw;
216
- if (index === 0) {
217
- // Preserve ignored words in their original capitalisation; for all-caps
218
- // first words, lower-case everything except the initial letter.
219
- if (ignoredWordsSet.has(word))
220
- return raw;
221
- if (isAcronymToken(word))
222
- return raw;
223
- // Sentence-start: ensure first letter is capital
224
- return raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase();
225
- }
226
- if (ignoredWordsSet.has(word))
292
+ // Proper nouns and acronyms keep their original capitalisation.
293
+ if (ignoredWordsSet.has(core))
227
294
  return raw;
228
- if (isAcronymToken(word))
295
+ if (isAcronymToken(core))
229
296
  return raw;
230
- // Non-first word: lower-case
231
- return raw.charAt(0).toLowerCase() + raw.slice(1);
297
+ const lowered = isAllUpperCase(core)
298
+ ? core.toLowerCase()
299
+ : core.charAt(0).toLowerCase() + core.slice(1);
300
+ // Sentence-start: ensure the first letter is a capital.
301
+ const cased = index === 0 ? capitalizeFirstLetter(lowered) : lowered;
302
+ return `${lead}${cased}${trail}`;
232
303
  })
233
304
  .join(' ');
234
305
  })
235
306
  .join(' ');
236
307
  }
308
+ /**
309
+ * Escape sequences for characters that cannot appear literally inside a
310
+ * JavaScript string literal.
311
+ */
312
+ const JS_STRING_ESCAPES = new Map([
313
+ ['\\', '\\\\'],
314
+ ['\n', '\\n'],
315
+ ['\r', '\\r'],
316
+ ['\t', '\\t'],
317
+ ['\b', '\\b'],
318
+ ['\f', '\\f'],
319
+ ['\v', '\\v'],
320
+ ['\u2028', '\\u2028'],
321
+ ['\u2029', '\\u2029'],
322
+ ]);
323
+ /**
324
+ * Escapes text for re-emission inside a JavaScript string literal delimited by
325
+ * `quote`. Without this the rebuilt literal is unparseable as soon as the text
326
+ * contains the delimiter (`'THE USER\'S FILE'`), a backslash, or a line
327
+ * terminator.
328
+ */
329
+ function escapeJsString(text, quote) {
330
+ let escaped = '';
331
+ for (let index = 0; index < text.length; index++) {
332
+ const char = text.charAt(index);
333
+ if (char === quote) {
334
+ escaped += `\\${char}`;
335
+ continue;
336
+ }
337
+ const mapped = JS_STRING_ESCAPES.get(char);
338
+ if (mapped) {
339
+ escaped += mapped;
340
+ continue;
341
+ }
342
+ if (char < ' ' || char === '\u007f') {
343
+ escaped += `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
344
+ continue;
345
+ }
346
+ escaped += char;
347
+ }
348
+ return escaped;
349
+ }
350
+ /**
351
+ * Escapes text for re-emission inside a JSX attribute string (`label="…"`).
352
+ * JSX attribute strings do not process backslash escapes, so the delimiter can
353
+ * only be represented as a character reference. `&` is re-encoded because the
354
+ * value read off the AST has already been entity-decoded.
355
+ */
356
+ function escapeJsxAttributeString(text, quote) {
357
+ const escaped = text.split('&').join('&amp;');
358
+ return quote === "'"
359
+ ? escaped.split("'").join('&#39;')
360
+ : escaped.split('"').join('&quot;');
361
+ }
362
+ /**
363
+ * Characters that change the meaning of JSX children and must therefore be
364
+ * written back as character references. `JSXText.value` is entity-decoded, so
365
+ * emitting it verbatim can break parsing (`&lt;` → `<`) or silently turn text
366
+ * into an expression container (`&#123;x&#125;` → `{x}`).
367
+ */
368
+ const JSX_TEXT_ENTITIES = new Map([
369
+ ['&', '&amp;'],
370
+ ['<', '&lt;'],
371
+ ['>', '&gt;'],
372
+ ['{', '&#123;'],
373
+ ['}', '&#125;'],
374
+ ]);
375
+ function escapeJsxText(text) {
376
+ return text.replace(/[&<>{}]/g, (char) => JSX_TEXT_ENTITIES.get(char) ?? char);
377
+ }
378
+ /**
379
+ * Replaces the first occurrence of `search` without treating `$` sequences in
380
+ * the replacement as `String.prototype.replace` patterns.
381
+ */
382
+ function replaceFirst(text, search, replacement) {
383
+ const index = text.indexOf(search);
384
+ if (index === -1)
385
+ return replacement;
386
+ return text.slice(0, index) + replacement + text.slice(index + search.length);
387
+ }
237
388
  /**
238
389
  * Returns the trimmed text to check and whether it is worth checking.
239
390
  * JSXText nodes often contain only whitespace / newlines from formatting.
@@ -314,6 +465,35 @@ exports.enforceM3SentenceCase = (0, createRule_1.createRule)({
314
465
  return true;
315
466
  return false;
316
467
  }
468
+ /**
469
+ * Produces the replacement source text for the reported node, re-escaping
470
+ * the suggestion for whichever literal form it is being written back into.
471
+ * Returns null when the node is not a rewritable literal.
472
+ */
473
+ function buildFixText(reportNode, checkable, suggestion) {
474
+ if (reportNode.type === utils_1.AST_NODE_TYPES.Literal) {
475
+ if (typeof reportNode.value !== 'string')
476
+ return null;
477
+ const raw = context.getSourceCode().getText(reportNode);
478
+ const quote = raw.charAt(0);
479
+ // Template literals never reach here (they are skipped as dynamic), so
480
+ // only the two string-literal delimiters are rewritable.
481
+ if (quote !== '"' && quote !== "'")
482
+ return null;
483
+ // Surrounding whitespace of the original value is preserved; only the
484
+ // trimmed, checked portion is re-cased.
485
+ const replaced = replaceFirst(reportNode.value, checkable, suggestion);
486
+ const inner = reportNode.parent?.type === utils_1.AST_NODE_TYPES.JSXAttribute
487
+ ? escapeJsxAttributeString(replaced, quote)
488
+ : escapeJsString(replaced, quote);
489
+ return `${quote}${inner}${quote}`;
490
+ }
491
+ if (reportNode.type === utils_1.AST_NODE_TYPES.JSXText) {
492
+ const replaced = replaceFirst(reportNode.value, checkable, suggestion);
493
+ return escapeJsxText(replaced);
494
+ }
495
+ return null;
496
+ }
317
497
  /**
318
498
  * Core checker. Reports on `reportNode` if `text` violates M3 sentence case.
319
499
  */
@@ -325,7 +505,7 @@ exports.enforceM3SentenceCase = (0, createRule_1.createRule)({
325
505
  return;
326
506
  // ALL-CAPS check first (higher severity and different fix)
327
507
  if (isAllCapsViolation(checkable, ignoredWordsSet)) {
328
- const suggestion = buildSuggestionText(checkable, ignoredWordsSet);
508
+ const suggestion = buildAllCapsSuggestionText(checkable, ignoredWordsSet);
329
509
  context.report({
330
510
  node: reportNode,
331
511
  messageId: 'allCaps',
@@ -335,17 +515,10 @@ exports.enforceM3SentenceCase = (0, createRule_1.createRule)({
335
515
  messageId: 'allCaps',
336
516
  data: { text: checkable, suggestion },
337
517
  fix(fixer) {
338
- if (reportNode.type === utils_1.AST_NODE_TYPES.Literal) {
339
- const raw = context.getSourceCode().getText(reportNode);
340
- const quote = raw[0];
341
- return fixer.replaceText(reportNode, `${quote}${suggestion}${quote}`);
342
- }
343
- if (reportNode.type === utils_1.AST_NODE_TYPES.JSXText) {
344
- const original = reportNode.value;
345
- const replaced = original.replace(checkable, suggestion);
346
- return fixer.replaceText(reportNode, replaced);
347
- }
348
- return null;
518
+ const fixText = buildFixText(reportNode, checkable, suggestion);
519
+ return fixText === null
520
+ ? null
521
+ : fixer.replaceText(reportNode, fixText);
349
522
  },
350
523
  },
351
524
  ],
@@ -359,7 +532,7 @@ exports.enforceM3SentenceCase = (0, createRule_1.createRule)({
359
532
  violatingWords.push(...titleCaseViolatingWords(sentence, ignoredWordsSet));
360
533
  });
361
534
  if (violatingWords.length > 0) {
362
- const suggestion = buildSuggestionText(checkable, ignoredWordsSet);
535
+ const suggestion = buildTitleCaseSuggestionText(checkable, ignoredWordsSet);
363
536
  context.report({
364
537
  node: reportNode,
365
538
  messageId: 'titleCase',
@@ -369,17 +542,10 @@ exports.enforceM3SentenceCase = (0, createRule_1.createRule)({
369
542
  messageId: 'titleCase',
370
543
  data: { text: checkable, suggestion },
371
544
  fix(fixer) {
372
- if (reportNode.type === utils_1.AST_NODE_TYPES.Literal) {
373
- const raw = context.getSourceCode().getText(reportNode);
374
- const quote = raw[0];
375
- return fixer.replaceText(reportNode, `${quote}${suggestion}${quote}`);
376
- }
377
- if (reportNode.type === utils_1.AST_NODE_TYPES.JSXText) {
378
- const original = reportNode.value;
379
- const replaced = original.replace(checkable, suggestion);
380
- return fixer.replaceText(reportNode, replaced);
381
- }
382
- return null;
545
+ const fixText = buildFixText(reportNode, checkable, suggestion);
546
+ return fixText === null
547
+ ? null
548
+ : fixer.replaceText(reportNode, fixText);
383
549
  },
384
550
  },
385
551
  ],
@@ -3,6 +3,84 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforcePropsNamingConsistency = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
7
+ const renameFixes_1 = require("../utils/renameFixes");
8
+ /**
9
+ * A body-less signature — an interface or object-type method member, or a
10
+ * `declare`/overload constructor — has no statements, so its parameter name is
11
+ * documentation-only and can never be referenced. A declaration-only rename is
12
+ * therefore complete rather than partial, and stays correct even though the
13
+ * scope analyzer models no variable for such a parameter.
14
+ */
15
+ const isBodylessSignature = (owner) => owner.type === utils_1.AST_NODE_TYPES.TSMethodSignature ||
16
+ owner.type === utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression;
17
+ /**
18
+ * Finds the class (declaration or expression) that owns a method definition.
19
+ */
20
+ const getEnclosingClass = (node) => {
21
+ const classBody = node.parent;
22
+ if (classBody?.type !== utils_1.AST_NODE_TYPES.ClassBody) {
23
+ return null;
24
+ }
25
+ const classNode = classBody.parent;
26
+ if (classNode?.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
27
+ classNode?.type === utils_1.AST_NODE_TYPES.ClassExpression) {
28
+ return classNode;
29
+ }
30
+ return null;
31
+ };
32
+ /**
33
+ * Reports whether renaming a constructor parameter property is unsafe to
34
+ * autofix.
35
+ *
36
+ * A parameter property (`private readonly settings: FooProps`) declares BOTH a
37
+ * constructor-local binding and a `this.settings` class field. The scope
38
+ * analyzer only models the binding, so a scope-driven rename silently rewrites
39
+ * the field declaration while leaving every `this.settings` access — and every
40
+ * plain `settings` use the analyzer attributes elsewhere — pointing at a name
41
+ * that no longer exists (Issue #1358). Since the field half of the rename
42
+ * cannot be resolved through scope analysis, the fix is withheld whenever the
43
+ * name occurs anywhere in the class other than at its declaration.
44
+ */
45
+ const parameterPropertyRenameIsUnsafe = (classNode, name, declarationId) => {
46
+ let unsafe = false;
47
+ const visit = (node) => {
48
+ if (unsafe) {
49
+ return;
50
+ }
51
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
52
+ node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
53
+ node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
54
+ node.property.name === name) {
55
+ unsafe = true;
56
+ return;
57
+ }
58
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
59
+ node.name === name &&
60
+ node !== declarationId) {
61
+ unsafe = true;
62
+ return;
63
+ }
64
+ for (const key of Object.keys(node)) {
65
+ if (key === 'parent') {
66
+ continue;
67
+ }
68
+ const value = node[key];
69
+ if (Array.isArray(value)) {
70
+ for (const child of value) {
71
+ if (ASTHelpers_1.ASTHelpers.isNode(child)) {
72
+ visit(child);
73
+ }
74
+ }
75
+ }
76
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
77
+ visit(value);
78
+ }
79
+ }
80
+ };
81
+ visit(classNode);
82
+ return unsafe;
83
+ };
6
84
  exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
7
85
  name: 'enforce-props-naming-consistency',
8
86
  meta: {
@@ -51,9 +129,39 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
51
129
  function isPropsNameWithPrefix(paramName) {
52
130
  return paramName.endsWith('Props') || paramName === 'props';
53
131
  }
54
- // Fix parameter name to "props"
55
- function fixParameterName(fixer, param) {
56
- return fixer.replaceText(param, 'props');
132
+ // Build the complete rename: the parameter declaration AND every in-file
133
+ // reference to it, rewriting only name tokens.
134
+ //
135
+ // `fixer.replaceText(param, 'props')` used to be the whole fix, which was
136
+ // wrong twice over: an `Identifier` range spans its optional marker and its
137
+ // type annotation, so replacing it deleted the very `: FooProps` annotation
138
+ // that triggered the report, and the body kept referencing the old name
139
+ // (Issue #1358). Returns null whenever the rename cannot be applied
140
+ // everywhere, so the report stands on its own instead of corrupting the
141
+ // source.
142
+ function buildParameterRenameFixes(fixer, owner, param) {
143
+ const sourceCode = context.getSourceCode();
144
+ // `getDeclaredVariables` on a function returns every parameter plus
145
+ // `arguments` and, for a declaration, the function's own name — and those
146
+ // can share a name (`function config(config: XProps)`), so the lookup
147
+ // matches on declaration identity instead of on the name.
148
+ const variable = context
149
+ .getDeclaredVariables(owner)
150
+ .find((candidate) => candidate.defs.some((def) => def.name === param)) ?? null;
151
+ if (!variable) {
152
+ if (!isBodylessSignature(owner)) {
153
+ return null;
154
+ }
155
+ const declarationFix = (0, renameFixes_1.renameIdentifierToken)(fixer, sourceCode, param, 'props');
156
+ return declarationFix ? [declarationFix] : null;
157
+ }
158
+ return (0, renameFixes_1.buildVariableRenameFixes)({
159
+ fixer,
160
+ sourceCode,
161
+ variable,
162
+ declarationId: param,
163
+ newName: 'props',
164
+ });
57
165
  }
58
166
  // Check function parameters
59
167
  function checkFunctionParams(node) {
@@ -77,7 +185,7 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
77
185
  node: param,
78
186
  messageId: 'usePropsName',
79
187
  data: { paramName: param.name },
80
- fix: (fixer) => fixParameterName(fixer, param),
188
+ fix: (fixer) => buildParameterRenameFixes(fixer, node, param),
81
189
  });
82
190
  }
83
191
  }
@@ -109,6 +217,7 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
109
217
  if (propsTypeParams.length > 1) {
110
218
  return; // Skip constructors with multiple Props parameters
111
219
  }
220
+ const enclosingClass = getEnclosingClass(node);
112
221
  for (const param of constructor.params) {
113
222
  if (shouldBeNamedProps(param) &&
114
223
  param.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -117,18 +226,28 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
117
226
  node: param,
118
227
  messageId: 'usePropsName',
119
228
  data: { paramName: param.name },
120
- fix: (fixer) => fixParameterName(fixer, param),
229
+ fix: (fixer) => buildParameterRenameFixes(fixer, constructor, param),
121
230
  });
122
231
  }
123
232
  else if (param.type === utils_1.AST_NODE_TYPES.TSParameterProperty &&
124
233
  param.parameter.type === utils_1.AST_NODE_TYPES.Identifier &&
125
234
  shouldBeNamedProps(param.parameter) &&
126
235
  !isPropsNameWithPrefix(param.parameter.name)) {
236
+ const declarationId = param.parameter;
127
237
  context.report({
128
- node: param.parameter,
238
+ node: declarationId,
129
239
  messageId: 'usePropsName',
130
- data: { paramName: param.parameter.name },
131
- fix: (fixer) => fixParameterName(fixer, param.parameter),
240
+ data: { paramName: declarationId.name },
241
+ fix: (fixer) => {
242
+ // A parameter property also declares a `this.<name>` field the
243
+ // scope analyzer does not model, so renaming it is only safe
244
+ // when the name appears nowhere else in the class.
245
+ if (!enclosingClass ||
246
+ parameterPropertyRenameIsUnsafe(enclosingClass, declarationId.name, declarationId)) {
247
+ return null;
248
+ }
249
+ return buildParameterRenameFixes(fixer, constructor, declarationId);
250
+ },
132
251
  });
133
252
  }
134
253
  }