eslint-plugin-flawless 0.1.2 → 0.1.4

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,11 +176,15 @@ pnpm eslint-docs
176
176
  💭
177
177
  Requires [type information](https://typescript-eslint.io/linting/typed-linting).
178
178
 
179
- | Name                       | Description | 🔧 | 💭 |
180
- | :---------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :-- | :-- |
181
- | [naming-convention](src/rules/naming-convention/documentation.md) | Enforce naming conventions for everything across a codebase | | 💭 |
182
- | [toml-sort-keys](src/rules/toml-sort-keys/documentation.md) | Enforce a configured sort order for TOML keys and tables | 🔧 | |
183
- | [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
+ | [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 | 🔧 | |
184
188
 
185
189
  <!-- end auto-generated rules list -->
186
190
 
package/dist/index.d.mts CHANGED
@@ -8,6 +8,9 @@ interface PluginDocumentation {
8
8
  requiresTypeChecking: boolean;
9
9
  }
10
10
  //#endregion
11
+ //#region src/rules/jsx-shorthand-fragment/rule.d.ts
12
+ type Options$2 = [fragmentName?: string];
13
+ //#endregion
11
14
  //#region src/rules/naming-convention/utils/enums.d.ts
12
15
  declare const Selector: {
13
16
  readonly variable: 1;
@@ -115,9 +118,19 @@ interface NamingSelector {
115
118
  }
116
119
  //#endregion
117
120
  //#region src/rules/naming-convention/rule.d.ts
118
- type MessageIds = "doesNotMatchFormat" | "doesNotMatchFormatTrimmed" | "missingAffix" | "missingUnderscore" | "satisfyCustom" | "unexpectedUnderscore";
121
+ type MessageIds$2 = "doesNotMatchFormat" | "doesNotMatchFormatTrimmed" | "missingAffix" | "missingUnderscore" | "satisfyCustom" | "unexpectedUnderscore";
119
122
  type Options$1 = Array<NamingSelector>;
120
123
  //#endregion
124
+ //#region src/rules/no-unnecessary-use-callback/rule.d.ts
125
+ declare const MESSAGE_ID_DEFAULT$1 = "default";
126
+ declare const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
127
+ type MessageIds$1 = typeof MESSAGE_ID_DEFAULT$1 | typeof MESSAGE_ID_INSIDE_USE_EFFECT$1;
128
+ //#endregion
129
+ //#region src/rules/no-unnecessary-use-memo/rule.d.ts
130
+ declare const MESSAGE_ID_DEFAULT = "default";
131
+ declare const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
132
+ type MessageIds = typeof MESSAGE_ID_DEFAULT | typeof MESSAGE_ID_INSIDE_USE_EFFECT;
133
+ //#endregion
121
134
  //#region src/rules/toml-sort-keys/rule.d.ts
122
135
  type Options = Array<SortSpec>;
123
136
  interface SortOrderObject {
@@ -138,7 +151,19 @@ declare const plugin: {
138
151
  version: string;
139
152
  };
140
153
  rules: {
141
- "naming-convention": TSESLint.RuleModule<MessageIds, Options$1, PluginDocumentation, TSESLint.RuleListener> & {
154
+ "jsx-shorthand-boolean": TSESLint.RuleModule<"setAttributeValue", [], PluginDocumentation, TSESLint.RuleListener> & {
155
+ name: string;
156
+ };
157
+ "jsx-shorthand-fragment": TSESLint.RuleModule<"useNamedFragment", Options$2, PluginDocumentation, TSESLint.RuleListener> & {
158
+ name: string;
159
+ };
160
+ "naming-convention": TSESLint.RuleModule<MessageIds$2, Options$1, PluginDocumentation, TSESLint.RuleListener> & {
161
+ name: string;
162
+ };
163
+ "no-unnecessary-use-callback": TSESLint.RuleModule<MessageIds$1, [], PluginDocumentation, TSESLint.RuleListener> & {
164
+ name: string;
165
+ };
166
+ "no-unnecessary-use-memo": TSESLint.RuleModule<MessageIds, [], PluginDocumentation, TSESLint.RuleListener> & {
142
167
  name: string;
143
168
  };
144
169
  "toml-sort-keys": TSESLint.RuleModule<"unsorted", Options, PluginDocumentation, TSESLint.RuleListener> & {
@@ -161,7 +186,19 @@ declare const _default: {
161
186
  version: string;
162
187
  };
163
188
  rules: {
164
- "naming-convention": import("${configDir}").RuleModule<MessageIds, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
189
+ "jsx-shorthand-boolean": import("${configDir}").RuleModule<"setAttributeValue", [], PluginDocumentation, import("${configDir}").RuleListener> & {
190
+ name: string;
191
+ };
192
+ "jsx-shorthand-fragment": import("${configDir}").RuleModule<"useNamedFragment", Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
193
+ name: string;
194
+ };
195
+ "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
196
+ name: string;
197
+ };
198
+ "no-unnecessary-use-callback": import("${configDir}").RuleModule<MessageIds$1, [], PluginDocumentation, import("${configDir}").RuleListener> & {
199
+ name: string;
200
+ };
201
+ "no-unnecessary-use-memo": import("${configDir}").RuleModule<MessageIds, [], PluginDocumentation, import("${configDir}").RuleListener> & {
165
202
  name: string;
166
203
  };
167
204
  "toml-sort-keys": import("${configDir}").RuleModule<"unsorted", Options, PluginDocumentation, import("${configDir}").RuleListener> & {
@@ -181,7 +218,19 @@ declare const _default: {
181
218
  version: string;
182
219
  };
183
220
  rules: {
184
- "naming-convention": import("${configDir}").RuleModule<MessageIds, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
221
+ "jsx-shorthand-boolean": import("${configDir}").RuleModule<"setAttributeValue", [], PluginDocumentation, import("${configDir}").RuleListener> & {
222
+ name: string;
223
+ };
224
+ "jsx-shorthand-fragment": import("${configDir}").RuleModule<"useNamedFragment", Options$2, PluginDocumentation, import("${configDir}").RuleListener> & {
225
+ name: string;
226
+ };
227
+ "naming-convention": import("${configDir}").RuleModule<MessageIds$2, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
228
+ name: string;
229
+ };
230
+ "no-unnecessary-use-callback": import("${configDir}").RuleModule<MessageIds$1, [], PluginDocumentation, import("${configDir}").RuleListener> & {
231
+ name: string;
232
+ };
233
+ "no-unnecessary-use-memo": import("${configDir}").RuleModule<MessageIds, [], PluginDocumentation, import("${configDir}").RuleListener> & {
185
234
  name: string;
186
235
  };
187
236
  "toml-sort-keys": import("${configDir}").RuleModule<"unsorted", Options, PluginDocumentation, import("${configDir}").RuleListener> & {
package/dist/index.mjs CHANGED
@@ -1,12 +1,14 @@
1
+ import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
1
2
  import { DefinitionType, ImplicitLibVariable, PatternVisitor, ScopeType, Visitor } from "@typescript-eslint/scope-manager";
2
3
  import { requiresQuoting } from "@typescript-eslint/type-utils";
3
4
  import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
4
- import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
5
5
  import assert from "node:assert";
6
+ import * as core from "@eslint-react/core";
7
+ import { findVariable } from "@typescript-eslint/utils/ast-utils";
6
8
  import { getStaticTOMLValue } from "toml-eslint-parser";
7
9
  //#region package.json
8
10
  var name = "eslint-plugin-flawless";
9
- var version = "0.1.2";
11
+ var version = "0.1.4";
10
12
  var repository = {
11
13
  "url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
12
14
  "type": "git"
@@ -32,6 +34,82 @@ function createEslintRule(rule) {
32
34
  return createRule(rule);
33
35
  }
34
36
  //#endregion
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) {
42
+ const { sourceCode } = context;
43
+ return { JSXAttribute(node) {
44
+ if (node.value !== null) return;
45
+ context.report({
46
+ data: { name: sourceCode.getText(node.name) },
47
+ fix: (fixer) => fixer.insertTextAfter(node.name, "={true}"),
48
+ messageId: MESSAGE_ID$2,
49
+ node
50
+ });
51
+ } };
52
+ }
53
+ const jsxShorthandBoolean = createEslintRule({
54
+ name: RULE_NAME$6,
55
+ create: create$6,
56
+ defaultOptions: [],
57
+ meta: {
58
+ docs: {
59
+ description: "Disallow shorthand boolean JSX attributes",
60
+ recommended: false,
61
+ requiresTypeChecking: false
62
+ },
63
+ fixable: "code",
64
+ hasSuggestions: false,
65
+ messages: messages$6,
66
+ schema: [],
67
+ type: "suggestion"
68
+ }
69
+ });
70
+ //#endregion
71
+ //#region src/rules/jsx-shorthand-fragment/rule.ts
72
+ const RULE_NAME$5 = "jsx-shorthand-fragment";
73
+ const MESSAGE_ID$1 = "useNamedFragment";
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 = [{
77
+ description: "The identifier to use for the named fragment element.",
78
+ type: "string"
79
+ }];
80
+ function create$5(context) {
81
+ const name = context.options[0] ?? DEFAULT_FRAGMENT_NAME;
82
+ return { JSXFragment(node) {
83
+ const { closingFragment, openingFragment } = node;
84
+ context.report({
85
+ data: { name },
86
+ fix: (fixer) => {
87
+ return [fixer.replaceText(openingFragment, `<${name}>`), fixer.replaceText(closingFragment, `</${name}>`)];
88
+ },
89
+ messageId: MESSAGE_ID$1,
90
+ node
91
+ });
92
+ } };
93
+ }
94
+ const jsxShorthandFragment = createEslintRule({
95
+ name: RULE_NAME$5,
96
+ create: create$5,
97
+ defaultOptions: [DEFAULT_FRAGMENT_NAME],
98
+ meta: {
99
+ defaultOptions: [DEFAULT_FRAGMENT_NAME],
100
+ docs: {
101
+ description: "Disallow the shorthand fragment syntax in favour of a named fragment",
102
+ recommended: false,
103
+ requiresTypeChecking: false
104
+ },
105
+ fixable: "code",
106
+ hasSuggestions: false,
107
+ messages: messages$5,
108
+ schema: schema$1,
109
+ type: "suggestion"
110
+ }
111
+ });
112
+ //#endregion
35
113
  //#region src/utils/is-type-import.ts
36
114
  /**
37
115
  * Determine whether a variable definition is a type import. E.g.:.
@@ -1191,8 +1269,8 @@ const SCHEMA = {
1191
1269
  };
1192
1270
  //#endregion
1193
1271
  //#region src/rules/naming-convention/rule.ts
1194
- const RULE_NAME$2 = "naming-convention";
1195
- const messages$2 = {
1272
+ const RULE_NAME$4 = "naming-convention";
1273
+ const messages$4 = {
1196
1274
  doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
1197
1275
  doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
1198
1276
  missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
@@ -1222,7 +1300,7 @@ const camelCaseNamingConfig = [
1222
1300
  selector: "typeLike"
1223
1301
  }
1224
1302
  ];
1225
- function create$2(contextWithoutDefaults) {
1303
+ function create$4(contextWithoutDefaults) {
1226
1304
  const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
1227
1305
  const validators = parseOptions(context);
1228
1306
  const compilerOptions = getParserServices(context, true).program?.getCompilerOptions() ?? {};
@@ -1542,8 +1620,8 @@ function create$2(contextWithoutDefaults) {
1542
1620
  }));
1543
1621
  }
1544
1622
  const namingConvention = createEslintRule({
1545
- name: RULE_NAME$2,
1546
- create: create$2,
1623
+ name: RULE_NAME$4,
1624
+ create: create$4,
1547
1625
  defaultOptions: camelCaseNamingConfig,
1548
1626
  meta: {
1549
1627
  docs: {
@@ -1553,7 +1631,7 @@ const namingConvention = createEslintRule({
1553
1631
  },
1554
1632
  fixable: void 0,
1555
1633
  hasSuggestions: false,
1556
- messages: messages$2,
1634
+ messages: messages$4,
1557
1635
  schema: SCHEMA,
1558
1636
  type: "suggestion"
1559
1637
  }
@@ -1583,6 +1661,305 @@ function requiresQuoting$1(node, target) {
1583
1661
  return requiresQuoting(node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`, target);
1584
1662
  }
1585
1663
  //#endregion
1664
+ //#region src/utils/nested-expressions.ts
1665
+ /**
1666
+ * Determines whether the given AST subtree contains a call or `new`
1667
+ * expression.
1668
+ *
1669
+ * Replaces `@eslint-react/ast`'s `getNestedCallExpressions` /
1670
+ * `getNestedNewExpressions` (not exposed by `@eslint-react/kit`). The `useMemo`
1671
+ * rule uses this to avoid flagging a factory that performs real computation.
1672
+ * The walk descends into every child node (skipping the `parent` back-link to
1673
+ * avoid cycles), so calls nested in awaits, tagged templates, computed member
1674
+ * expressions, call targets and the like are all detected.
1675
+ *
1676
+ * @param root - The subtree root to inspect (typically a function body).
1677
+ * @returns `true` if a `CallExpression` or `NewExpression` is present.
1678
+ */
1679
+ function hasNestedCallOrNew(root) {
1680
+ const stack = [root];
1681
+ while (stack.length > 0) {
1682
+ const current = stack.pop();
1683
+ if (current === void 0) break;
1684
+ if (current.type === AST_NODE_TYPES.CallExpression || current.type === AST_NODE_TYPES.NewExpression) return true;
1685
+ pushChildNodes(current, stack);
1686
+ }
1687
+ return false;
1688
+ }
1689
+ /**
1690
+ * Type guard for an AST node value encountered while walking arbitrary
1691
+ * properties of a parent node.
1692
+ *
1693
+ * @param value - The candidate value.
1694
+ * @returns `true` if the value looks like a `TSESTree.Node`.
1695
+ */
1696
+ function isNode(value) {
1697
+ return typeof value === "object" && value !== null && typeof value.type === "string";
1698
+ }
1699
+ /**
1700
+ * Pushes every child node of `node` onto the traversal stack, skipping the
1701
+ * `parent` back-link.
1702
+ *
1703
+ * @param node - The node whose children to enqueue.
1704
+ * @param stack - The traversal stack to push onto.
1705
+ */
1706
+ function pushChildNodes(node, stack) {
1707
+ for (const key of Object.keys(node)) {
1708
+ if (key === "parent") continue;
1709
+ const value = node[key];
1710
+ if (Array.isArray(value)) {
1711
+ for (const item of value) if (isNode(item)) stack.push(item);
1712
+ } else if (isNode(value)) stack.push(value);
1713
+ }
1714
+ }
1715
+ //#endregion
1716
+ //#region src/utils/resolve.ts
1717
+ /**
1718
+ * Resolves an identifier to the AST node that represents its value.
1719
+ *
1720
+ * This is a focused re-implementation of `@eslint-react/var`'s `resolve`
1721
+ * covering only the cases the unnecessary-hook rules need: variable
1722
+ * initializers and function/class declarations. Every other definition kind
1723
+ * (imports, parameters, catch bindings, ...) resolves to `null`.
1724
+ *
1725
+ * @param sourceCode - Provides the scope used to look up the binding.
1726
+ * @param node - The identifier to resolve.
1727
+ * @returns The resolved value node, or `null` when it cannot be determined.
1728
+ */
1729
+ function resolve(sourceCode, node) {
1730
+ const variable = findVariable(sourceCode.getScope(node), node);
1731
+ if (variable === null) return null;
1732
+ const definition = variable.defs.at(0);
1733
+ if (definition === void 0) return null;
1734
+ if (definition.type === DefinitionType.ClassName || definition.type === DefinitionType.FunctionName) return definition.node;
1735
+ if (definition.type === DefinitionType.Variable) return definition.node.init;
1736
+ return null;
1737
+ }
1738
+ //#endregion
1739
+ //#region src/utils/unnecessary-hook.ts
1740
+ /**
1741
+ * Builds the `create` for the `no-unnecessary-use-memo` /
1742
+ * `no-unnecessary-use-callback` rules.
1743
+ *
1744
+ * Faithfully ports the rules removed from `eslint-plugin-react-x` (they were
1745
+ * dropped because the React Compiler makes them redundant; the Roblox /
1746
+ * `@rbxts/react` ecosystem has no Compiler). Hook detection is delegated to
1747
+ * `@eslint-react/core` so it matches the upstream semantics (fully-qualified
1748
+ * name resolution across imports, namespaces and `require`).
1749
+ *
1750
+ * @param config - The hook-specific configuration.
1751
+ * @returns A rule `create` function.
1752
+ * @template MessageIds - The rule's message identifiers.
1753
+ */
1754
+ function createUnnecessaryHookRule(config) {
1755
+ const { hook, messageIds } = config;
1756
+ const skipComputation = hook === "useMemo";
1757
+ return (context) => {
1758
+ const { sourceCode } = context;
1759
+ const reactContext = context;
1760
+ const detectHookCall = hook === "useMemo" ? core.isUseMemoCall : core.isUseCallbackCall;
1761
+ return { VariableDeclarator(node) {
1762
+ const { id, init } = node;
1763
+ if (id.type !== AST_NODE_TYPES.Identifier || init?.type !== AST_NODE_TYPES.CallExpression || !detectHookCall(reactContext, init)) return;
1764
+ const [variable, ...rest] = sourceCode.getDeclaredVariables(node);
1765
+ if (variable === void 0 || rest.length > 0) return;
1766
+ const insideEffectReport = checkForUsageInsideUseEffect(sourceCode, init, messageIds.insideUseEffect);
1767
+ const component = sourceCode.getScope(init).block;
1768
+ if (!ASTUtils.isFunction(component)) return;
1769
+ const [argument0, argument1] = init.arguments;
1770
+ if (argument0 === void 0 || argument1 === void 0) return;
1771
+ if (skipComputation && ASTUtils.isFunction(argument0) && hasNestedCallOrNew(argument0.body)) {
1772
+ reportIf(context, insideEffectReport);
1773
+ return;
1774
+ }
1775
+ if (!hasEmptyDeps(sourceCode, argument1)) {
1776
+ reportIf(context, insideEffectReport);
1777
+ return;
1778
+ }
1779
+ const factory = resolveFactory(sourceCode, argument0);
1780
+ if (factory === null) return;
1781
+ if (!referencesComponentScope(sourceCode, factory, component)) {
1782
+ context.report({
1783
+ messageId: messageIds.default,
1784
+ node
1785
+ });
1786
+ return;
1787
+ }
1788
+ reportIf(context, insideEffectReport);
1789
+ } };
1790
+ };
1791
+ }
1792
+ /**
1793
+ * Finds the nearest ancestor that is a `useEffect`-like call.
1794
+ *
1795
+ * @param node - The node to search upward from.
1796
+ * @returns The enclosing effect call, or `null` when there is none.
1797
+ */
1798
+ function findEnclosingEffect(node) {
1799
+ let current = node.parent;
1800
+ while (current !== void 0) {
1801
+ if (core.isUseEffectLikeCall(current)) return current;
1802
+ if (current.type === AST_NODE_TYPES.Program) return null;
1803
+ current = current.parent;
1804
+ }
1805
+ return null;
1806
+ }
1807
+ /**
1808
+ * Reports the "used inside a single useEffect" case when applicable.
1809
+ *
1810
+ * @param sourceCode - Provides declared-variable and text lookups.
1811
+ * @param node - The hook call expression.
1812
+ * @param messageId - The message id to report.
1813
+ * @returns A report descriptor, or `null` when the case does not apply.
1814
+ * @template MessageIds - The rule's message identifiers.
1815
+ */
1816
+ function checkForUsageInsideUseEffect(sourceCode, node, messageId) {
1817
+ if (!/use\w*Effect/u.test(sourceCode.text)) return null;
1818
+ const { parent } = node;
1819
+ if (parent.type !== AST_NODE_TYPES.VariableDeclarator || parent.id.type !== AST_NODE_TYPES.Identifier) return null;
1820
+ const usages = (sourceCode.getDeclaredVariables(parent).at(0)?.references ?? []).filter((reference) => reference.init !== true);
1821
+ if (usages.length === 0) return null;
1822
+ const effects = /* @__PURE__ */ new Set();
1823
+ for (const usage of usages) {
1824
+ const effect = findEnclosingEffect(usage.identifier);
1825
+ if (effect === null) return null;
1826
+ effects.add(effect);
1827
+ if (effects.size > 1) return null;
1828
+ }
1829
+ return {
1830
+ data: { name: parent.id.name },
1831
+ messageId,
1832
+ node
1833
+ };
1834
+ }
1835
+ /**
1836
+ * Determines whether a dependency argument is an empty array (directly or via a
1837
+ * resolvable identifier).
1838
+ *
1839
+ * @param sourceCode - Provides scope lookup to resolve identifiers.
1840
+ * @param node - The dependency argument node.
1841
+ * @returns `true` if the dependencies are empty.
1842
+ */
1843
+ function hasEmptyDeps(sourceCode, node) {
1844
+ if (node.type === AST_NODE_TYPES.ArrayExpression) return node.elements.length === 0;
1845
+ if (node.type === AST_NODE_TYPES.Identifier) {
1846
+ const resolved = resolve(sourceCode, node);
1847
+ return resolved?.type === AST_NODE_TYPES.ArrayExpression && resolved.elements.length === 0;
1848
+ }
1849
+ return false;
1850
+ }
1851
+ /**
1852
+ * Collects a scope together with all of its descendant scopes.
1853
+ *
1854
+ * @param scope - The root scope.
1855
+ * @returns The scope and every nested child scope.
1856
+ */
1857
+ function flattenScopes(scope) {
1858
+ return scope.childScopes.reduce((accumulator, child) => [...accumulator, ...flattenScopes(child)], [scope]);
1859
+ }
1860
+ /**
1861
+ * Determines whether any reference inside the factory resolves to a binding
1862
+ * declared in the component's scope.
1863
+ *
1864
+ * @param sourceCode - Provides scope lookup for the factory.
1865
+ * @param factory - The memoized factory function node.
1866
+ * @param component - The enclosing component function node.
1867
+ * @returns `true` if the factory reads from the component scope.
1868
+ */
1869
+ function referencesComponentScope(sourceCode, factory, component) {
1870
+ return flattenScopes(sourceCode.getScope(factory)).flatMap((scope) => scope.references).some((reference) => reference.resolved?.scope.block === component);
1871
+ }
1872
+ /**
1873
+ * Reports the descriptor when it is present.
1874
+ *
1875
+ * @param context - The rule context.
1876
+ * @param descriptor - The descriptor to report, or `null` to skip.
1877
+ * @template MessageIds - The rule's message identifiers.
1878
+ */
1879
+ function reportIf(context, descriptor) {
1880
+ if (descriptor !== null) context.report(descriptor);
1881
+ }
1882
+ /**
1883
+ * Resolves the first argument of the hook call to the underlying factory
1884
+ * function node, unwrapping a curried arrow (`() => () => ...`) and resolving
1885
+ * identifiers to their initializer.
1886
+ *
1887
+ * @param sourceCode - Provides scope lookup to resolve identifiers.
1888
+ * @param node - The first hook argument.
1889
+ * @returns The factory function node, or `null` when it is not a function.
1890
+ */
1891
+ function resolveFactory(sourceCode, node) {
1892
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) return node.body.type === AST_NODE_TYPES.ArrowFunctionExpression ? node.body : node;
1893
+ if (node.type === AST_NODE_TYPES.FunctionExpression) return node;
1894
+ if (node.type === AST_NODE_TYPES.Identifier) {
1895
+ const resolved = resolve(sourceCode, node);
1896
+ if (resolved?.type === AST_NODE_TYPES.ArrowFunctionExpression || resolved?.type === AST_NODE_TYPES.FunctionExpression) return resolved;
1897
+ }
1898
+ return null;
1899
+ }
1900
+ //#endregion
1901
+ //#region src/rules/no-unnecessary-use-callback/rule.ts
1902
+ const RULE_NAME$3 = "no-unnecessary-use-callback";
1903
+ const MESSAGE_ID_DEFAULT$1 = "default";
1904
+ const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
1905
+ const messages$3 = {
1906
+ [MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
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
+ };
1909
+ const noUnnecessaryUseCallback = createEslintRule({
1910
+ name: RULE_NAME$3,
1911
+ create: createUnnecessaryHookRule({
1912
+ hook: "useCallback",
1913
+ messageIds: {
1914
+ default: MESSAGE_ID_DEFAULT$1,
1915
+ insideUseEffect: MESSAGE_ID_INSIDE_USE_EFFECT$1
1916
+ }
1917
+ }),
1918
+ defaultOptions: [],
1919
+ meta: {
1920
+ docs: {
1921
+ description: "Disallow unnecessary usage of 'useCallback'",
1922
+ recommended: false,
1923
+ requiresTypeChecking: false
1924
+ },
1925
+ hasSuggestions: false,
1926
+ messages: messages$3,
1927
+ schema: [],
1928
+ type: "suggestion"
1929
+ }
1930
+ });
1931
+ //#endregion
1932
+ //#region src/rules/no-unnecessary-use-memo/rule.ts
1933
+ const RULE_NAME$2 = "no-unnecessary-use-memo";
1934
+ const MESSAGE_ID_DEFAULT = "default";
1935
+ const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
1936
+ const messages$2 = {
1937
+ [MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
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
+ };
1940
+ const noUnnecessaryUseMemo = createEslintRule({
1941
+ name: RULE_NAME$2,
1942
+ create: createUnnecessaryHookRule({
1943
+ hook: "useMemo",
1944
+ messageIds: {
1945
+ default: MESSAGE_ID_DEFAULT,
1946
+ insideUseEffect: MESSAGE_ID_INSIDE_USE_EFFECT
1947
+ }
1948
+ }),
1949
+ defaultOptions: [],
1950
+ meta: {
1951
+ docs: {
1952
+ description: "Disallow unnecessary usage of 'useMemo'",
1953
+ recommended: false,
1954
+ requiresTypeChecking: false
1955
+ },
1956
+ hasSuggestions: false,
1957
+ messages: messages$2,
1958
+ schema: [],
1959
+ type: "suggestion"
1960
+ }
1961
+ });
1962
+ //#endregion
1586
1963
  //#region src/rules/toml-sort-keys/rule.ts
1587
1964
  const RULE_NAME$1 = "toml-sort-keys";
1588
1965
  const MESSAGE_ID = "unsorted";
@@ -1878,7 +2255,11 @@ const plugin = {
1878
2255
  version
1879
2256
  },
1880
2257
  rules: {
2258
+ "jsx-shorthand-boolean": jsxShorthandBoolean,
2259
+ "jsx-shorthand-fragment": jsxShorthandFragment,
1881
2260
  "naming-convention": namingConvention,
2261
+ "no-unnecessary-use-callback": noUnnecessaryUseCallback,
2262
+ "no-unnecessary-use-memo": noUnnecessaryUseMemo,
1882
2263
  "toml-sort-keys": tomlSortKeys,
1883
2264
  "yaml-block-key-blank-lines": yamlBlockKeyBlankLines
1884
2265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-flawless",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Your ESLint plugin description",
5
5
  "keywords": [
6
6
  "eslint",
@@ -34,6 +34,7 @@
34
34
  "*": "eslint --fix --cache"
35
35
  },
36
36
  "dependencies": {
37
+ "@eslint-react/core": "5.10.0",
37
38
  "@typescript-eslint/scope-manager": "8.62.1",
38
39
  "@typescript-eslint/type-utils": "8.62.1",
39
40
  "@typescript-eslint/utils": "8.62.1",