eslint-plugin-flawless 0.1.7 → 0.1.9

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 CHANGED
@@ -184,6 +184,7 @@ Requires [type information](https://typescript-eslint.io/linting/typed-linting).
184
184
  | [no-unnecessary-use-callback](src/rules/no-unnecessary-use-callback/documentation.md) | Disallow unnecessary usage of 'useCallback' | | |
185
185
  | [no-unnecessary-use-memo](src/rules/no-unnecessary-use-memo/documentation.md) | Disallow unnecessary usage of 'useMemo' | | |
186
186
  | [prefer-destructuring-assignment](src/rules/prefer-destructuring-assignment/documentation.md) | Enforce destructuring assignment for component props | 🔧 | |
187
+ | [prefer-parameter-destructuring](src/rules/prefer-parameter-destructuring/documentation.md) | Enforce destructuring parameters in the function signature | 🔧 | |
187
188
  | [purity](src/rules/purity/documentation.md) | Disallow impure calls such as `math.random` or `os.clock` during render | | |
188
189
  | [toml-sort-keys](src/rules/toml-sort-keys/documentation.md) | Enforce a configured sort order for TOML keys and tables | 🔧 | |
189
190
  | [yaml-block-key-blank-lines](src/rules/yaml-block-key-blank-lines/documentation.md) | Enforce blank lines around top-level YAML block collection keys | 🔧 | |
package/dist/index.d.mts CHANGED
@@ -197,6 +197,9 @@ declare const plugin: {
197
197
  "prefer-destructuring-assignment": TSESLint.RuleModule<"default", [], PluginDocumentation, TSESLint.RuleListener> & {
198
198
  name: string;
199
199
  };
200
+ "prefer-parameter-destructuring": TSESLint.RuleModule<"default", [], PluginDocumentation, TSESLint.RuleListener> & {
201
+ name: string;
202
+ };
200
203
  purity: TSESLint.RuleModule<"impureCall", Options$1, PluginDocumentation, TSESLint.RuleListener> & {
201
204
  name: string;
202
205
  };
@@ -238,6 +241,9 @@ declare const _default: {
238
241
  "prefer-destructuring-assignment": import("${configDir}").RuleModule<"default", [], PluginDocumentation, import("${configDir}").RuleListener> & {
239
242
  name: string;
240
243
  };
244
+ "prefer-parameter-destructuring": import("${configDir}").RuleModule<"default", [], PluginDocumentation, import("${configDir}").RuleListener> & {
245
+ name: string;
246
+ };
241
247
  purity: import("${configDir}").RuleModule<"impureCall", Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
242
248
  name: string;
243
249
  };
@@ -276,6 +282,9 @@ declare const _default: {
276
282
  "prefer-destructuring-assignment": import("${configDir}").RuleModule<"default", [], PluginDocumentation, import("${configDir}").RuleListener> & {
277
283
  name: string;
278
284
  };
285
+ "prefer-parameter-destructuring": import("${configDir}").RuleModule<"default", [], PluginDocumentation, import("${configDir}").RuleListener> & {
286
+ name: string;
287
+ };
279
288
  purity: import("${configDir}").RuleModule<"impureCall", Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
280
289
  name: string;
281
290
  };
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import { findVariable } from "@typescript-eslint/utils/ast-utils";
8
8
  import { getStaticTOMLValue } from "toml-eslint-parser";
9
9
  //#region package.json
10
10
  var name = "eslint-plugin-flawless";
11
- var version = "0.1.7";
11
+ var version = "0.1.9";
12
12
  var repository = {
13
13
  "url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
14
14
  "type": "git"
@@ -35,24 +35,24 @@ function createEslintRule(rule) {
35
35
  }
36
36
  //#endregion
37
37
  //#region src/rules/jsx-shorthand-boolean/rule.ts
38
- const RULE_NAME$8 = "jsx-shorthand-boolean";
39
- const MESSAGE_ID$3 = "setAttributeValue";
40
- const messages$8 = { [MESSAGE_ID$3]: "Set an explicit value for boolean attribute '{{name}}'." };
41
- function create$8(context) {
38
+ const RULE_NAME$9 = "jsx-shorthand-boolean";
39
+ const MESSAGE_ID$4 = "setAttributeValue";
40
+ const messages$9 = { [MESSAGE_ID$4]: "Set an explicit value for boolean attribute '{{name}}'." };
41
+ function create$9(context) {
42
42
  const { sourceCode } = context;
43
43
  return { JSXAttribute(node) {
44
44
  if (node.value !== null) return;
45
45
  context.report({
46
46
  data: { name: sourceCode.getText(node.name) },
47
47
  fix: (fixer) => fixer.insertTextAfter(node.name, "={true}"),
48
- messageId: MESSAGE_ID$3,
48
+ messageId: MESSAGE_ID$4,
49
49
  node
50
50
  });
51
51
  } };
52
52
  }
53
53
  const jsxShorthandBoolean = createEslintRule({
54
- name: RULE_NAME$8,
55
- create: create$8,
54
+ name: RULE_NAME$9,
55
+ create: create$9,
56
56
  defaultOptions: [],
57
57
  meta: {
58
58
  docs: {
@@ -62,19 +62,19 @@ const jsxShorthandBoolean = createEslintRule({
62
62
  },
63
63
  fixable: "code",
64
64
  hasSuggestions: false,
65
- messages: messages$8,
65
+ messages: messages$9,
66
66
  schema: [],
67
67
  type: "suggestion"
68
68
  }
69
69
  });
70
70
  //#endregion
71
71
  //#region src/rules/jsx-shorthand-fragment/rule.ts
72
- const RULE_NAME$7 = "jsx-shorthand-fragment";
72
+ const RULE_NAME$8 = "jsx-shorthand-fragment";
73
73
  const MESSAGE_ID_NAMED = "useNamedFragment";
74
74
  const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
75
75
  const DEFAULT_MODE = "syntax";
76
76
  const DEFAULT_FRAGMENT_NAME = "Fragment";
77
- const messages$7 = {
77
+ const messages$8 = {
78
78
  [MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
79
79
  [MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
80
80
  };
@@ -110,7 +110,7 @@ function jsxNameToString(node) {
110
110
  }
111
111
  return null;
112
112
  }
113
- function create$7(context) {
113
+ function create$8(context) {
114
114
  const options = context.options[0] ?? {};
115
115
  const mode = options.mode ?? DEFAULT_MODE;
116
116
  const fragmentName = options.fragmentName ?? DEFAULT_FRAGMENT_NAME;
@@ -148,8 +148,8 @@ function create$7(context) {
148
148
  } };
149
149
  }
150
150
  const jsxShorthandFragment = createEslintRule({
151
- name: RULE_NAME$7,
152
- create: create$7,
151
+ name: RULE_NAME$8,
152
+ create: create$8,
153
153
  defaultOptions: [{
154
154
  fragmentName: DEFAULT_FRAGMENT_NAME,
155
155
  mode: DEFAULT_MODE
@@ -166,7 +166,7 @@ const jsxShorthandFragment = createEslintRule({
166
166
  },
167
167
  fixable: "code",
168
168
  hasSuggestions: false,
169
- messages: messages$7,
169
+ messages: messages$8,
170
170
  schema: schema$2,
171
171
  type: "suggestion"
172
172
  }
@@ -1331,8 +1331,8 @@ const SCHEMA = {
1331
1331
  };
1332
1332
  //#endregion
1333
1333
  //#region src/rules/naming-convention/rule.ts
1334
- const RULE_NAME$6 = "naming-convention";
1335
- const messages$6 = {
1334
+ const RULE_NAME$7 = "naming-convention";
1335
+ const messages$7 = {
1336
1336
  doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
1337
1337
  doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
1338
1338
  missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
@@ -1362,12 +1362,11 @@ const camelCaseNamingConfig = [
1362
1362
  selector: "typeLike"
1363
1363
  }
1364
1364
  ];
1365
- function create$6(contextWithoutDefaults) {
1365
+ function create$7(contextWithoutDefaults) {
1366
1366
  const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
1367
1367
  const validators = parseOptions(context);
1368
1368
  const compilerOptions = getParserServices(context, true).program?.getCompilerOptions() ?? {};
1369
- function handleMember(validator, node, modifiers) {
1370
- const { key } = node;
1369
+ function handleMember(validator, { key }, modifiers) {
1371
1370
  if (requiresQuoting$1(key, compilerOptions.target)) modifiers.add(Modifier.requiresQuotes);
1372
1371
  validator(key, modifiers);
1373
1372
  }
@@ -1600,8 +1599,7 @@ function create$6(contextWithoutDefaults) {
1600
1599
  validator: validators.enum
1601
1600
  },
1602
1601
  "TSEnumMember": {
1603
- handler: (node, validator) => {
1604
- const { id } = node;
1602
+ handler: ({ id }, validator) => {
1605
1603
  const modifiers = /* @__PURE__ */ new Set();
1606
1604
  if (requiresQuoting$1(id, compilerOptions.target)) modifiers.add(Modifier.requiresQuotes);
1607
1605
  validator(id, modifiers);
@@ -1682,8 +1680,8 @@ function create$6(contextWithoutDefaults) {
1682
1680
  }));
1683
1681
  }
1684
1682
  const namingConvention = createEslintRule({
1685
- name: RULE_NAME$6,
1686
- create: create$6,
1683
+ name: RULE_NAME$7,
1684
+ create: create$7,
1687
1685
  defaultOptions: camelCaseNamingConfig,
1688
1686
  meta: {
1689
1687
  docs: {
@@ -1693,7 +1691,7 @@ const namingConvention = createEslintRule({
1693
1691
  },
1694
1692
  fixable: void 0,
1695
1693
  hasSuggestions: false,
1696
- messages: messages$6,
1694
+ messages: messages$7,
1697
1695
  schema: SCHEMA,
1698
1696
  type: "suggestion"
1699
1697
  }
@@ -1813,8 +1811,7 @@ function resolve(sourceCode, node) {
1813
1811
  * @returns A rule `create` function.
1814
1812
  * @template MessageIds - The rule's message identifiers.
1815
1813
  */
1816
- function createUnnecessaryHookRule(config) {
1817
- const { hook, messageIds } = config;
1814
+ function createUnnecessaryHookRule({ hook, messageIds }) {
1818
1815
  const skipComputation = hook === "useMemo";
1819
1816
  return (context) => {
1820
1817
  const { sourceCode } = context;
@@ -1961,15 +1958,15 @@ function resolveFactory(sourceCode, node) {
1961
1958
  }
1962
1959
  //#endregion
1963
1960
  //#region src/rules/no-unnecessary-use-callback/rule.ts
1964
- const RULE_NAME$5 = "no-unnecessary-use-callback";
1961
+ const RULE_NAME$6 = "no-unnecessary-use-callback";
1965
1962
  const MESSAGE_ID_DEFAULT$1 = "default";
1966
1963
  const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
1967
- const messages$5 = {
1964
+ const messages$6 = {
1968
1965
  [MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
1969
1966
  [MESSAGE_ID_INSIDE_USE_EFFECT$1]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
1970
1967
  };
1971
1968
  const noUnnecessaryUseCallback = createEslintRule({
1972
- name: RULE_NAME$5,
1969
+ name: RULE_NAME$6,
1973
1970
  create: createUnnecessaryHookRule({
1974
1971
  hook: "useCallback",
1975
1972
  messageIds: {
@@ -1985,22 +1982,22 @@ const noUnnecessaryUseCallback = createEslintRule({
1985
1982
  requiresTypeChecking: false
1986
1983
  },
1987
1984
  hasSuggestions: false,
1988
- messages: messages$5,
1985
+ messages: messages$6,
1989
1986
  schema: [],
1990
1987
  type: "suggestion"
1991
1988
  }
1992
1989
  });
1993
1990
  //#endregion
1994
1991
  //#region src/rules/no-unnecessary-use-memo/rule.ts
1995
- const RULE_NAME$4 = "no-unnecessary-use-memo";
1992
+ const RULE_NAME$5 = "no-unnecessary-use-memo";
1996
1993
  const MESSAGE_ID_DEFAULT = "default";
1997
1994
  const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
1998
- const messages$4 = {
1995
+ const messages$5 = {
1999
1996
  [MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
2000
1997
  [MESSAGE_ID_INSIDE_USE_EFFECT]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
2001
1998
  };
2002
1999
  const noUnnecessaryUseMemo = createEslintRule({
2003
- name: RULE_NAME$4,
2000
+ name: RULE_NAME$5,
2004
2001
  create: createUnnecessaryHookRule({
2005
2002
  hook: "useMemo",
2006
2003
  messageIds: {
@@ -2016,16 +2013,16 @@ const noUnnecessaryUseMemo = createEslintRule({
2016
2013
  requiresTypeChecking: false
2017
2014
  },
2018
2015
  hasSuggestions: false,
2019
- messages: messages$4,
2016
+ messages: messages$5,
2020
2017
  schema: [],
2021
2018
  type: "suggestion"
2022
2019
  }
2023
2020
  });
2024
2021
  //#endregion
2025
2022
  //#region src/rules/prefer-destructuring-assignment/rule.ts
2026
- const RULE_NAME$3 = "prefer-destructuring-assignment";
2027
- const MESSAGE_ID$2 = "default";
2028
- const messages$3 = { [MESSAGE_ID$2]: "Use destructuring assignment for component props." };
2023
+ const RULE_NAME$4 = "prefer-destructuring-assignment";
2024
+ const MESSAGE_ID$3 = "default";
2025
+ const messages$4 = { [MESSAGE_ID$3]: "Use destructuring assignment for component props." };
2029
2026
  /**
2030
2027
  * Identifiers that cannot be used as a binding name in strict-mode module code.
2031
2028
  * A property may be accessed with such a name (`props.default`), but a shorthand
@@ -2151,7 +2148,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
2151
2148
  const propertyNames = collectPropertyNames(memberReferences);
2152
2149
  if (!(!hasNonMemberReference && propertyNames !== null && !wouldShadowExistingBinding(context.sourceCode, scope, propertyVariable, memberReferences))) {
2153
2150
  for (const { member } of memberReferences) context.report({
2154
- messageId: MESSAGE_ID$2,
2151
+ messageId: MESSAGE_ID$3,
2155
2152
  node: member
2156
2153
  });
2157
2154
  return;
@@ -2168,7 +2165,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
2168
2165
  if (index === 0) return [fixer.replaceTextRange([propsParameter.range[0], nameEnd], pattern), replaceAccess];
2169
2166
  return replaceAccess;
2170
2167
  },
2171
- messageId: MESSAGE_ID$2,
2168
+ messageId: MESSAGE_ID$3,
2172
2169
  node: member
2173
2170
  });
2174
2171
  }
@@ -2187,7 +2184,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
2187
2184
  * @param context - The rule context.
2188
2185
  * @returns The rule listener.
2189
2186
  */
2190
- function create$3(context) {
2187
+ function create$4(context) {
2191
2188
  const { api, visitor } = core.getFunctionComponentCollector(context);
2192
2189
  return {
2193
2190
  ...visitor,
@@ -2205,12 +2202,325 @@ function create$3(context) {
2205
2202
  };
2206
2203
  }
2207
2204
  const preferDestructuringAssignment = createEslintRule({
2205
+ name: RULE_NAME$4,
2206
+ create: create$4,
2207
+ defaultOptions: [],
2208
+ meta: {
2209
+ docs: {
2210
+ description: "Enforce destructuring assignment for component props",
2211
+ recommended: false,
2212
+ requiresTypeChecking: false
2213
+ },
2214
+ fixable: "code",
2215
+ hasSuggestions: false,
2216
+ messages: messages$4,
2217
+ schema: [],
2218
+ type: "problem"
2219
+ }
2220
+ });
2221
+ //#endregion
2222
+ //#region src/rules/prefer-parameter-destructuring/rule.ts
2223
+ const RULE_NAME$3 = "prefer-parameter-destructuring";
2224
+ const MESSAGE_ID$2 = "default";
2225
+ const messages$3 = { [MESSAGE_ID$2]: "Destructure parameter '{{name}}' in the function signature instead of the body." };
2226
+ /**
2227
+ * Collects the binding names introduced by a pattern (or parameter), including
2228
+ * names nested in object/array patterns, defaults, and rest elements.
2229
+ *
2230
+ * @param node - The pattern node to walk.
2231
+ * @param out - Receives every bound name, in source order.
2232
+ */
2233
+ function collectBoundNames(node, out) {
2234
+ switch (node.type) {
2235
+ case AST_NODE_TYPES.ArrayPattern:
2236
+ for (const element of node.elements) if (element !== null) collectBoundNames(element, out);
2237
+ break;
2238
+ case AST_NODE_TYPES.AssignmentPattern:
2239
+ collectBoundNames(node.left, out);
2240
+ break;
2241
+ case AST_NODE_TYPES.Identifier:
2242
+ out.push(node.name);
2243
+ break;
2244
+ case AST_NODE_TYPES.ObjectPattern:
2245
+ for (const property of node.properties) collectBoundNames(property, out);
2246
+ break;
2247
+ case AST_NODE_TYPES.Property:
2248
+ collectBoundNames(node.value, out);
2249
+ break;
2250
+ case AST_NODE_TYPES.RestElement:
2251
+ collectBoundNames(node.argument, out);
2252
+ break;
2253
+ case AST_NODE_TYPES.TSParameterProperty:
2254
+ collectBoundNames(node.parameter, out);
2255
+ break;
2256
+ default: break;
2257
+ }
2258
+ }
2259
+ /**
2260
+ * Collects every top-level-body object destructuring of the parameter, or
2261
+ * bails when the parameter has any other reference (member access, call
2262
+ * argument, reassignment, a destructure inside a nested block or closure, …) —
2263
+ * those mean the parameter is genuinely used and must stay.
2264
+ *
2265
+ * @param variable - The parameter's resolved scope variable.
2266
+ * @param identifier - The parameter identifier (its default-value write
2267
+ * reference is not a use).
2268
+ * @param body - The function body; only its direct child declarations qualify.
2269
+ * @returns The qualifying destructuring statements in source order, or `null`
2270
+ * when the parameter has a non-destructuring reference.
2271
+ */
2272
+ function collectDestructureStatements(variable, identifier, body) {
2273
+ const statements = [];
2274
+ for (const reference of variable.references) {
2275
+ const referenceIdentifier = reference.identifier;
2276
+ if (referenceIdentifier === identifier) continue;
2277
+ const { parent } = referenceIdentifier;
2278
+ if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.init === referenceIdentifier && parent.id.type === AST_NODE_TYPES.ObjectPattern && parent.parent.parent === body) statements.push({
2279
+ declaration: parent.parent,
2280
+ declarator: parent,
2281
+ pattern: parent.id
2282
+ });
2283
+ else return null;
2284
+ }
2285
+ statements.sort((left, right) => left.declarator.range[0] - right.declarator.range[0]);
2286
+ return statements;
2287
+ }
2288
+ /**
2289
+ * Collects the binding names of every parameter other than the target one, so
2290
+ * the fix can avoid creating a duplicate parameter name (a SyntaxError in a
2291
+ * non-simple parameter list).
2292
+ *
2293
+ * @param node - The function whose parameters are inspected.
2294
+ * @param parameter - The parameter being rewritten (excluded).
2295
+ * @returns The other parameters' bound names.
2296
+ */
2297
+ function collectOtherParameterNames(node, parameter) {
2298
+ const names = [];
2299
+ for (const other of node.params) if (other !== parameter) collectBoundNames(other, names);
2300
+ return new Set(names);
2301
+ }
2302
+ /**
2303
+ * A structural check for AST nodes reached through visitor keys.
2304
+ *
2305
+ * @param value - The child value to test.
2306
+ * @returns `true` when the value is an AST node.
2307
+ */
2308
+ function isNodeLike(value) {
2309
+ return typeof value === "object" && value !== null && typeof value.type === "string";
2310
+ }
2311
+ /**
2312
+ * Checks whether a pattern subtree contains `await` or `yield` (in a computed
2313
+ * key or default value). Parameter initializers may not contain either, so the
2314
+ * fix bails.
2315
+ *
2316
+ * @param root - The pattern to walk.
2317
+ * @param visitorKeys - The parser's visitor keys, used to walk without
2318
+ * following `parent` links.
2319
+ * @returns `true` if `await`/`yield` appears anywhere in the pattern.
2320
+ */
2321
+ function containsAwaitOrYield(root, visitorKeys) {
2322
+ const stack = [root];
2323
+ for (let current = stack.pop(); current !== void 0; current = stack.pop()) {
2324
+ if (current.type === AST_NODE_TYPES.AwaitExpression || current.type === AST_NODE_TYPES.YieldExpression) return true;
2325
+ for (const key of visitorKeys[current.type] ?? []) {
2326
+ const child = current[key];
2327
+ const children = Array.isArray(child) ? child : [child];
2328
+ for (const item of children) if (isNodeLike(item)) stack.push(item);
2329
+ }
2330
+ }
2331
+ return false;
2332
+ }
2333
+ /**
2334
+ * Extracts the identifier a parameter binds, unwrapping a default value
2335
+ * (`obj = {}`); patterns, rest parameters, and TS parameter properties yield
2336
+ * `null`.
2337
+ *
2338
+ * @param parameter - The parameter node.
2339
+ * @returns The parameter's identifier, or `null` when it is not a plain one.
2340
+ */
2341
+ function getParameterIdentifier(parameter) {
2342
+ if (parameter.type === AST_NODE_TYPES.Identifier) return parameter;
2343
+ if (parameter.type === AST_NODE_TYPES.AssignmentPattern && parameter.left.type === AST_NODE_TYPES.Identifier) return parameter.left;
2344
+ return null;
2345
+ }
2346
+ /**
2347
+ * Determines whether any expression inside the patterns (computed keys,
2348
+ * default values, moved type annotations) references a binding that would be
2349
+ * out of scope — or in its temporal dead zone — at the parameter position:
2350
+ * anything declared in the function body, a parameter at or after the target,
2351
+ * or this function's own `arguments`.
2352
+ *
2353
+ * @param query - The rewrite being planned.
2354
+ * @param patterns - The object patterns being moved.
2355
+ * @returns `true` when moving the patterns would break a reference.
2356
+ */
2357
+ function hasUnsafePatternReferences({ body, identifier, node, scope }, patterns) {
2358
+ function inPattern(range) {
2359
+ return patterns.some((pattern) => range[0] >= pattern.range[0] && range[1] <= pattern.range[1]);
2360
+ }
2361
+ const stack = [scope];
2362
+ for (let current = stack.pop(); current !== void 0; current = stack.pop()) {
2363
+ stack.push(...current.childScopes);
2364
+ for (const reference of current.references) {
2365
+ if (!inPattern(reference.identifier.range)) continue;
2366
+ const { resolved } = reference;
2367
+ if (resolved === null) continue;
2368
+ if (resolved.defs.length === 0) {
2369
+ const blockRange = resolved.scope.block.range;
2370
+ if (blockRange[0] >= node.range[0] && blockRange[1] <= node.range[1]) return true;
2371
+ continue;
2372
+ }
2373
+ if (resolved.defs.some((definition) => {
2374
+ const { range } = definition.name;
2375
+ if (inPattern(range)) return false;
2376
+ if (range[1] <= node.range[0] || range[0] >= node.range[1]) return false;
2377
+ if (range[0] >= body.range[0]) return true;
2378
+ return range[0] >= identifier.range[0];
2379
+ })) return true;
2380
+ }
2381
+ }
2382
+ return false;
2383
+ }
2384
+ /**
2385
+ * Checks whether a function body opens with a `"use strict"` directive. A
2386
+ * destructured parameter makes the parameter list non-simple, and a function
2387
+ * with a non-simple parameter list may not contain a `"use strict"` directive,
2388
+ * so such functions are skipped entirely.
2389
+ *
2390
+ * @param body - The function body.
2391
+ * @returns `true` if the body has a `"use strict"` directive.
2392
+ */
2393
+ function hasUseStrictDirective(body) {
2394
+ for (const statement of body.body) {
2395
+ if (statement.type !== AST_NODE_TYPES.ExpressionStatement || typeof statement.directive !== "string") break;
2396
+ if (statement.directive === "use strict") return true;
2397
+ }
2398
+ return false;
2399
+ }
2400
+ /**
2401
+ * Computes the removal range for a declaration, swallowing the whole line
2402
+ * (indentation and trailing newline) when the declaration is alone on it.
2403
+ *
2404
+ * @param sourceCode - Provides the raw text around the declaration.
2405
+ * @param statement - The declaration to remove.
2406
+ * @returns The range to delete.
2407
+ */
2408
+ function statementRemovalRange({ text }, statement) {
2409
+ const [start, end] = statement.range;
2410
+ const lineStart = text.lastIndexOf("\n", start - 1) + 1;
2411
+ const newlineIndex = text.indexOf("\n", end);
2412
+ const lineEnd = newlineIndex === -1 ? text.length : newlineIndex + 1;
2413
+ const leadingIsBlank = text.slice(lineStart, start).trim().length === 0;
2414
+ const trailingIsBlank = text.slice(end, newlineIndex === -1 ? text.length : newlineIndex).trim().length === 0;
2415
+ if (leadingIsBlank && trailingIsBlank) return [lineStart, lineEnd];
2416
+ return [start, end];
2417
+ }
2418
+ /**
2419
+ * Builds the autofix, or returns `null` when the rewrite is not unambiguously
2420
+ * safe: unrelated sibling declarators, unmergeable or annotated patterns,
2421
+ * duplicate or colliding binding names, `await`/`yield` in the pattern, or
2422
+ * expressions that reference bindings unavailable at the parameter position.
2423
+ *
2424
+ * @param query - The rewrite being planned.
2425
+ * @returns The fix plan, or `null` when only a report should be emitted.
2426
+ */
2427
+ function planFix(query) {
2428
+ const { identifier, node, otherParameterNames, sourceCode, statements } = query;
2429
+ const declaratorSet = new Set(statements.map((statement) => statement.declarator));
2430
+ const declarations = [...new Set(statements.map((statement) => statement.declaration))];
2431
+ if (!declarations.every((declaration) => {
2432
+ return declaration.declarations.every((declarator) => declaratorSet.has(declarator));
2433
+ })) return null;
2434
+ const patterns = statements.map((statement) => statement.pattern);
2435
+ if (patterns.some((pattern) => pattern.typeAnnotation !== void 0) && (patterns.length > 1 || identifier.typeAnnotation !== void 0)) return null;
2436
+ if (patterns.length > 1) {
2437
+ if (patterns.some((pattern) => {
2438
+ return pattern.properties.some((property) => property.type === AST_NODE_TYPES.RestElement);
2439
+ })) return null;
2440
+ }
2441
+ const boundNames = [];
2442
+ for (const pattern of patterns) collectBoundNames(pattern, boundNames);
2443
+ if (new Set(boundNames).size !== boundNames.length) return null;
2444
+ if (boundNames.some((name) => otherParameterNames.has(name))) return null;
2445
+ if (patterns.some((pattern) => containsAwaitOrYield(pattern, sourceCode.visitorKeys))) return null;
2446
+ if (hasUnsafePatternReferences(query, patterns)) return null;
2447
+ const [firstPattern] = patterns;
2448
+ let parameterText;
2449
+ if (patterns.length === 1 && firstPattern !== void 0) parameterText = sourceCode.getText(firstPattern);
2450
+ else {
2451
+ const innerTexts = patterns.map((pattern) => sourceCode.getText(pattern).slice(1, -1).trim().replace(/,$/u, "").trim()).filter((text) => text.length > 0);
2452
+ parameterText = innerTexts.length === 0 ? "{}" : `{ ${innerTexts.join(", ")} }`;
2453
+ }
2454
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression && sourceCode.getTokenAfter(identifier)?.value === "=>") parameterText = `(${parameterText})`;
2455
+ const parameterRange = [identifier.range[0], identifier.typeAnnotation?.range[0] ?? identifier.range[1]];
2456
+ const removalRanges = declarations.map((declaration) => {
2457
+ return statementRemovalRange(sourceCode, declaration);
2458
+ });
2459
+ return {
2460
+ parameterRange,
2461
+ parameterText,
2462
+ removalRanges
2463
+ };
2464
+ }
2465
+ /**
2466
+ * Reports body destructuring statements of parameters that have no other use,
2467
+ * preferring the pattern in the function signature. The autofix rewrites the
2468
+ * parameter (merging multiple statements into one pattern) and removes the
2469
+ * statements; it is withheld when the rewrite is not unambiguously safe, see
2470
+ * {@link planFix}.
2471
+ *
2472
+ * @param context - The rule context.
2473
+ * @returns The rule listener.
2474
+ */
2475
+ function create$3(context) {
2476
+ function checkFunction(node) {
2477
+ const { body, params } = node;
2478
+ if (body.type !== AST_NODE_TYPES.BlockStatement || hasUseStrictDirective(body)) return;
2479
+ const scope = context.sourceCode.getScope(node);
2480
+ for (const parameter of params) {
2481
+ const identifier = getParameterIdentifier(parameter);
2482
+ if (identifier === null || identifier.name === "this") continue;
2483
+ const variable = scope.variables.find((candidate) => {
2484
+ return candidate.defs.some((definition) => definition.name === identifier);
2485
+ });
2486
+ if (variable?.defs.length !== 1) continue;
2487
+ const statements = collectDestructureStatements(variable, identifier, body);
2488
+ if (statements === null || statements.length === 0) continue;
2489
+ const otherParameterNames = collectOtherParameterNames(node, parameter);
2490
+ if (otherParameterNames.has(identifier.name)) continue;
2491
+ const plan = planFix({
2492
+ body,
2493
+ identifier,
2494
+ node,
2495
+ otherParameterNames,
2496
+ scope,
2497
+ sourceCode: context.sourceCode,
2498
+ statements
2499
+ });
2500
+ for (const [index, statement] of statements.entries()) context.report({
2501
+ data: { name: identifier.name },
2502
+ fix(fixer) {
2503
+ if (index !== 0 || plan === null) return null;
2504
+ return [fixer.replaceTextRange(plan.parameterRange, plan.parameterText), ...plan.removalRanges.map((range) => fixer.removeRange(range))];
2505
+ },
2506
+ messageId: MESSAGE_ID$2,
2507
+ node: statement.declarator
2508
+ });
2509
+ }
2510
+ }
2511
+ return {
2512
+ ArrowFunctionExpression: checkFunction,
2513
+ FunctionDeclaration: checkFunction,
2514
+ FunctionExpression: checkFunction
2515
+ };
2516
+ }
2517
+ const preferParameterDestructuring = createEslintRule({
2208
2518
  name: RULE_NAME$3,
2209
2519
  create: create$3,
2210
2520
  defaultOptions: [],
2211
2521
  meta: {
2212
2522
  docs: {
2213
- description: "Enforce destructuring assignment for component props",
2523
+ description: "Enforce destructuring parameters in the function signature",
2214
2524
  recommended: false,
2215
2525
  requiresTypeChecking: false
2216
2526
  },
@@ -2218,7 +2528,7 @@ const preferDestructuringAssignment = createEslintRule({
2218
2528
  hasSuggestions: false,
2219
2529
  messages: messages$3,
2220
2530
  schema: [],
2221
- type: "problem"
2531
+ type: "suggestion"
2222
2532
  }
2223
2533
  });
2224
2534
  //#endregion
@@ -2460,8 +2770,7 @@ function makeRank(order) {
2460
2770
  return Number.POSITIVE_INFINITY;
2461
2771
  };
2462
2772
  }
2463
- function makeFallback(config) {
2464
- const { caseSensitive = true, natural = false, type = "asc" } = config;
2773
+ function makeFallback({ caseSensitive = true, natural = false, type = "asc" }) {
2465
2774
  const sensitivity = caseSensitive ? "variant" : "accent";
2466
2775
  return (a, b) => {
2467
2776
  const result = a.localeCompare(b, "en", {
@@ -2474,14 +2783,18 @@ function makeFallback(config) {
2474
2783
  /**
2475
2784
  * Builds a comparator for one table body. Explicit-order entries sort first by
2476
2785
  * their listed position, then unlisted entries fall back to a natural/asc sort
2477
- * (mirroring `yaml/sort-keys`). At the top level, bare key-values are always
2478
- * kept before any `[table]` header, since TOML would otherwise re-scope them.
2786
+ * (mirroring `yaml/sort-keys`). The fallback compares keys as written in
2787
+ * source, so quotes participate in the order and quoted keys (e.g.
2788
+ * `"github:owner/repo"`) group before bare keys. At the top level, bare
2789
+ * key-values are always kept before any `[table]` header, since TOML would
2790
+ * otherwise re-scope them.
2479
2791
  *
2480
2792
  * @param order - The order configuration for this table's path.
2481
2793
  * @param isTopLevel - Whether the body is the top-level table.
2794
+ * @param rawName - Returns an entry's key exactly as written in source.
2482
2795
  * @returns A comparator over two entries.
2483
2796
  */
2484
- function makeComparator(order, isTopLevel) {
2797
+ function makeComparator(order, isTopLevel, rawName) {
2485
2798
  const rank = Array.isArray(order) ? makeRank(order) : void 0;
2486
2799
  const fallback = makeFallback(Array.isArray(order) ? {
2487
2800
  natural: true,
@@ -2502,7 +2815,7 @@ function makeComparator(order, isTopLevel) {
2502
2815
  return aRank - bRank;
2503
2816
  }
2504
2817
  }
2505
- return fallback(nameOf(a), nameOf(b));
2818
+ return fallback(rawName(a), rawName(b));
2506
2819
  };
2507
2820
  }
2508
2821
  function create$1(context) {
@@ -2581,7 +2894,7 @@ function create$1(context) {
2581
2894
  const order = resolveOrder(path);
2582
2895
  if (order === void 0) return;
2583
2896
  const entries = [...body];
2584
- const target = [...entries].sort(makeComparator(order, isTopLevel));
2897
+ const target = [...entries].sort(makeComparator(order, isTopLevel, (entry) => sourceCode.getText(entry.key)));
2585
2898
  let outOfPlace;
2586
2899
  for (const [index, entry] of entries.entries()) if (entry !== target[index]) {
2587
2900
  outOfPlace = entry;
@@ -2705,6 +3018,7 @@ const plugin = {
2705
3018
  "no-unnecessary-use-callback": noUnnecessaryUseCallback,
2706
3019
  "no-unnecessary-use-memo": noUnnecessaryUseMemo,
2707
3020
  "prefer-destructuring-assignment": preferDestructuringAssignment,
3021
+ "prefer-parameter-destructuring": preferParameterDestructuring,
2708
3022
  "purity": purity,
2709
3023
  "toml-sort-keys": tomlSortKeys,
2710
3024
  "yaml-block-key-blank-lines": yamlBlockKeyBlankLines
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-flawless",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Your ESLint plugin description",
5
5
  "keywords": [
6
6
  "eslint",