@next-friday/eslint-plugin-friday 1.0.0 → 1.0.1

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.
Files changed (3) hide show
  1. package/README.md +10 -10
  2. package/dist/index.mjs +104 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,16 +33,16 @@ The package exports rule implementations and their documented options.
33
33
 
34
34
  🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
35
35
 
36
- | Name                                | Description | 🔧 |
37
- | :--------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-- |
38
- | [component-module](docs/rules/component-module.md) | require React component modules to contain only imports, directives, export lists or re-exports, TypeScript interfaces, React function components, and explicitly allowed exported declarations | |
39
- | [index-export-only](docs/rules/index-export-only.md) | require index files to contain only imports, exports without inline runtime implementation, directives, and type declarations | |
40
- | [jsx-newline-between-elements](docs/rules/jsx-newline-between-elements.md) | require empty lines between adjacent JSX elements, fragments, or expression containers when either is multi-line | 🔧 |
41
- | [jsx-no-newline-single-line-elements](docs/rules/jsx-no-newline-single-line-elements.md) | disallow empty lines between adjacent single-line JSX elements or fragments | 🔧 |
42
- | [named-props](docs/rules/named-props.md) | disallow inline intersections in the first parameter of React function components | |
43
- | [no-lazy-identifiers](docs/rules/no-lazy-identifiers.md) | disallow lazy placeholder identifiers such as repeated characters and keyboard-row runs | |
44
- | [object-curly-newline](docs/rules/object-curly-newline.md) | require every non-empty object literal to use multiline braces | 🔧 |
45
- | [props-in-body](docs/rules/props-in-body.md) | disallow object destructuring in the first parameter of React function components | |
36
+ | Name                                | Description | 🔧 |
37
+ | :--------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-- |
38
+ | [component-module](docs/rules/component-module.md) | require React component modules to contain only component definitions, component-support declarations, imports, exports, directives, and explicitly allowed framework declarations | |
39
+ | [index-export-only](docs/rules/index-export-only.md) | require index files to contain only imports, exports without inline runtime implementation, directives, and type declarations | |
40
+ | [jsx-newline-between-elements](docs/rules/jsx-newline-between-elements.md) | require empty lines between adjacent JSX elements, fragments, or expression containers when either is multi-line | 🔧 |
41
+ | [jsx-no-newline-single-line-elements](docs/rules/jsx-no-newline-single-line-elements.md) | disallow empty lines between adjacent single-line JSX elements or fragments | 🔧 |
42
+ | [named-props](docs/rules/named-props.md) | disallow inline intersections in the first parameter of React function components | |
43
+ | [no-lazy-identifiers](docs/rules/no-lazy-identifiers.md) | disallow lazy placeholder identifiers such as repeated characters and keyboard-row runs | |
44
+ | [object-curly-newline](docs/rules/object-curly-newline.md) | require every non-empty object literal to use multiline braces | 🔧 |
45
+ | [props-in-body](docs/rules/props-in-body.md) | disallow object destructuring in the first parameter of React function components | |
46
46
 
47
47
  <!-- end auto-generated rules list -->
48
48
 
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  const meta = {
6
6
  name: "@next-friday/eslint-plugin-friday",
7
7
  namespace: "friday",
8
- version: "1.0.0"
8
+ version: "1.0.1"
9
9
  };
10
10
  //#endregion
11
11
  //#region src/utils/function-component-visitor.ts
@@ -37,8 +37,10 @@ const MESSAGE_ID$6 = "componentModule";
37
37
  const ALLOWED_MODULE_STATEMENT_TYPES = /* @__PURE__ */ new Set([
38
38
  "ExportAllDeclaration",
39
39
  "ImportDeclaration",
40
- "TSInterfaceDeclaration"
40
+ "TSInterfaceDeclaration",
41
+ "TSTypeAliasDeclaration"
41
42
  ]);
43
+ const COMPONENT_METADATA_PROPERTIES = /* @__PURE__ */ new Set(["displayName"]);
42
44
  const EXPORT_STATEMENT_TYPES = /* @__PURE__ */ new Set(["ExportDefaultDeclaration", "ExportNamedDeclaration"]);
43
45
  /**
44
46
  * Check whether a declaration contains only React function components.
@@ -54,6 +56,89 @@ function isAllowedComponentDeclaration(node, components) {
54
56
  }
55
57
  }
56
58
  /**
59
+ * Collect local React bindings needed to recognize component-support declarations.
60
+ * @param statements Top-level program statements.
61
+ * @returns Local createContext and React namespace/default import names.
62
+ */
63
+ function getReactBindings(statements) {
64
+ const createContextNames = /* @__PURE__ */ new Set();
65
+ const namespaceNames = /* @__PURE__ */ new Set();
66
+ for (const statement of statements) {
67
+ if (statement.type !== "ImportDeclaration" || statement.importKind === "type" || statement.source?.value !== "react") continue;
68
+ const specifiers = statement.specifiers;
69
+ for (const specifier of specifiers) {
70
+ if (specifier.importKind === "type") continue;
71
+ if (specifier.type === "ImportSpecifier" && specifier.imported?.name === "createContext" && specifier.local?.name !== void 0) createContextNames.add(specifier.local.name);
72
+ if ((specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier") && specifier.local?.name !== void 0) namespaceNames.add(specifier.local.name);
73
+ }
74
+ }
75
+ return {
76
+ createContextNames,
77
+ namespaceNames
78
+ };
79
+ }
80
+ /**
81
+ * Check whether a declaration is owned directly by the program body or an export wrapper.
82
+ * @param declaration Declaration node to locate.
83
+ * @param statements Top-level program statements.
84
+ * @returns Whether the declaration belongs to module scope.
85
+ */
86
+ function isTopLevelDeclaration(declaration, statements) {
87
+ return statements.some((statement) => Object.is(statement, declaration) || EXPORT_STATEMENT_TYPES.has(statement.type) && Object.is(statement.declaration, declaration));
88
+ }
89
+ /**
90
+ * Collect module-scope bindings for detected React function components.
91
+ * @param components React function components detected by the React detector.
92
+ * @param statements Top-level program statements.
93
+ * @returns Component bindings that can own component metadata or identifier exports.
94
+ */
95
+ function getComponentNames(components, statements) {
96
+ const names = /* @__PURE__ */ new Set();
97
+ for (const component of components) {
98
+ const initPath = component.initPath;
99
+ if (initPath === null || !isTopLevelDeclaration(initPath[0], statements)) continue;
100
+ if (initPath.length === 1) {
101
+ const declaration = initPath[0];
102
+ if (declaration.id?.name !== void 0) names.add(declaration.id.name);
103
+ continue;
104
+ }
105
+ const declarator = initPath[1];
106
+ names.add(declarator.id.name);
107
+ }
108
+ return names;
109
+ }
110
+ /**
111
+ * Check whether an expression calls React.createContext through a verified React import.
112
+ * @param expression Candidate initializer.
113
+ * @param reactBindings Local React bindings.
114
+ * @returns Whether the expression is a React createContext call.
115
+ */
116
+ function isReactCreateContextCall(expression, reactBindings) {
117
+ if (expression?.type !== "CallExpression") return false;
118
+ const { callee } = expression;
119
+ return callee?.type === "Identifier" && callee.name !== void 0 && reactBindings.createContextNames.has(callee.name) ? true : callee?.type === "MemberExpression" && callee.computed !== true && callee.object?.type === "Identifier" && callee.object.name !== void 0 && reactBindings.namespaceNames.has(callee.object.name) && callee.property?.type === "Identifier" && callee.property.name === "createContext";
120
+ }
121
+ /**
122
+ * Check whether a variable declaration contains only React structural declarations.
123
+ * @param node Declaration node to evaluate.
124
+ * @param reactBindings Local React bindings.
125
+ * @returns Whether every declared value is a React context.
126
+ */
127
+ function isAllowedReactStructuralDeclaration(node, reactBindings) {
128
+ return node.type === "VariableDeclaration" && node.declarations !== void 0 && node.declarations.length > 0 && node.declarations.every((declaration) => isReactCreateContextCall(declaration.init, reactBindings));
129
+ }
130
+ /**
131
+ * Check whether a top-level statement assigns supported metadata to a detected component.
132
+ * @param node Top-level program statement.
133
+ * @param componentNames Detected local React component names.
134
+ * @returns Whether the statement is supported component metadata.
135
+ */
136
+ function isAllowedComponentMetadataStatement(node, componentNames) {
137
+ if (node.type !== "ExpressionStatement" || node.expression?.type !== "AssignmentExpression" || node.expression.operator !== "=") return false;
138
+ const { left, right } = node.expression;
139
+ return left.type === "MemberExpression" && left.computed !== true && left.object?.type === "Identifier" && left.object.name !== void 0 && componentNames.has(left.object.name) && left.property?.type === "Identifier" && left.property.name !== void 0 && COMPONENT_METADATA_PROPERTIES.has(left.property.name) && right.type === "Literal" && typeof right.value === "string";
140
+ }
141
+ /**
57
142
  * Check whether a declaration name is explicitly allowed by framework policy.
58
143
  * @param node Declaration node to evaluate.
59
144
  * @param allowedDeclarations Framework-owned declaration names.
@@ -70,19 +155,21 @@ function isAllowedDeclarationName(node, allowedDeclarations) {
70
155
  * Check whether an export contains only allowed declarations.
71
156
  * @param node Export statement to evaluate.
72
157
  * @param components React function components detected by the React detector.
158
+ * @param componentNames Detected local React component names.
159
+ * @param reactBindings Local React bindings.
73
160
  * @param allowedDeclarations Framework-owned declaration names.
74
161
  * @returns Whether the export belongs in the component module.
75
162
  */
76
- function isAllowedExportStatement(node, components, allowedDeclarations) {
163
+ function isAllowedExportStatement(node, components, componentNames, reactBindings, allowedDeclarations) {
77
164
  if (!EXPORT_STATEMENT_TYPES.has(node.type)) return false;
78
165
  if (!node.declaration) return true;
79
166
  const declaration = node.declaration;
80
- return declaration.type === "TSInterfaceDeclaration" || isAllowedComponentDeclaration(declaration, components) || isAllowedDeclarationName(declaration, allowedDeclarations);
167
+ return declaration.type === "TSInterfaceDeclaration" || declaration.type === "TSTypeAliasDeclaration" || declaration.type === "Identifier" && declaration.name !== void 0 && componentNames.has(declaration.name) || isAllowedComponentDeclaration(declaration, components) || isAllowedReactStructuralDeclaration(declaration, reactBindings) || isAllowedDeclarationName(declaration, allowedDeclarations);
81
168
  }
82
169
  /**
83
170
  * Check whether the statement is permitted module syntax.
84
171
  * @param node Top-level program statement.
85
- * @returns Whether the statement is an import, re-export, interface, or directive.
172
+ * @returns Whether the statement is an import, re-export, type declaration, or directive.
86
173
  */
87
174
  function isAllowedModuleSyntax(node) {
88
175
  return ALLOWED_MODULE_STATEMENT_TYPES.has(node.type) || node.type === "ExpressionStatement" && node.directive !== void 0;
@@ -91,33 +178,33 @@ function isAllowedModuleSyntax(node) {
91
178
  * Check whether a top-level statement is permitted in a React component module.
92
179
  * @param node Top-level program statement.
93
180
  * @param components React function components detected by the React detector.
181
+ * @param componentNames Detected local React component names.
182
+ * @param reactBindings Local React bindings.
94
183
  * @param allowedDeclarations Framework-owned declaration names.
95
184
  * @returns Whether the statement belongs in a JSX or TSX component module.
96
185
  */
97
- function isAllowedStatement$1(node, components, allowedDeclarations) {
98
- return isAllowedModuleSyntax(node) || isAllowedComponentDeclaration(node, components) || isAllowedExportStatement(node, components, allowedDeclarations);
186
+ function isAllowedStatement$1(node, components, componentNames, reactBindings, allowedDeclarations) {
187
+ return isAllowedModuleSyntax(node) || isAllowedComponentDeclaration(node, components) || isAllowedReactStructuralDeclaration(node, reactBindings) || isAllowedComponentMetadataStatement(node, componentNames) || isAllowedExportStatement(node, components, componentNames, reactBindings, allowedDeclarations);
99
188
  }
100
189
  const componentModule = {
101
190
  create(context) {
102
191
  const [options] = context.options;
103
192
  const allowedDeclarations = new Set(options?.allowDeclarations);
104
193
  return createFunctionComponentVisitor(context, (node, components) => {
105
- let index = 0;
106
- while (index < node.body.length) {
107
- const statement = node.body[index];
108
- index += 1;
109
- if (!isAllowedStatement$1(statement, components, allowedDeclarations)) context.report({
110
- messageId: MESSAGE_ID$6,
111
- node: statement
112
- });
113
- }
194
+ const statements = node.body;
195
+ const componentNames = getComponentNames(components, statements);
196
+ const reactBindings = getReactBindings(statements);
197
+ for (const statement of statements) if (!isAllowedStatement$1(statement, components, componentNames, reactBindings, allowedDeclarations)) context.report({
198
+ messageId: MESSAGE_ID$6,
199
+ node: statement
200
+ });
114
201
  });
115
202
  },
116
203
  meta: {
117
204
  languages: ["js/js"],
118
205
  type: "suggestion",
119
206
  docs: {
120
- description: "require React component modules to contain only imports, directives, export lists or re-exports, TypeScript interfaces, React function components, and explicitly allowed exported declarations",
207
+ description: "require React component modules to contain only component definitions, component-support declarations, imports, exports, directives, and explicitly allowed framework declarations",
121
208
  url: getRuleDocumentationUrl("component-module")
122
209
  },
123
210
  defaultOptions: [{ allowDeclarations: [] }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next-friday/eslint-plugin-friday",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Next Friday-specific ESLint rules for shared engineering standards.",
5
5
  "keywords": [
6
6
  "eslint",