eslint-plugin-flawless 0.1.5 → 0.1.7

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,16 +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
- | [prefer-destructuring-assignment](src/rules/prefer-destructuring-assignment/documentation.md) | Enforce destructuring assignment for component props | 🔧 | |
187
- | [toml-sort-keys](src/rules/toml-sort-keys/documentation.md) | Enforce a configured sort order for TOML keys and tables | 🔧 | |
188
- | [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) | Enforce a consistent fragment form: the shorthand `<>...</>` or 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 | 🔧 | |
189
190
 
190
191
  <!-- end auto-generated rules list -->
191
192
 
package/dist/index.d.mts CHANGED
@@ -9,7 +9,23 @@ 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
+ declare const MESSAGE_ID_NAMED = "useNamedFragment";
13
+ declare const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
14
+ type MessageIds$3 = typeof MESSAGE_ID_NAMED | typeof MESSAGE_ID_SHORTHAND;
15
+ interface JsxShorthandFragmentOptions {
16
+ /**
17
+ * The identifier used for the named fragment element in `"element"` mode
18
+ * (e.g. `"Fragment"` or `"React.Fragment"`). Defaults to `"Fragment"`.
19
+ */
20
+ readonly fragmentName?: string;
21
+ /**
22
+ * Which form to enforce: `"syntax"` (shorthand `<>...</>`, the default) or
23
+ * `"element"` (a named fragment such as `<Fragment>...</Fragment>`).
24
+ */
25
+ readonly mode?: Mode;
26
+ }
27
+ type Options$3 = [JsxShorthandFragmentOptions?];
28
+ type Mode = "element" | "syntax";
13
29
  //#endregion
14
30
  //#region src/rules/naming-convention/utils/enums.d.ts
15
31
  declare const Selector: {
@@ -119,7 +135,7 @@ interface NamingSelector {
119
135
  //#endregion
120
136
  //#region src/rules/naming-convention/rule.d.ts
121
137
  type MessageIds$2 = "doesNotMatchFormat" | "doesNotMatchFormatTrimmed" | "missingAffix" | "missingUnderscore" | "satisfyCustom" | "unexpectedUnderscore";
122
- type Options$1 = Array<NamingSelector>;
138
+ type Options$2 = Array<NamingSelector>;
123
139
  //#endregion
124
140
  //#region src/rules/no-unnecessary-use-callback/rule.d.ts
125
141
  declare const MESSAGE_ID_DEFAULT$1 = "default";
@@ -131,6 +147,18 @@ declare const MESSAGE_ID_DEFAULT = "default";
131
147
  declare const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
132
148
  type MessageIds = typeof MESSAGE_ID_DEFAULT | typeof MESSAGE_ID_INSIDE_USE_EFFECT;
133
149
  //#endregion
150
+ //#region src/rules/purity/rule.d.ts
151
+ interface PurityOptions {
152
+ /**
153
+ * Extra dotted call signatures to treat as impure, added to the defaults
154
+ * (e.g. `"Math.random"` when using a Luau `Math` polyfill).
155
+ */
156
+ readonly additionalFunctions?: ReadonlyArray<string>;
157
+ /** Default signatures to exclude (e.g. `"os.date"`). */
158
+ readonly ignore?: ReadonlyArray<string>;
159
+ }
160
+ type Options$1 = [PurityOptions?];
161
+ //#endregion
134
162
  //#region src/rules/toml-sort-keys/rule.d.ts
135
163
  type Options = Array<SortSpec>;
136
164
  interface SortOrderObject {
@@ -154,10 +182,10 @@ declare const plugin: {
154
182
  "jsx-shorthand-boolean": TSESLint.RuleModule<"setAttributeValue", [], PluginDocumentation, TSESLint.RuleListener> & {
155
183
  name: string;
156
184
  };
157
- "jsx-shorthand-fragment": TSESLint.RuleModule<"useNamedFragment", Options$2, PluginDocumentation, TSESLint.RuleListener> & {
185
+ "jsx-shorthand-fragment": TSESLint.RuleModule<MessageIds$3, Options$3, PluginDocumentation, TSESLint.RuleListener> & {
158
186
  name: string;
159
187
  };
160
- "naming-convention": TSESLint.RuleModule<MessageIds$2, Options$1, PluginDocumentation, TSESLint.RuleListener> & {
188
+ "naming-convention": TSESLint.RuleModule<MessageIds$2, Options$2, PluginDocumentation, TSESLint.RuleListener> & {
161
189
  name: string;
162
190
  };
163
191
  "no-unnecessary-use-callback": TSESLint.RuleModule<MessageIds$1, [], PluginDocumentation, TSESLint.RuleListener> & {
@@ -169,6 +197,9 @@ declare const plugin: {
169
197
  "prefer-destructuring-assignment": TSESLint.RuleModule<"default", [], PluginDocumentation, TSESLint.RuleListener> & {
170
198
  name: string;
171
199
  };
200
+ purity: TSESLint.RuleModule<"impureCall", Options$1, PluginDocumentation, TSESLint.RuleListener> & {
201
+ name: string;
202
+ };
172
203
  "toml-sort-keys": TSESLint.RuleModule<"unsorted", Options, PluginDocumentation, TSESLint.RuleListener> & {
173
204
  name: string;
174
205
  };
@@ -192,10 +223,10 @@ declare const _default: {
192
223
  "jsx-shorthand-boolean": import("${configDir}").RuleModule<"setAttributeValue", [], PluginDocumentation, import("${configDir}").RuleListener> & {
193
224
  name: string;
194
225
  };
195
- "jsx-shorthand-fragment": import("${configDir}").RuleModule<"useNamedFragment", Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
226
+ "jsx-shorthand-fragment": import("${configDir}").RuleModule<MessageIds$3, Options$3, PluginDocumentation, import("${configDir}").RuleListener> & {
196
227
  name: string;
197
228
  };
198
- "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
229
+ "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
199
230
  name: string;
200
231
  };
201
232
  "no-unnecessary-use-callback": import("${configDir}").RuleModule<MessageIds$1, [], PluginDocumentation, import("${configDir}").RuleListener> & {
@@ -207,6 +238,9 @@ declare const _default: {
207
238
  "prefer-destructuring-assignment": import("${configDir}").RuleModule<"default", [], PluginDocumentation, import("${configDir}").RuleListener> & {
208
239
  name: string;
209
240
  };
241
+ purity: import("${configDir}").RuleModule<"impureCall", Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
242
+ name: string;
243
+ };
210
244
  "toml-sort-keys": import("${configDir}").RuleModule<"unsorted", Options, PluginDocumentation, import("${configDir}").RuleListener> & {
211
245
  name: string;
212
246
  };
@@ -227,10 +261,10 @@ declare const _default: {
227
261
  "jsx-shorthand-boolean": import("${configDir}").RuleModule<"setAttributeValue", [], PluginDocumentation, import("${configDir}").RuleListener> & {
228
262
  name: string;
229
263
  };
230
- "jsx-shorthand-fragment": import("${configDir}").RuleModule<"useNamedFragment", Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
264
+ "jsx-shorthand-fragment": import("${configDir}").RuleModule<MessageIds$3, Options$3, PluginDocumentation, import("${configDir}").RuleListener> & {
231
265
  name: string;
232
266
  };
233
- "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
267
+ "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
234
268
  name: string;
235
269
  };
236
270
  "no-unnecessary-use-callback": import("${configDir}").RuleModule<MessageIds$1, [], PluginDocumentation, import("${configDir}").RuleListener> & {
@@ -242,6 +276,9 @@ declare const _default: {
242
276
  "prefer-destructuring-assignment": import("${configDir}").RuleModule<"default", [], PluginDocumentation, import("${configDir}").RuleListener> & {
243
277
  name: string;
244
278
  };
279
+ purity: import("${configDir}").RuleModule<"impureCall", Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
280
+ name: string;
281
+ };
245
282
  "toml-sort-keys": import("${configDir}").RuleModule<"unsorted", Options, PluginDocumentation, import("${configDir}").RuleListener> & {
246
283
  name: string;
247
284
  };
package/dist/index.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
2
+ import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
2
3
  import { DefinitionType, ImplicitLibVariable, PatternVisitor, ScopeType, Visitor } from "@typescript-eslint/scope-manager";
3
4
  import { requiresQuoting } from "@typescript-eslint/type-utils";
4
- import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
5
5
  import assert from "node:assert";
6
6
  import * as core from "@eslint-react/core";
7
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.5";
11
+ var version = "0.1.7";
12
12
  var repository = {
13
13
  "url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
14
14
  "type": "git"
@@ -35,10 +35,10 @@ function createEslintRule(rule) {
35
35
  }
36
36
  //#endregion
37
37
  //#region src/rules/jsx-shorthand-boolean/rule.ts
38
- const RULE_NAME$7 = "jsx-shorthand-boolean";
38
+ const RULE_NAME$8 = "jsx-shorthand-boolean";
39
39
  const MESSAGE_ID$3 = "setAttributeValue";
40
- const messages$7 = { [MESSAGE_ID$3]: "Set an explicit value for boolean attribute '{{name}}'." };
41
- function create$7(context) {
40
+ const messages$8 = { [MESSAGE_ID$3]: "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;
@@ -51,8 +51,8 @@ function create$7(context) {
51
51
  } };
52
52
  }
53
53
  const jsxShorthandBoolean = createEslintRule({
54
- name: RULE_NAME$7,
55
- create: create$7,
54
+ name: RULE_NAME$8,
55
+ create: create$8,
56
56
  defaultOptions: [],
57
57
  meta: {
58
58
  docs: {
@@ -62,50 +62,112 @@ const jsxShorthandBoolean = createEslintRule({
62
62
  },
63
63
  fixable: "code",
64
64
  hasSuggestions: false,
65
- messages: messages$7,
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$6 = "jsx-shorthand-fragment";
73
- const MESSAGE_ID$2 = "useNamedFragment";
72
+ const RULE_NAME$7 = "jsx-shorthand-fragment";
73
+ const MESSAGE_ID_NAMED = "useNamedFragment";
74
+ const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
75
+ const DEFAULT_MODE = "syntax";
74
76
  const DEFAULT_FRAGMENT_NAME = "Fragment";
75
- const messages$6 = { [MESSAGE_ID$2]: "Use the '{{name}}' component instead of fragment shorthand syntax." };
76
- const schema$1 = [{
77
- description: "The identifier to use for the named fragment element.",
78
- type: "string"
77
+ const messages$7 = {
78
+ [MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
79
+ [MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
80
+ };
81
+ const schema$2 = [{
82
+ additionalProperties: false,
83
+ properties: {
84
+ fragmentName: {
85
+ description: "The identifier to use for the named fragment element in \"element\" mode.",
86
+ type: "string"
87
+ },
88
+ mode: {
89
+ description: "Which form to enforce: \"syntax\" (shorthand `<>...</>`, default) or \"element\" (a named fragment).",
90
+ enum: ["element", "syntax"],
91
+ type: "string"
92
+ }
93
+ },
94
+ type: "object"
79
95
  }];
80
- function create$6(context) {
81
- const name = context.options[0] ?? DEFAULT_FRAGMENT_NAME;
82
- return { JSXFragment(node) {
96
+ /**
97
+ * Flattens a JSX element name into a dotted string, e.g. `Fragment` or
98
+ * `React.Fragment`. Returns `null` for namespaced names (`<a:b>`) which cannot
99
+ * be a fragment.
100
+ *
101
+ * @param node - The JSX element name node.
102
+ * @returns The dotted name, or `null` when it is not a plain identifier chain.
103
+ */
104
+ function jsxNameToString(node) {
105
+ if (node.type === AST_NODE_TYPES.JSXIdentifier) return node.name;
106
+ if (node.type === AST_NODE_TYPES.JSXMemberExpression) {
107
+ const object = jsxNameToString(node.object);
108
+ if (object === null) return null;
109
+ return `${object}.${node.property.name}`;
110
+ }
111
+ return null;
112
+ }
113
+ function create$7(context) {
114
+ const options = context.options[0] ?? {};
115
+ const mode = options.mode ?? DEFAULT_MODE;
116
+ const fragmentName = options.fragmentName ?? DEFAULT_FRAGMENT_NAME;
117
+ if (mode === "element") return { JSXFragment(node) {
83
118
  const { closingFragment, openingFragment } = node;
119
+ context.report({
120
+ data: { name: fragmentName },
121
+ fix: (fixer) => {
122
+ return [fixer.replaceText(openingFragment, `<${fragmentName}>`), fixer.replaceText(closingFragment, `</${fragmentName}>`)];
123
+ },
124
+ messageId: MESSAGE_ID_NAMED,
125
+ node
126
+ });
127
+ } };
128
+ const namedFragments = /* @__PURE__ */ new Set([
129
+ "Fragment",
130
+ fragmentName,
131
+ "React.Fragment"
132
+ ]);
133
+ return { JSXElement(node) {
134
+ const { openingElement } = node;
135
+ const name = jsxNameToString(openingElement.name);
136
+ if (name === null || !namedFragments.has(name)) return;
137
+ if (openingElement.attributes.length > 0) return;
84
138
  context.report({
85
139
  data: { name },
86
140
  fix: (fixer) => {
87
- return [fixer.replaceText(openingFragment, `<${name}>`), fixer.replaceText(closingFragment, `</${name}>`)];
141
+ const { closingElement } = node;
142
+ if (closingElement === null) return fixer.replaceText(node, "<></>");
143
+ return [fixer.replaceText(openingElement, "<>"), fixer.replaceText(closingElement, "</>")];
88
144
  },
89
- messageId: MESSAGE_ID$2,
145
+ messageId: MESSAGE_ID_SHORTHAND,
90
146
  node
91
147
  });
92
148
  } };
93
149
  }
94
150
  const jsxShorthandFragment = createEslintRule({
95
- name: RULE_NAME$6,
96
- create: create$6,
97
- defaultOptions: [DEFAULT_FRAGMENT_NAME],
151
+ name: RULE_NAME$7,
152
+ create: create$7,
153
+ defaultOptions: [{
154
+ fragmentName: DEFAULT_FRAGMENT_NAME,
155
+ mode: DEFAULT_MODE
156
+ }],
98
157
  meta: {
99
- defaultOptions: [DEFAULT_FRAGMENT_NAME],
158
+ defaultOptions: [{
159
+ fragmentName: DEFAULT_FRAGMENT_NAME,
160
+ mode: DEFAULT_MODE
161
+ }],
100
162
  docs: {
101
- description: "Disallow the shorthand fragment syntax in favour of a named fragment",
163
+ description: "Enforce a consistent fragment form: the shorthand `<>...</>` or a named fragment",
102
164
  recommended: false,
103
165
  requiresTypeChecking: false
104
166
  },
105
167
  fixable: "code",
106
168
  hasSuggestions: false,
107
- messages: messages$6,
108
- schema: schema$1,
169
+ messages: messages$7,
170
+ schema: schema$2,
109
171
  type: "suggestion"
110
172
  }
111
173
  });
@@ -1269,8 +1331,8 @@ const SCHEMA = {
1269
1331
  };
1270
1332
  //#endregion
1271
1333
  //#region src/rules/naming-convention/rule.ts
1272
- const RULE_NAME$5 = "naming-convention";
1273
- const messages$5 = {
1334
+ const RULE_NAME$6 = "naming-convention";
1335
+ const messages$6 = {
1274
1336
  doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
1275
1337
  doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
1276
1338
  missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
@@ -1300,7 +1362,7 @@ const camelCaseNamingConfig = [
1300
1362
  selector: "typeLike"
1301
1363
  }
1302
1364
  ];
1303
- function create$5(contextWithoutDefaults) {
1365
+ function create$6(contextWithoutDefaults) {
1304
1366
  const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
1305
1367
  const validators = parseOptions(context);
1306
1368
  const compilerOptions = getParserServices(context, true).program?.getCompilerOptions() ?? {};
@@ -1620,8 +1682,8 @@ function create$5(contextWithoutDefaults) {
1620
1682
  }));
1621
1683
  }
1622
1684
  const namingConvention = createEslintRule({
1623
- name: RULE_NAME$5,
1624
- create: create$5,
1685
+ name: RULE_NAME$6,
1686
+ create: create$6,
1625
1687
  defaultOptions: camelCaseNamingConfig,
1626
1688
  meta: {
1627
1689
  docs: {
@@ -1631,7 +1693,7 @@ const namingConvention = createEslintRule({
1631
1693
  },
1632
1694
  fixable: void 0,
1633
1695
  hasSuggestions: false,
1634
- messages: messages$5,
1696
+ messages: messages$6,
1635
1697
  schema: SCHEMA,
1636
1698
  type: "suggestion"
1637
1699
  }
@@ -1899,15 +1961,15 @@ function resolveFactory(sourceCode, node) {
1899
1961
  }
1900
1962
  //#endregion
1901
1963
  //#region src/rules/no-unnecessary-use-callback/rule.ts
1902
- const RULE_NAME$4 = "no-unnecessary-use-callback";
1964
+ const RULE_NAME$5 = "no-unnecessary-use-callback";
1903
1965
  const MESSAGE_ID_DEFAULT$1 = "default";
1904
1966
  const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
1905
- const messages$4 = {
1967
+ const messages$5 = {
1906
1968
  [MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
1907
1969
  [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
1970
  };
1909
1971
  const noUnnecessaryUseCallback = createEslintRule({
1910
- name: RULE_NAME$4,
1972
+ name: RULE_NAME$5,
1911
1973
  create: createUnnecessaryHookRule({
1912
1974
  hook: "useCallback",
1913
1975
  messageIds: {
@@ -1923,22 +1985,22 @@ const noUnnecessaryUseCallback = createEslintRule({
1923
1985
  requiresTypeChecking: false
1924
1986
  },
1925
1987
  hasSuggestions: false,
1926
- messages: messages$4,
1988
+ messages: messages$5,
1927
1989
  schema: [],
1928
1990
  type: "suggestion"
1929
1991
  }
1930
1992
  });
1931
1993
  //#endregion
1932
1994
  //#region src/rules/no-unnecessary-use-memo/rule.ts
1933
- const RULE_NAME$3 = "no-unnecessary-use-memo";
1995
+ const RULE_NAME$4 = "no-unnecessary-use-memo";
1934
1996
  const MESSAGE_ID_DEFAULT = "default";
1935
1997
  const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
1936
- const messages$3 = {
1998
+ const messages$4 = {
1937
1999
  [MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
1938
2000
  [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
2001
  };
1940
2002
  const noUnnecessaryUseMemo = createEslintRule({
1941
- name: RULE_NAME$3,
2003
+ name: RULE_NAME$4,
1942
2004
  create: createUnnecessaryHookRule({
1943
2005
  hook: "useMemo",
1944
2006
  messageIds: {
@@ -1954,16 +2016,16 @@ const noUnnecessaryUseMemo = createEslintRule({
1954
2016
  requiresTypeChecking: false
1955
2017
  },
1956
2018
  hasSuggestions: false,
1957
- messages: messages$3,
2019
+ messages: messages$4,
1958
2020
  schema: [],
1959
2021
  type: "suggestion"
1960
2022
  }
1961
2023
  });
1962
2024
  //#endregion
1963
2025
  //#region src/rules/prefer-destructuring-assignment/rule.ts
1964
- const RULE_NAME$2 = "prefer-destructuring-assignment";
1965
- const MESSAGE_ID$1 = "default";
1966
- const messages$2 = { [MESSAGE_ID$1]: "Use destructuring assignment for component props." };
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." };
1967
2029
  /**
1968
2030
  * Identifiers that cannot be used as a binding name in strict-mode module code.
1969
2031
  * A property may be accessed with such a name (`props.default`), but a shorthand
@@ -2089,7 +2151,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
2089
2151
  const propertyNames = collectPropertyNames(memberReferences);
2090
2152
  if (!(!hasNonMemberReference && propertyNames !== null && !wouldShadowExistingBinding(context.sourceCode, scope, propertyVariable, memberReferences))) {
2091
2153
  for (const { member } of memberReferences) context.report({
2092
- messageId: MESSAGE_ID$1,
2154
+ messageId: MESSAGE_ID$2,
2093
2155
  node: member
2094
2156
  });
2095
2157
  return;
@@ -2106,7 +2168,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
2106
2168
  if (index === 0) return [fixer.replaceTextRange([propsParameter.range[0], nameEnd], pattern), replaceAccess];
2107
2169
  return replaceAccess;
2108
2170
  },
2109
- messageId: MESSAGE_ID$1,
2171
+ messageId: MESSAGE_ID$2,
2110
2172
  node: member
2111
2173
  });
2112
2174
  }
@@ -2125,7 +2187,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
2125
2187
  * @param context - The rule context.
2126
2188
  * @returns The rule listener.
2127
2189
  */
2128
- function create$2(context) {
2190
+ function create$3(context) {
2129
2191
  const { api, visitor } = core.getFunctionComponentCollector(context);
2130
2192
  return {
2131
2193
  ...visitor,
@@ -2143,8 +2205,8 @@ function create$2(context) {
2143
2205
  };
2144
2206
  }
2145
2207
  const preferDestructuringAssignment = createEslintRule({
2146
- name: RULE_NAME$2,
2147
- create: create$2,
2208
+ name: RULE_NAME$3,
2209
+ create: create$3,
2148
2210
  defaultOptions: [],
2149
2211
  meta: {
2150
2212
  docs: {
@@ -2154,12 +2216,194 @@ const preferDestructuringAssignment = createEslintRule({
2154
2216
  },
2155
2217
  fixable: "code",
2156
2218
  hasSuggestions: false,
2157
- messages: messages$2,
2219
+ messages: messages$3,
2158
2220
  schema: [],
2159
2221
  type: "problem"
2160
2222
  }
2161
2223
  });
2162
2224
  //#endregion
2225
+ //#region src/rules/purity/rule.ts
2226
+ const RULE_NAME$2 = "purity";
2227
+ const MESSAGE_ID$1 = "impureCall";
2228
+ /**
2229
+ * Non-deterministic Luau / Roblox calls, written as dotted paths matching how
2230
+ * they appear in roblox-ts source. `new Random()` is normalized to
2231
+ * `"Random.new"` (see the `NewExpression` visitor).
2232
+ */
2233
+ const DEFAULT_SIGNATURES = [
2234
+ "DateTime.now",
2235
+ "HttpService.GenerateGUID",
2236
+ "Random.new",
2237
+ "Workspace.GetServerTimeNow",
2238
+ "elapsedTime",
2239
+ "math.random",
2240
+ "math.randomseed",
2241
+ "os.clock",
2242
+ "os.date",
2243
+ "os.time",
2244
+ "tick",
2245
+ "time"
2246
+ ];
2247
+ /**
2248
+ * Bare Luau globals for which a matching local binding means the reference is
2249
+ * shadowed rather than the ambient global. Service objects are intentionally
2250
+ * excluded because they are impure even when imported from `@rbxts/services`.
2251
+ */
2252
+ const GUARDED_GLOBALS = /* @__PURE__ */ new Set([
2253
+ "elapsedTime",
2254
+ "math",
2255
+ "os",
2256
+ "tick",
2257
+ "time"
2258
+ ]);
2259
+ 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." };
2260
+ const schema$1 = [{
2261
+ additionalProperties: false,
2262
+ properties: {
2263
+ additionalFunctions: {
2264
+ description: "Extra dotted call signatures to treat as impure (e.g. \"Math.random\").",
2265
+ items: { type: "string" },
2266
+ type: "array",
2267
+ uniqueItems: true
2268
+ },
2269
+ ignore: {
2270
+ description: "Default signatures to exclude (e.g. \"os.date\").",
2271
+ items: { type: "string" },
2272
+ type: "array",
2273
+ uniqueItems: true
2274
+ }
2275
+ },
2276
+ type: "object"
2277
+ }];
2278
+ /**
2279
+ * Strips wrappers that are irrelevant to the callee identity so that
2280
+ * `a.b?.()` and `a.b!()` still match.
2281
+ *
2282
+ * @param node - The node to unwrap.
2283
+ * @returns The unwrapped node.
2284
+ */
2285
+ function unwrap(node) {
2286
+ let current = node;
2287
+ while (current.type === AST_NODE_TYPES.ChainExpression || current.type === AST_NODE_TYPES.TSNonNullExpression) current = current.expression;
2288
+ return current;
2289
+ }
2290
+ /**
2291
+ * Builds the dotted path for a callee, e.g. `math.random` or `tick`.
2292
+ *
2293
+ * @param callee - The callee expression.
2294
+ * @returns The match, or `null` when the callee cannot be represented as a
2295
+ * simple dotted path (computed access, or an object that is an expression).
2296
+ */
2297
+ function dottedCalleePath(callee) {
2298
+ let current = unwrap(callee);
2299
+ const parts = [];
2300
+ while (current.type === AST_NODE_TYPES.MemberExpression) {
2301
+ if (current.computed || current.property.type !== AST_NODE_TYPES.Identifier) return null;
2302
+ parts.unshift(current.property.name);
2303
+ current = unwrap(current.object);
2304
+ }
2305
+ if (current.type !== AST_NODE_TYPES.Identifier) return null;
2306
+ parts.unshift(current.name);
2307
+ return {
2308
+ path: parts.join("."),
2309
+ root: current
2310
+ };
2311
+ }
2312
+ /**
2313
+ * Determines whether an identifier resolves to a real user binding (import,
2314
+ * parameter, or local declaration) rather than the ambient Luau global.
2315
+ *
2316
+ * @param sourceCode - Provides scope lookup.
2317
+ * @param node - The identifier node.
2318
+ * @returns `true` when the identifier is a user binding rather than the ambient
2319
+ * global.
2320
+ */
2321
+ function isUserBinding(sourceCode, node) {
2322
+ let scope = sourceCode.getScope(node);
2323
+ while (scope !== null) {
2324
+ const variable = scope.variables.find((candidate) => candidate.name === node.name);
2325
+ if (variable !== void 0) return variable.defs.length > 0;
2326
+ scope = scope.upper;
2327
+ }
2328
+ return false;
2329
+ }
2330
+ /**
2331
+ * Finds the immediate enclosing function of a node, stopping at the program.
2332
+ *
2333
+ * @param node - The node to search upward from.
2334
+ * @returns The enclosing function, or `null` when the node is at module level.
2335
+ */
2336
+ function enclosingFunction(node) {
2337
+ let current = node.parent;
2338
+ while (current !== void 0) {
2339
+ if (ASTUtils.isFunction(current)) return current;
2340
+ if (current.type === AST_NODE_TYPES.Program) return null;
2341
+ current = current.parent;
2342
+ }
2343
+ return null;
2344
+ }
2345
+ /**
2346
+ * Determines whether a function executes during render: a component body, a
2347
+ * custom/builtin hook body, or a `useMemo` callback.
2348
+ *
2349
+ * @param reactContext - The context `@eslint-react/core` predicates expect.
2350
+ * @param func - The enclosing function node.
2351
+ * @returns `true` when the function runs during render.
2352
+ */
2353
+ function isRenderContext(reactContext, func) {
2354
+ if (core.isFunctionComponentDefinition(reactContext, func, core.DEFAULT_COMPONENT_DETECTION_HINT)) return true;
2355
+ if (core.isHookDefinition(func)) return true;
2356
+ const { parent } = func;
2357
+ return parent.type === AST_NODE_TYPES.CallExpression && parent.arguments[0] === func && core.isUseMemoCall(reactContext, parent);
2358
+ }
2359
+ function create$2(context) {
2360
+ const { sourceCode } = context;
2361
+ const reactContext = context;
2362
+ const options = context.options[0] ?? {};
2363
+ const signatures = new Set(DEFAULT_SIGNATURES);
2364
+ for (const signature of options.additionalFunctions ?? []) signatures.add(signature);
2365
+ for (const signature of options.ignore ?? []) signatures.delete(signature);
2366
+ function handle(node, match, path) {
2367
+ if (!signatures.has(path)) return;
2368
+ if (path === "Random.new" && node.arguments.length > 0) return;
2369
+ if (GUARDED_GLOBALS.has(match.root.name) && isUserBinding(sourceCode, match.root)) return;
2370
+ const func = enclosingFunction(node);
2371
+ if (func === null || !isRenderContext(reactContext, func)) return;
2372
+ context.report({
2373
+ data: { name: path },
2374
+ messageId: MESSAGE_ID$1,
2375
+ node
2376
+ });
2377
+ }
2378
+ return {
2379
+ CallExpression(node) {
2380
+ const match = dottedCalleePath(node.callee);
2381
+ if (match !== null) handle(node, match, match.path);
2382
+ },
2383
+ NewExpression(node) {
2384
+ const match = dottedCalleePath(node.callee);
2385
+ if (match !== null) handle(node, match, `${match.path}.new`);
2386
+ }
2387
+ };
2388
+ }
2389
+ const purity = createEslintRule({
2390
+ name: RULE_NAME$2,
2391
+ create: create$2,
2392
+ defaultOptions: [{}],
2393
+ meta: {
2394
+ defaultOptions: [{}],
2395
+ docs: {
2396
+ description: "Disallow impure calls such as `math.random` or `os.clock` during render",
2397
+ recommended: false,
2398
+ requiresTypeChecking: false
2399
+ },
2400
+ hasSuggestions: false,
2401
+ messages: messages$2,
2402
+ schema: schema$1,
2403
+ type: "problem"
2404
+ }
2405
+ });
2406
+ //#endregion
2163
2407
  //#region src/rules/toml-sort-keys/rule.ts
2164
2408
  const RULE_NAME$1 = "toml-sort-keys";
2165
2409
  const MESSAGE_ID = "unsorted";
@@ -2461,6 +2705,7 @@ const plugin = {
2461
2705
  "no-unnecessary-use-callback": noUnnecessaryUseCallback,
2462
2706
  "no-unnecessary-use-memo": noUnnecessaryUseMemo,
2463
2707
  "prefer-destructuring-assignment": preferDestructuringAssignment,
2708
+ "purity": purity,
2464
2709
  "toml-sort-keys": tomlSortKeys,
2465
2710
  "yaml-block-key-blank-lines": yamlBlockKeyBlankLines
2466
2711
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-flawless",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Your ESLint plugin description",
5
5
  "keywords": [
6
6
  "eslint",