eslint-plugin-flawless 0.1.4 → 0.1.6

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
@@ -176,15 +176,17 @@ pnpm eslint-docs
176
176
  💭
177
177
  Requires [type information](https://typescript-eslint.io/linting/typed-linting).
178
178
 
179
- | Name                        | Description | 🔧 | 💭 |
180
- | :------------------------------------------------------------------------------------ | :------------------------------------------------------------------- | :-- | :-- |
181
- | [jsx-shorthand-boolean](src/rules/jsx-shorthand-boolean/documentation.md) | Disallow shorthand boolean JSX attributes | 🔧 | |
182
- | [jsx-shorthand-fragment](src/rules/jsx-shorthand-fragment/documentation.md) | Disallow the shorthand fragment syntax in favour of a named fragment | 🔧 | |
183
- | [naming-convention](src/rules/naming-convention/documentation.md) | Enforce naming conventions for everything across a codebase | | 💭 |
184
- | [no-unnecessary-use-callback](src/rules/no-unnecessary-use-callback/documentation.md) | Disallow unnecessary usage of 'useCallback' | | |
185
- | [no-unnecessary-use-memo](src/rules/no-unnecessary-use-memo/documentation.md) | Disallow unnecessary usage of 'useMemo' | | |
186
- | [toml-sort-keys](src/rules/toml-sort-keys/documentation.md) | Enforce a configured sort order for TOML keys and tables | 🔧 | |
187
- | [yaml-block-key-blank-lines](src/rules/yaml-block-key-blank-lines/documentation.md) | Enforce blank lines around top-level YAML block collection keys | 🔧 | |
179
+ | Name                            | Description | 🔧 | 💭 |
180
+ | :-------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | :-- | :-- |
181
+ | [jsx-shorthand-boolean](src/rules/jsx-shorthand-boolean/documentation.md) | Disallow shorthand boolean JSX attributes | 🔧 | |
182
+ | [jsx-shorthand-fragment](src/rules/jsx-shorthand-fragment/documentation.md) | Disallow the shorthand fragment syntax in favour of a named fragment | 🔧 | |
183
+ | [naming-convention](src/rules/naming-convention/documentation.md) | Enforce naming conventions for everything across a codebase | | 💭 |
184
+ | [no-unnecessary-use-callback](src/rules/no-unnecessary-use-callback/documentation.md) | Disallow unnecessary usage of 'useCallback' | | |
185
+ | [no-unnecessary-use-memo](src/rules/no-unnecessary-use-memo/documentation.md) | Disallow unnecessary usage of 'useMemo' | | |
186
+ | [prefer-destructuring-assignment](src/rules/prefer-destructuring-assignment/documentation.md) | Enforce destructuring assignment for component props | 🔧 | |
187
+ | [purity](src/rules/purity/documentation.md) | Disallow impure calls such as `math.random` or `os.clock` during render | | |
188
+ | [toml-sort-keys](src/rules/toml-sort-keys/documentation.md) | Enforce a configured sort order for TOML keys and tables | 🔧 | |
189
+ | [yaml-block-key-blank-lines](src/rules/yaml-block-key-blank-lines/documentation.md) | Enforce blank lines around top-level YAML block collection keys | 🔧 | |
188
190
 
189
191
  <!-- end auto-generated rules list -->
190
192
 
package/dist/index.d.mts CHANGED
@@ -9,7 +9,7 @@ interface PluginDocumentation {
9
9
  }
10
10
  //#endregion
11
11
  //#region src/rules/jsx-shorthand-fragment/rule.d.ts
12
- type Options$2 = [fragmentName?: string];
12
+ type Options$3 = [fragmentName?: string];
13
13
  //#endregion
14
14
  //#region src/rules/naming-convention/utils/enums.d.ts
15
15
  declare const Selector: {
@@ -119,7 +119,7 @@ interface NamingSelector {
119
119
  //#endregion
120
120
  //#region src/rules/naming-convention/rule.d.ts
121
121
  type MessageIds$2 = "doesNotMatchFormat" | "doesNotMatchFormatTrimmed" | "missingAffix" | "missingUnderscore" | "satisfyCustom" | "unexpectedUnderscore";
122
- type Options$1 = Array<NamingSelector>;
122
+ type Options$2 = Array<NamingSelector>;
123
123
  //#endregion
124
124
  //#region src/rules/no-unnecessary-use-callback/rule.d.ts
125
125
  declare const MESSAGE_ID_DEFAULT$1 = "default";
@@ -131,6 +131,18 @@ declare const MESSAGE_ID_DEFAULT = "default";
131
131
  declare const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
132
132
  type MessageIds = typeof MESSAGE_ID_DEFAULT | typeof MESSAGE_ID_INSIDE_USE_EFFECT;
133
133
  //#endregion
134
+ //#region src/rules/purity/rule.d.ts
135
+ interface PurityOptions {
136
+ /**
137
+ * Extra dotted call signatures to treat as impure, added to the defaults
138
+ * (e.g. `"Math.random"` when using a Luau `Math` polyfill).
139
+ */
140
+ readonly additionalFunctions?: ReadonlyArray<string>;
141
+ /** Default signatures to exclude (e.g. `"os.date"`). */
142
+ readonly ignore?: ReadonlyArray<string>;
143
+ }
144
+ type Options$1 = [PurityOptions?];
145
+ //#endregion
134
146
  //#region src/rules/toml-sort-keys/rule.d.ts
135
147
  type Options = Array<SortSpec>;
136
148
  interface SortOrderObject {
@@ -154,10 +166,10 @@ declare const plugin: {
154
166
  "jsx-shorthand-boolean": TSESLint.RuleModule<"setAttributeValue", [], PluginDocumentation, TSESLint.RuleListener> & {
155
167
  name: string;
156
168
  };
157
- "jsx-shorthand-fragment": TSESLint.RuleModule<"useNamedFragment", Options$2, PluginDocumentation, TSESLint.RuleListener> & {
169
+ "jsx-shorthand-fragment": TSESLint.RuleModule<"useNamedFragment", Options$3, PluginDocumentation, TSESLint.RuleListener> & {
158
170
  name: string;
159
171
  };
160
- "naming-convention": TSESLint.RuleModule<MessageIds$2, Options$1, PluginDocumentation, TSESLint.RuleListener> & {
172
+ "naming-convention": TSESLint.RuleModule<MessageIds$2, Options$2, PluginDocumentation, TSESLint.RuleListener> & {
161
173
  name: string;
162
174
  };
163
175
  "no-unnecessary-use-callback": TSESLint.RuleModule<MessageIds$1, [], PluginDocumentation, TSESLint.RuleListener> & {
@@ -166,6 +178,12 @@ declare const plugin: {
166
178
  "no-unnecessary-use-memo": TSESLint.RuleModule<MessageIds, [], PluginDocumentation, TSESLint.RuleListener> & {
167
179
  name: string;
168
180
  };
181
+ "prefer-destructuring-assignment": TSESLint.RuleModule<"default", [], PluginDocumentation, TSESLint.RuleListener> & {
182
+ name: string;
183
+ };
184
+ purity: TSESLint.RuleModule<"impureCall", Options$1, PluginDocumentation, TSESLint.RuleListener> & {
185
+ name: string;
186
+ };
169
187
  "toml-sort-keys": TSESLint.RuleModule<"unsorted", Options, PluginDocumentation, TSESLint.RuleListener> & {
170
188
  name: string;
171
189
  };
@@ -189,10 +207,10 @@ declare const _default: {
189
207
  "jsx-shorthand-boolean": import("${configDir}").RuleModule<"setAttributeValue", [], PluginDocumentation, import("${configDir}").RuleListener> & {
190
208
  name: string;
191
209
  };
192
- "jsx-shorthand-fragment": import("${configDir}").RuleModule<"useNamedFragment", Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
210
+ "jsx-shorthand-fragment": import("${configDir}").RuleModule<"useNamedFragment", Options$3, PluginDocumentation, import("${configDir}").RuleListener> & {
193
211
  name: string;
194
212
  };
195
- "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
213
+ "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
196
214
  name: string;
197
215
  };
198
216
  "no-unnecessary-use-callback": import("${configDir}").RuleModule<MessageIds$1, [], PluginDocumentation, import("${configDir}").RuleListener> & {
@@ -201,6 +219,12 @@ declare const _default: {
201
219
  "no-unnecessary-use-memo": import("${configDir}").RuleModule<MessageIds, [], PluginDocumentation, import("${configDir}").RuleListener> & {
202
220
  name: string;
203
221
  };
222
+ "prefer-destructuring-assignment": import("${configDir}").RuleModule<"default", [], PluginDocumentation, import("${configDir}").RuleListener> & {
223
+ name: string;
224
+ };
225
+ purity: import("${configDir}").RuleModule<"impureCall", Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
226
+ name: string;
227
+ };
204
228
  "toml-sort-keys": import("${configDir}").RuleModule<"unsorted", Options, PluginDocumentation, import("${configDir}").RuleListener> & {
205
229
  name: string;
206
230
  };
@@ -221,10 +245,10 @@ declare const _default: {
221
245
  "jsx-shorthand-boolean": import("${configDir}").RuleModule<"setAttributeValue", [], PluginDocumentation, import("${configDir}").RuleListener> & {
222
246
  name: string;
223
247
  };
224
- "jsx-shorthand-fragment": import("${configDir}").RuleModule<"useNamedFragment", Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
248
+ "jsx-shorthand-fragment": import("${configDir}").RuleModule<"useNamedFragment", Options$3, PluginDocumentation, import("${configDir}").RuleListener> & {
225
249
  name: string;
226
250
  };
227
- "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
251
+ "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
228
252
  name: string;
229
253
  };
230
254
  "no-unnecessary-use-callback": import("${configDir}").RuleModule<MessageIds$1, [], PluginDocumentation, import("${configDir}").RuleListener> & {
@@ -233,6 +257,12 @@ declare const _default: {
233
257
  "no-unnecessary-use-memo": import("${configDir}").RuleModule<MessageIds, [], PluginDocumentation, import("${configDir}").RuleListener> & {
234
258
  name: string;
235
259
  };
260
+ "prefer-destructuring-assignment": import("${configDir}").RuleModule<"default", [], PluginDocumentation, import("${configDir}").RuleListener> & {
261
+ name: string;
262
+ };
263
+ purity: import("${configDir}").RuleModule<"impureCall", Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
264
+ name: string;
265
+ };
236
266
  "toml-sort-keys": import("${configDir}").RuleModule<"unsorted", Options, PluginDocumentation, import("${configDir}").RuleListener> & {
237
267
  name: string;
238
268
  };
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.4";
11
+ var version = "0.1.6";
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$6 = "jsx-shorthand-boolean";
39
- const MESSAGE_ID$2 = "setAttributeValue";
40
- const messages$6 = { [MESSAGE_ID$2]: "Set an explicit value for boolean attribute '{{name}}'." };
41
- function create$6(context) {
38
+ const RULE_NAME$8 = "jsx-shorthand-boolean";
39
+ const MESSAGE_ID$4 = "setAttributeValue";
40
+ const messages$8 = { [MESSAGE_ID$4]: "Set an explicit value for boolean attribute '{{name}}'." };
41
+ function create$8(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$2,
48
+ messageId: MESSAGE_ID$4,
49
49
  node
50
50
  });
51
51
  } };
52
52
  }
53
53
  const jsxShorthandBoolean = createEslintRule({
54
- name: RULE_NAME$6,
55
- create: create$6,
54
+ name: RULE_NAME$8,
55
+ create: create$8,
56
56
  defaultOptions: [],
57
57
  meta: {
58
58
  docs: {
@@ -62,22 +62,22 @@ const jsxShorthandBoolean = createEslintRule({
62
62
  },
63
63
  fixable: "code",
64
64
  hasSuggestions: false,
65
- messages: messages$6,
65
+ messages: messages$8,
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$5 = "jsx-shorthand-fragment";
73
- const MESSAGE_ID$1 = "useNamedFragment";
72
+ const RULE_NAME$7 = "jsx-shorthand-fragment";
73
+ const MESSAGE_ID$3 = "useNamedFragment";
74
74
  const DEFAULT_FRAGMENT_NAME = "Fragment";
75
- const messages$5 = { [MESSAGE_ID$1]: "Use the '{{name}}' component instead of fragment shorthand syntax." };
76
- const schema$1 = [{
75
+ const messages$7 = { [MESSAGE_ID$3]: "Use the '{{name}}' component instead of fragment shorthand syntax." };
76
+ const schema$2 = [{
77
77
  description: "The identifier to use for the named fragment element.",
78
78
  type: "string"
79
79
  }];
80
- function create$5(context) {
80
+ function create$7(context) {
81
81
  const name = context.options[0] ?? DEFAULT_FRAGMENT_NAME;
82
82
  return { JSXFragment(node) {
83
83
  const { closingFragment, openingFragment } = node;
@@ -86,14 +86,14 @@ function create$5(context) {
86
86
  fix: (fixer) => {
87
87
  return [fixer.replaceText(openingFragment, `<${name}>`), fixer.replaceText(closingFragment, `</${name}>`)];
88
88
  },
89
- messageId: MESSAGE_ID$1,
89
+ messageId: MESSAGE_ID$3,
90
90
  node
91
91
  });
92
92
  } };
93
93
  }
94
94
  const jsxShorthandFragment = createEslintRule({
95
- name: RULE_NAME$5,
96
- create: create$5,
95
+ name: RULE_NAME$7,
96
+ create: create$7,
97
97
  defaultOptions: [DEFAULT_FRAGMENT_NAME],
98
98
  meta: {
99
99
  defaultOptions: [DEFAULT_FRAGMENT_NAME],
@@ -104,8 +104,8 @@ const jsxShorthandFragment = createEslintRule({
104
104
  },
105
105
  fixable: "code",
106
106
  hasSuggestions: false,
107
- messages: messages$5,
108
- schema: schema$1,
107
+ messages: messages$7,
108
+ schema: schema$2,
109
109
  type: "suggestion"
110
110
  }
111
111
  });
@@ -1269,8 +1269,8 @@ const SCHEMA = {
1269
1269
  };
1270
1270
  //#endregion
1271
1271
  //#region src/rules/naming-convention/rule.ts
1272
- const RULE_NAME$4 = "naming-convention";
1273
- const messages$4 = {
1272
+ const RULE_NAME$6 = "naming-convention";
1273
+ const messages$6 = {
1274
1274
  doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
1275
1275
  doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
1276
1276
  missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
@@ -1300,7 +1300,7 @@ const camelCaseNamingConfig = [
1300
1300
  selector: "typeLike"
1301
1301
  }
1302
1302
  ];
1303
- function create$4(contextWithoutDefaults) {
1303
+ function create$6(contextWithoutDefaults) {
1304
1304
  const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
1305
1305
  const validators = parseOptions(context);
1306
1306
  const compilerOptions = getParserServices(context, true).program?.getCompilerOptions() ?? {};
@@ -1620,8 +1620,8 @@ function create$4(contextWithoutDefaults) {
1620
1620
  }));
1621
1621
  }
1622
1622
  const namingConvention = createEslintRule({
1623
- name: RULE_NAME$4,
1624
- create: create$4,
1623
+ name: RULE_NAME$6,
1624
+ create: create$6,
1625
1625
  defaultOptions: camelCaseNamingConfig,
1626
1626
  meta: {
1627
1627
  docs: {
@@ -1631,7 +1631,7 @@ const namingConvention = createEslintRule({
1631
1631
  },
1632
1632
  fixable: void 0,
1633
1633
  hasSuggestions: false,
1634
- messages: messages$4,
1634
+ messages: messages$6,
1635
1635
  schema: SCHEMA,
1636
1636
  type: "suggestion"
1637
1637
  }
@@ -1899,15 +1899,15 @@ function resolveFactory(sourceCode, node) {
1899
1899
  }
1900
1900
  //#endregion
1901
1901
  //#region src/rules/no-unnecessary-use-callback/rule.ts
1902
- const RULE_NAME$3 = "no-unnecessary-use-callback";
1902
+ const RULE_NAME$5 = "no-unnecessary-use-callback";
1903
1903
  const MESSAGE_ID_DEFAULT$1 = "default";
1904
1904
  const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
1905
- const messages$3 = {
1905
+ const messages$5 = {
1906
1906
  [MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
1907
1907
  [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."
1908
1908
  };
1909
1909
  const noUnnecessaryUseCallback = createEslintRule({
1910
- name: RULE_NAME$3,
1910
+ name: RULE_NAME$5,
1911
1911
  create: createUnnecessaryHookRule({
1912
1912
  hook: "useCallback",
1913
1913
  messageIds: {
@@ -1923,22 +1923,22 @@ const noUnnecessaryUseCallback = createEslintRule({
1923
1923
  requiresTypeChecking: false
1924
1924
  },
1925
1925
  hasSuggestions: false,
1926
- messages: messages$3,
1926
+ messages: messages$5,
1927
1927
  schema: [],
1928
1928
  type: "suggestion"
1929
1929
  }
1930
1930
  });
1931
1931
  //#endregion
1932
1932
  //#region src/rules/no-unnecessary-use-memo/rule.ts
1933
- const RULE_NAME$2 = "no-unnecessary-use-memo";
1933
+ const RULE_NAME$4 = "no-unnecessary-use-memo";
1934
1934
  const MESSAGE_ID_DEFAULT = "default";
1935
1935
  const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
1936
- const messages$2 = {
1936
+ const messages$4 = {
1937
1937
  [MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
1938
1938
  [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."
1939
1939
  };
1940
1940
  const noUnnecessaryUseMemo = createEslintRule({
1941
- name: RULE_NAME$2,
1941
+ name: RULE_NAME$4,
1942
1942
  create: createUnnecessaryHookRule({
1943
1943
  hook: "useMemo",
1944
1944
  messageIds: {
@@ -1954,12 +1954,394 @@ const noUnnecessaryUseMemo = createEslintRule({
1954
1954
  requiresTypeChecking: false
1955
1955
  },
1956
1956
  hasSuggestions: false,
1957
- messages: messages$2,
1957
+ messages: messages$4,
1958
1958
  schema: [],
1959
1959
  type: "suggestion"
1960
1960
  }
1961
1961
  });
1962
1962
  //#endregion
1963
+ //#region src/rules/prefer-destructuring-assignment/rule.ts
1964
+ const RULE_NAME$3 = "prefer-destructuring-assignment";
1965
+ const MESSAGE_ID$2 = "default";
1966
+ const messages$3 = { [MESSAGE_ID$2]: "Use destructuring assignment for component props." };
1967
+ /**
1968
+ * Identifiers that cannot be used as a binding name in strict-mode module code.
1969
+ * A property may be accessed with such a name (`props.default`), but a shorthand
1970
+ * destructuring pattern built from it (`{ default }`) would be a syntax error,
1971
+ * so the autofix bails when one is encountered.
1972
+ */
1973
+ const RESERVED_WORDS = /* @__PURE__ */ new Set([
1974
+ "arguments",
1975
+ "await",
1976
+ "break",
1977
+ "case",
1978
+ "catch",
1979
+ "class",
1980
+ "const",
1981
+ "continue",
1982
+ "debugger",
1983
+ "default",
1984
+ "delete",
1985
+ "do",
1986
+ "else",
1987
+ "enum",
1988
+ "eval",
1989
+ "export",
1990
+ "extends",
1991
+ "false",
1992
+ "finally",
1993
+ "for",
1994
+ "function",
1995
+ "if",
1996
+ "implements",
1997
+ "import",
1998
+ "in",
1999
+ "instanceof",
2000
+ "interface",
2001
+ "let",
2002
+ "new",
2003
+ "null",
2004
+ "package",
2005
+ "private",
2006
+ "protected",
2007
+ "public",
2008
+ "return",
2009
+ "static",
2010
+ "super",
2011
+ "switch",
2012
+ "this",
2013
+ "throw",
2014
+ "true",
2015
+ "try",
2016
+ "typeof",
2017
+ "var",
2018
+ "void",
2019
+ "while",
2020
+ "with",
2021
+ "yield"
2022
+ ]);
2023
+ /**
2024
+ * Collects the unique accessed property names, in first-seen order, when every
2025
+ * member reference is a simple non-computed `props.<identifier>` access.
2026
+ *
2027
+ * @param memberReferences - The member accesses to inspect.
2028
+ * @returns The property names, or `null` when any access is not destructurable
2029
+ * (computed access such as `props[key]`, `props` used as a computed key, or a
2030
+ * property whose name is a reserved word that cannot be a binding).
2031
+ */
2032
+ function collectPropertyNames(memberReferences) {
2033
+ const names = [];
2034
+ for (const { member, reference } of memberReferences) {
2035
+ if (member.object !== reference.identifier || member.computed || member.property.type !== AST_NODE_TYPES.Identifier || RESERVED_WORDS.has(member.property.name)) return null;
2036
+ if (!names.includes(member.property.name)) names.push(member.property.name);
2037
+ }
2038
+ return names;
2039
+ }
2040
+ /**
2041
+ * Determines whether rewriting `props.foo` to `foo` would resolve to a different
2042
+ * binding than the new destructured parameter at any access site.
2043
+ *
2044
+ * A name is unsafe when it is already bound in the component scope (other than by
2045
+ * the props parameter itself) or in any scope nested between an access and the
2046
+ * component, since the rewritten `foo` reference would then resolve to that
2047
+ * binding instead of the destructured prop.
2048
+ *
2049
+ * @param sourceCode - Provides scope lookup for each reference.
2050
+ * @param componentScope - The component function's scope.
2051
+ * @param propsVariable - The props parameter variable (excluded from the check).
2052
+ * @param memberReferences - The member accesses that would be rewritten.
2053
+ * @returns `true` if any rewritten name would collide with an existing binding.
2054
+ */
2055
+ function wouldShadowExistingBinding(sourceCode, componentScope, propsVariable, memberReferences) {
2056
+ for (const { member, reference } of memberReferences) {
2057
+ const { name } = member.property;
2058
+ let scope = sourceCode.getScope(reference.identifier);
2059
+ while (scope !== null) {
2060
+ if (scope.variables.some((variable) => variable.name === name && variable !== propsVariable)) return true;
2061
+ if (scope === componentScope) break;
2062
+ scope = scope.upper;
2063
+ }
2064
+ }
2065
+ return false;
2066
+ }
2067
+ /**
2068
+ * Reports every `props.<member>` access on a component's props parameter,
2069
+ * attaching an autofix when the parameter can be safely destructured.
2070
+ *
2071
+ * @param context - The rule context.
2072
+ * @param scope - The component function's scope.
2073
+ * @param propsParameter - The props parameter identifier.
2074
+ * @param propertyVariable - The resolved variable for the props parameter.
2075
+ */
2076
+ function reportComponent(context, scope, propsParameter, propertyVariable) {
2077
+ const memberReferences = [];
2078
+ let hasNonMemberReference = false;
2079
+ for (const reference of propertyVariable.references) {
2080
+ const { parent } = reference.identifier;
2081
+ if (parent.type === AST_NODE_TYPES.MemberExpression) memberReferences.push({
2082
+ member: parent,
2083
+ reference
2084
+ });
2085
+ else hasNonMemberReference = true;
2086
+ }
2087
+ if (memberReferences.length === 0) return;
2088
+ memberReferences.sort((left, right) => left.member.range[0] - right.member.range[0]);
2089
+ const propertyNames = collectPropertyNames(memberReferences);
2090
+ if (!(!hasNonMemberReference && propertyNames !== null && !wouldShadowExistingBinding(context.sourceCode, scope, propertyVariable, memberReferences))) {
2091
+ for (const { member } of memberReferences) context.report({
2092
+ messageId: MESSAGE_ID$2,
2093
+ node: member
2094
+ });
2095
+ return;
2096
+ }
2097
+ const needsParentheses = propsParameter.parent.type === AST_NODE_TYPES.ArrowFunctionExpression && context.sourceCode.getTokenAfter(propsParameter)?.value === "=>";
2098
+ const destructured = `{ ${propertyNames.join(", ")} }`;
2099
+ const pattern = needsParentheses ? `(${destructured})` : destructured;
2100
+ const nameEnd = propsParameter.typeAnnotation?.range[0] ?? propsParameter.range[1];
2101
+ for (const [index, { member }] of memberReferences.entries()) {
2102
+ const propertyName = member.property.name;
2103
+ context.report({
2104
+ fix(fixer) {
2105
+ const replaceAccess = fixer.replaceText(member, propertyName);
2106
+ if (index === 0) return [fixer.replaceTextRange([propsParameter.range[0], nameEnd], pattern), replaceAccess];
2107
+ return replaceAccess;
2108
+ },
2109
+ messageId: MESSAGE_ID$2,
2110
+ node: member
2111
+ });
2112
+ }
2113
+ }
2114
+ /**
2115
+ * Faithfully ports `react-x/prefer-destructuring-assignment`, removed from
2116
+ * `eslint-plugin-react-x` in v5.0.0 (deprecated due to low usage). Component
2117
+ * detection is delegated to `@eslint-react/core` so it matches the upstream
2118
+ * semantics.
2119
+ *
2120
+ * Unlike upstream, this port adds an autofix that rewrites the props parameter
2121
+ * into a destructuring pattern (`(props) => props.id` becomes `({ id }) => id`).
2122
+ * The fix is only offered when it is unambiguously safe; see
2123
+ * {@link collectPropertyNames} and {@link wouldShadowExistingBinding}.
2124
+ *
2125
+ * @param context - The rule context.
2126
+ * @returns The rule listener.
2127
+ */
2128
+ function create$3(context) {
2129
+ const { api, visitor } = core.getFunctionComponentCollector(context);
2130
+ return {
2131
+ ...visitor,
2132
+ "Program:exit": function(program) {
2133
+ for (const component of api.getAllComponents(program)) {
2134
+ if (component.name === null || component.isExportDefaultDeclaration) continue;
2135
+ const [propsParameter] = component.node.params;
2136
+ if (propsParameter?.type !== AST_NODE_TYPES.Identifier) continue;
2137
+ const scope = context.sourceCode.getScope(component.node);
2138
+ const propertyVariable = scope.variables.find((variable) => variable.name === propsParameter.name);
2139
+ if (propertyVariable === void 0) continue;
2140
+ reportComponent(context, scope, propsParameter, propertyVariable);
2141
+ }
2142
+ }
2143
+ };
2144
+ }
2145
+ const preferDestructuringAssignment = createEslintRule({
2146
+ name: RULE_NAME$3,
2147
+ create: create$3,
2148
+ defaultOptions: [],
2149
+ meta: {
2150
+ docs: {
2151
+ description: "Enforce destructuring assignment for component props",
2152
+ recommended: false,
2153
+ requiresTypeChecking: false
2154
+ },
2155
+ fixable: "code",
2156
+ hasSuggestions: false,
2157
+ messages: messages$3,
2158
+ schema: [],
2159
+ type: "problem"
2160
+ }
2161
+ });
2162
+ //#endregion
2163
+ //#region src/rules/purity/rule.ts
2164
+ const RULE_NAME$2 = "purity";
2165
+ const MESSAGE_ID$1 = "impureCall";
2166
+ /**
2167
+ * Non-deterministic Luau / Roblox calls, written as dotted paths matching how
2168
+ * they appear in roblox-ts source. `new Random()` is normalized to
2169
+ * `"Random.new"` (see the `NewExpression` visitor).
2170
+ */
2171
+ const DEFAULT_SIGNATURES = [
2172
+ "DateTime.now",
2173
+ "HttpService.GenerateGUID",
2174
+ "Random.new",
2175
+ "Workspace.GetServerTimeNow",
2176
+ "elapsedTime",
2177
+ "math.random",
2178
+ "math.randomseed",
2179
+ "os.clock",
2180
+ "os.date",
2181
+ "os.time",
2182
+ "tick",
2183
+ "time"
2184
+ ];
2185
+ /**
2186
+ * Bare Luau globals for which a matching local binding means the reference is
2187
+ * shadowed rather than the ambient global. Service objects are intentionally
2188
+ * excluded because they are impure even when imported from `@rbxts/services`.
2189
+ */
2190
+ const GUARDED_GLOBALS = /* @__PURE__ */ new Set([
2191
+ "elapsedTime",
2192
+ "math",
2193
+ "os",
2194
+ "tick",
2195
+ "time"
2196
+ ]);
2197
+ const messages$2 = { [MESSAGE_ID$1]: "Do not call '{{name}}' during render. Components and hooks must be pure. Move this call into an event handler, effect, or state initializer." };
2198
+ const schema$1 = [{
2199
+ additionalProperties: false,
2200
+ properties: {
2201
+ additionalFunctions: {
2202
+ description: "Extra dotted call signatures to treat as impure (e.g. \"Math.random\").",
2203
+ items: { type: "string" },
2204
+ type: "array",
2205
+ uniqueItems: true
2206
+ },
2207
+ ignore: {
2208
+ description: "Default signatures to exclude (e.g. \"os.date\").",
2209
+ items: { type: "string" },
2210
+ type: "array",
2211
+ uniqueItems: true
2212
+ }
2213
+ },
2214
+ type: "object"
2215
+ }];
2216
+ /**
2217
+ * Strips wrappers that are irrelevant to the callee identity so that
2218
+ * `a.b?.()` and `a.b!()` still match.
2219
+ *
2220
+ * @param node - The node to unwrap.
2221
+ * @returns The unwrapped node.
2222
+ */
2223
+ function unwrap(node) {
2224
+ let current = node;
2225
+ while (current.type === AST_NODE_TYPES.ChainExpression || current.type === AST_NODE_TYPES.TSNonNullExpression) current = current.expression;
2226
+ return current;
2227
+ }
2228
+ /**
2229
+ * Builds the dotted path for a callee, e.g. `math.random` or `tick`.
2230
+ *
2231
+ * @param callee - The callee expression.
2232
+ * @returns The match, or `null` when the callee cannot be represented as a
2233
+ * simple dotted path (computed access, or an object that is an expression).
2234
+ */
2235
+ function dottedCalleePath(callee) {
2236
+ let current = unwrap(callee);
2237
+ const parts = [];
2238
+ while (current.type === AST_NODE_TYPES.MemberExpression) {
2239
+ if (current.computed || current.property.type !== AST_NODE_TYPES.Identifier) return null;
2240
+ parts.unshift(current.property.name);
2241
+ current = unwrap(current.object);
2242
+ }
2243
+ if (current.type !== AST_NODE_TYPES.Identifier) return null;
2244
+ parts.unshift(current.name);
2245
+ return {
2246
+ path: parts.join("."),
2247
+ root: current
2248
+ };
2249
+ }
2250
+ /**
2251
+ * Determines whether an identifier resolves to a real user binding (import,
2252
+ * parameter, or local declaration) rather than the ambient Luau global.
2253
+ *
2254
+ * @param sourceCode - Provides scope lookup.
2255
+ * @param node - The identifier node.
2256
+ * @returns `true` when the identifier is a user binding rather than the ambient
2257
+ * global.
2258
+ */
2259
+ function isUserBinding(sourceCode, node) {
2260
+ let scope = sourceCode.getScope(node);
2261
+ while (scope !== null) {
2262
+ const variable = scope.variables.find((candidate) => candidate.name === node.name);
2263
+ if (variable !== void 0) return variable.defs.length > 0;
2264
+ scope = scope.upper;
2265
+ }
2266
+ return false;
2267
+ }
2268
+ /**
2269
+ * Finds the immediate enclosing function of a node, stopping at the program.
2270
+ *
2271
+ * @param node - The node to search upward from.
2272
+ * @returns The enclosing function, or `null` when the node is at module level.
2273
+ */
2274
+ function enclosingFunction(node) {
2275
+ let current = node.parent;
2276
+ while (current !== void 0) {
2277
+ if (ASTUtils.isFunction(current)) return current;
2278
+ if (current.type === AST_NODE_TYPES.Program) return null;
2279
+ current = current.parent;
2280
+ }
2281
+ return null;
2282
+ }
2283
+ /**
2284
+ * Determines whether a function executes during render: a component body, a
2285
+ * custom/builtin hook body, or a `useMemo` callback.
2286
+ *
2287
+ * @param reactContext - The context `@eslint-react/core` predicates expect.
2288
+ * @param func - The enclosing function node.
2289
+ * @returns `true` when the function runs during render.
2290
+ */
2291
+ function isRenderContext(reactContext, func) {
2292
+ if (core.isFunctionComponentDefinition(reactContext, func, core.DEFAULT_COMPONENT_DETECTION_HINT)) return true;
2293
+ if (core.isHookDefinition(func)) return true;
2294
+ const { parent } = func;
2295
+ return parent.type === AST_NODE_TYPES.CallExpression && parent.arguments[0] === func && core.isUseMemoCall(reactContext, parent);
2296
+ }
2297
+ function create$2(context) {
2298
+ const { sourceCode } = context;
2299
+ const reactContext = context;
2300
+ const options = context.options[0] ?? {};
2301
+ const signatures = new Set(DEFAULT_SIGNATURES);
2302
+ for (const signature of options.additionalFunctions ?? []) signatures.add(signature);
2303
+ for (const signature of options.ignore ?? []) signatures.delete(signature);
2304
+ function handle(node, match, path) {
2305
+ if (!signatures.has(path)) return;
2306
+ if (path === "Random.new" && node.arguments.length > 0) return;
2307
+ if (GUARDED_GLOBALS.has(match.root.name) && isUserBinding(sourceCode, match.root)) return;
2308
+ const func = enclosingFunction(node);
2309
+ if (func === null || !isRenderContext(reactContext, func)) return;
2310
+ context.report({
2311
+ data: { name: path },
2312
+ messageId: MESSAGE_ID$1,
2313
+ node
2314
+ });
2315
+ }
2316
+ return {
2317
+ CallExpression(node) {
2318
+ const match = dottedCalleePath(node.callee);
2319
+ if (match !== null) handle(node, match, match.path);
2320
+ },
2321
+ NewExpression(node) {
2322
+ const match = dottedCalleePath(node.callee);
2323
+ if (match !== null) handle(node, match, `${match.path}.new`);
2324
+ }
2325
+ };
2326
+ }
2327
+ const purity = createEslintRule({
2328
+ name: RULE_NAME$2,
2329
+ create: create$2,
2330
+ defaultOptions: [{}],
2331
+ meta: {
2332
+ defaultOptions: [{}],
2333
+ docs: {
2334
+ description: "Disallow impure calls such as `math.random` or `os.clock` during render",
2335
+ recommended: false,
2336
+ requiresTypeChecking: false
2337
+ },
2338
+ hasSuggestions: false,
2339
+ messages: messages$2,
2340
+ schema: schema$1,
2341
+ type: "problem"
2342
+ }
2343
+ });
2344
+ //#endregion
1963
2345
  //#region src/rules/toml-sort-keys/rule.ts
1964
2346
  const RULE_NAME$1 = "toml-sort-keys";
1965
2347
  const MESSAGE_ID = "unsorted";
@@ -2260,6 +2642,8 @@ const plugin = {
2260
2642
  "naming-convention": namingConvention,
2261
2643
  "no-unnecessary-use-callback": noUnnecessaryUseCallback,
2262
2644
  "no-unnecessary-use-memo": noUnnecessaryUseMemo,
2645
+ "prefer-destructuring-assignment": preferDestructuringAssignment,
2646
+ "purity": purity,
2263
2647
  "toml-sort-keys": tomlSortKeys,
2264
2648
  "yaml-block-key-blank-lines": yamlBlockKeyBlankLines
2265
2649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-flawless",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Your ESLint plugin description",
5
5
  "keywords": [
6
6
  "eslint",