@codyswann/lisa 1.50.0 → 1.50.2

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 (21) hide show
  1. package/node_modules/@codyswann/eslint-plugin-code-organization/README.md +149 -0
  2. package/node_modules/@codyswann/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +473 -0
  3. package/node_modules/@codyswann/eslint-plugin-code-organization/index.js +28 -0
  4. package/node_modules/@codyswann/eslint-plugin-code-organization/package.json +10 -0
  5. package/node_modules/@codyswann/eslint-plugin-code-organization/rules/enforce-statement-order.js +162 -0
  6. package/node_modules/@codyswann/eslint-plugin-component-structure/README.md +234 -0
  7. package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/plugin-index.test.js +89 -0
  8. package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/require-memo-in-view.test.js +201 -0
  9. package/node_modules/@codyswann/eslint-plugin-component-structure/__tests__/single-component-per-file.test.js +294 -0
  10. package/node_modules/@codyswann/eslint-plugin-component-structure/index.js +37 -0
  11. package/node_modules/@codyswann/eslint-plugin-component-structure/package.json +10 -0
  12. package/node_modules/@codyswann/eslint-plugin-component-structure/rules/enforce-component-structure.js +235 -0
  13. package/node_modules/@codyswann/eslint-plugin-component-structure/rules/no-return-in-view.js +96 -0
  14. package/node_modules/@codyswann/eslint-plugin-component-structure/rules/require-memo-in-view.js +183 -0
  15. package/node_modules/@codyswann/eslint-plugin-component-structure/rules/single-component-per-file.js +243 -0
  16. package/node_modules/@codyswann/eslint-plugin-ui-standards/README.md +192 -0
  17. package/node_modules/@codyswann/eslint-plugin-ui-standards/index.js +31 -0
  18. package/node_modules/@codyswann/eslint-plugin-ui-standards/package.json +10 -0
  19. package/node_modules/@codyswann/eslint-plugin-ui-standards/rules/no-classname-outside-ui.js +56 -0
  20. package/node_modules/@codyswann/eslint-plugin-ui-standards/rules/no-direct-rn-imports.js +60 -0
  21. package/package.json +6 -1
@@ -0,0 +1,294 @@
1
+ /**
2
+ * This file is managed by Lisa.
3
+ * Do not edit directly — changes will be overwritten on the next `lisa` run.
4
+ */
5
+
6
+ /**
7
+ * Unit tests for the single-component-per-file ESLint rule
8
+ *
9
+ * Tests that View and Container files contain exactly one React component.
10
+ * Ensures components/ui/** and components/shared/** directories are excluded from the rule.
11
+ * @module eslint-plugin-component-structure/tests
12
+ */
13
+
14
+ const { RuleTester } = require("eslint");
15
+
16
+ const rule = require("../rules/single-component-per-file");
17
+
18
+ const FEATURE_EXAMPLE_COMPONENTS_PATH = "/features/example/components";
19
+ const SHARED_COMPONENTS_PATH = "/components/shared";
20
+ const UI_COMPONENTS_PATH = "/components/ui";
21
+
22
+ const ruleTester = new RuleTester({
23
+ languageOptions: {
24
+ ecmaVersion: 2020,
25
+ sourceType: "module",
26
+ parserOptions: {
27
+ ecmaFeatures: {
28
+ jsx: true,
29
+ },
30
+ },
31
+ },
32
+ });
33
+
34
+ ruleTester.run("single-component-per-file", rule, {
35
+ valid: [
36
+ // 1. Single component - arrow function
37
+ {
38
+ code: `
39
+ const MyView = () => <div>Hello</div>;
40
+ MyView.displayName = "MyView";
41
+ export default MyView;
42
+ `,
43
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
44
+ },
45
+ // 2. Single component - memo wrapped
46
+ {
47
+ code: `
48
+ import { memo } from "react";
49
+ const MyView = memo(() => <div>Hello</div>);
50
+ MyView.displayName = "MyView";
51
+ export default MyView;
52
+ `,
53
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
54
+ },
55
+ // 3. Single component - React.memo wrapped
56
+ {
57
+ code: `
58
+ import React from "react";
59
+ const MyView = React.memo(() => <div>Hello</div>);
60
+ MyView.displayName = "MyView";
61
+ export default MyView;
62
+ `,
63
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
64
+ },
65
+ // 4. Single component - function declaration
66
+ {
67
+ code: `
68
+ function MyView() {
69
+ return <div>Hello</div>;
70
+ }
71
+ export default MyView;
72
+ `,
73
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
74
+ },
75
+ // 5. Single component - TypeScript React.FC (parser will handle TypeScript syntax)
76
+ {
77
+ code: `
78
+ const MyView: React.FC = () => <div>Hello</div>;
79
+ MyView.displayName = "MyView";
80
+ export default MyView;
81
+ `,
82
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
83
+ languageOptions: {
84
+ parser: require("@typescript-eslint/parser"),
85
+ },
86
+ },
87
+ // 6. Single component in Container file
88
+ {
89
+ code: `
90
+ const MyContainer = () => {
91
+ return <div>Hello</div>;
92
+ };
93
+ export default MyContainer;
94
+ `,
95
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyContainer.tsx`,
96
+ },
97
+ // 7. PascalCase function that doesn't return JSX (not a component)
98
+ {
99
+ code: `
100
+ const MyView = () => <div>Hello</div>;
101
+ const HelperFunction = () => {
102
+ return "not JSX";
103
+ };
104
+ export default MyView;
105
+ `,
106
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
107
+ },
108
+ // 8. Non-View/Container file (rule should not apply)
109
+ {
110
+ code: `
111
+ const Component1 = () => <div>1</div>;
112
+ const Component2 = () => <div>2</div>;
113
+ export { Component1, Component2 };
114
+ `,
115
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/utils.tsx`,
116
+ },
117
+ // 9. Excluded directory - UI components
118
+ {
119
+ code: `
120
+ const Component1 = () => <div>1</div>;
121
+ const Component2 = () => <div>2</div>;
122
+ export { Component1, Component2 };
123
+ `,
124
+ filename: `${UI_COMPONENTS_PATH}/MyView.tsx`,
125
+ },
126
+ // 10. Excluded directory - Shared components
127
+ {
128
+ code: `
129
+ const Component1 = () => <div>1</div>;
130
+ const Component2 = () => <div>2</div>;
131
+ export { Component1, Component2 };
132
+ `,
133
+ filename: `${SHARED_COMPONENTS_PATH}/MyView.tsx`,
134
+ },
135
+ // 11. Single component - conditional expression
136
+ {
137
+ code: `
138
+ const MyView = ({ show }) => show ? <div>Visible</div> : <div>Hidden</div>;
139
+ export default MyView;
140
+ `,
141
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
142
+ },
143
+ // 12. Single component - logical expression
144
+ {
145
+ code: `
146
+ const MyView = ({ show }) => show && <div>Content</div>;
147
+ export default MyView;
148
+ `,
149
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
150
+ },
151
+ ],
152
+ invalid: [
153
+ // 1. Two arrow function components
154
+ {
155
+ code: `
156
+ const Component1 = () => <div>First</div>;
157
+ const Component2 = () => <div>Second</div>;
158
+ export default Component1;
159
+ `,
160
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
161
+ errors: [
162
+ {
163
+ messageId: "multipleComponents",
164
+ data: {
165
+ componentName: "Component2",
166
+ firstComponentName: "Component1",
167
+ },
168
+ },
169
+ ],
170
+ },
171
+ // 2. Two memo-wrapped components
172
+ {
173
+ code: `
174
+ import { memo } from "react";
175
+ const Component1 = memo(() => <div>First</div>);
176
+ const Component2 = memo(() => <div>Second</div>);
177
+ export default Component1;
178
+ `,
179
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
180
+ errors: [
181
+ {
182
+ messageId: "multipleComponents",
183
+ data: {
184
+ componentName: "Component2",
185
+ firstComponentName: "Component1",
186
+ },
187
+ },
188
+ ],
189
+ },
190
+ // 3. Main component and helper component (realistic violation)
191
+ {
192
+ code: `
193
+ import React from "react";
194
+ const MessageItem = ({ item }) => <div>{item}</div>;
195
+ const MessageListView = ({ messages }) => (
196
+ <div>{messages.map(msg => <MessageItem item={msg} />)}</div>
197
+ );
198
+ export default MessageListView;
199
+ `,
200
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MessageListView.tsx`,
201
+ errors: [
202
+ {
203
+ messageId: "multipleComponents",
204
+ data: {
205
+ componentName: "MessageListView",
206
+ firstComponentName: "MessageItem",
207
+ },
208
+ },
209
+ ],
210
+ },
211
+ // 4. Function declaration and arrow function
212
+ {
213
+ code: `
214
+ function Component1() {
215
+ return <div>First</div>;
216
+ }
217
+ const Component2 = () => <div>Second</div>;
218
+ export default Component1;
219
+ `,
220
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
221
+ errors: [
222
+ {
223
+ messageId: "multipleComponents",
224
+ data: {
225
+ componentName: "Component2",
226
+ firstComponentName: "Component1",
227
+ },
228
+ },
229
+ ],
230
+ },
231
+ // 5. Three components (multiple violations)
232
+ {
233
+ code: `
234
+ const Component1 = () => <div>First</div>;
235
+ const Component2 = () => <div>Second</div>;
236
+ const Component3 = () => <div>Third</div>;
237
+ export default Component1;
238
+ `,
239
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
240
+ errors: [
241
+ {
242
+ messageId: "multipleComponents",
243
+ data: {
244
+ componentName: "Component2",
245
+ firstComponentName: "Component1",
246
+ },
247
+ },
248
+ {
249
+ messageId: "multipleComponents",
250
+ data: {
251
+ componentName: "Component3",
252
+ firstComponentName: "Component1",
253
+ },
254
+ },
255
+ ],
256
+ },
257
+ // 6. Container file with multiple components
258
+ {
259
+ code: `
260
+ const Helper = () => <div>Helper</div>;
261
+ const MyContainer = () => <div><Helper /></div>;
262
+ export default MyContainer;
263
+ `,
264
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyContainer.tsx`,
265
+ errors: [
266
+ {
267
+ messageId: "multipleComponents",
268
+ data: {
269
+ componentName: "MyContainer",
270
+ firstComponentName: "Helper",
271
+ },
272
+ },
273
+ ],
274
+ },
275
+ // 7. Multiple components with conditional expressions
276
+ {
277
+ code: `
278
+ const Component1 = ({ show }) => show ? <div>First</div> : null;
279
+ const Component2 = ({ show }) => show && <div>Second</div>;
280
+ export default Component1;
281
+ `,
282
+ filename: `${FEATURE_EXAMPLE_COMPONENTS_PATH}/MyView.tsx`,
283
+ errors: [
284
+ {
285
+ messageId: "multipleComponents",
286
+ data: {
287
+ componentName: "Component2",
288
+ firstComponentName: "Component1",
289
+ },
290
+ },
291
+ ],
292
+ },
293
+ ],
294
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * This file is managed by Lisa.
3
+ * Do not edit directly — changes will be overwritten on the next `lisa` run.
4
+ */
5
+
6
+ /**
7
+ * ESLint plugin for component structure standards
8
+ *
9
+ * This plugin enforces component structure and patterns for React components
10
+ * in the frontend application. Supports ESLint 9 flat config format.
11
+ *
12
+ * Rules:
13
+ * - enforce-component-structure: Ensures components follow the Container/View pattern
14
+ * - no-return-in-view: Disallows return statements in View components
15
+ * - require-memo-in-view: Enforces React.memo usage in View components
16
+ * - single-component-per-file: Ensures only one React component per file
17
+ * @module eslint-plugin-component-structure
18
+ */
19
+ const enforceComponentStructure = require("./rules/enforce-component-structure");
20
+ const noReturnInView = require("./rules/no-return-in-view");
21
+ const requireMemoInView = require("./rules/require-memo-in-view");
22
+ const singleComponentPerFile = require("./rules/single-component-per-file");
23
+
24
+ const plugin = {
25
+ meta: {
26
+ name: "eslint-plugin-component-structure",
27
+ version: "1.0.0",
28
+ },
29
+ rules: {
30
+ "enforce-component-structure": enforceComponentStructure,
31
+ "no-return-in-view": noReturnInView,
32
+ "require-memo-in-view": requireMemoInView,
33
+ "single-component-per-file": singleComponentPerFile,
34
+ },
35
+ };
36
+
37
+ module.exports = plugin;
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@codyswann/eslint-plugin-component-structure",
3
+ "version": "1.0.0",
4
+ "description": "ESLint plugin for component structure standards",
5
+ "main": "index.js",
6
+ "publishConfig": { "access": "public" },
7
+ "peerDependencies": {
8
+ "eslint": ">=9.0.0"
9
+ }
10
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * This file is managed by Lisa.
3
+ * Do not edit directly — changes will be overwritten on the next `lisa` run.
4
+ */
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description:
14
+ "Enforce component structure in features/**/components directories",
15
+ category: "Best Practices",
16
+ recommended: true,
17
+ },
18
+ fixable: null,
19
+ schema: [],
20
+ messages: {
21
+ missingContainer:
22
+ 'Component directory "{{componentName}}" is missing {{componentName}}Container.tsx file',
23
+ missingView:
24
+ 'Component directory "{{componentName}}" is missing {{componentName}}View.tsx file',
25
+ missingIndex:
26
+ 'Component directory "{{componentName}}" is missing index.tsx file',
27
+ incorrectIndexExport:
28
+ "index.tsx should export {{componentName}}Container or {{componentName}}View as default",
29
+ componentNotInDirectory:
30
+ "Component files must be inside a directory named after the component",
31
+ incorrectFileNaming: "{{fileName}} should be named {{expectedName}}",
32
+ invalidFileInComponentDirectory:
33
+ "Only index.ts(x), {{componentName}}Container.tsx, and {{componentName}}View.tsx are allowed in component directories. Found: {{fileName}}",
34
+ },
35
+ },
36
+
37
+ create(context) {
38
+ const filename = context.getFilename();
39
+ const normalizedPath = filename.replace(/\\/g, "/");
40
+
41
+ // Get the path after components/ or screens/
42
+ const componentsMatch = normalizedPath.match(
43
+ /\/(components|screens)\/(.+)$/
44
+ );
45
+ if (!componentsMatch) return {};
46
+ const afterComponents = componentsMatch[2];
47
+
48
+ const pathParts = afterComponents.split("/");
49
+
50
+ // If file is directly in components/ directory (not in a subdirectory)
51
+ if (pathParts.length === 1) {
52
+ const fileName = pathParts[0];
53
+ if (fileName.endsWith(".tsx") || fileName.endsWith(".jsx")) {
54
+ context.report({
55
+ node: context.getSourceCode().ast,
56
+ messageId: "componentNotInDirectory",
57
+ });
58
+ }
59
+ return {};
60
+ }
61
+
62
+ // Get component name and file name from the END of the path
63
+ // This handles both ComponentName/file.tsx and custom/ui/ComponentName/file.tsx
64
+ const fileName = pathParts[pathParts.length - 1];
65
+ const componentName = pathParts[pathParts.length - 2];
66
+
67
+ // Skip validation for files in __tests__ directories
68
+ if (componentName === "__tests__") {
69
+ return {};
70
+ }
71
+
72
+ // Only check .ts/.tsx/.jsx files
73
+ if (
74
+ !fileName ||
75
+ (!fileName.endsWith(".ts") &&
76
+ !fileName.endsWith(".tsx") &&
77
+ !fileName.endsWith(".jsx"))
78
+ ) {
79
+ return {};
80
+ }
81
+
82
+ // Get the directory path
83
+ const dirPath = path.dirname(filename);
84
+
85
+ // Check if file is one of the allowed types
86
+ const isIndex =
87
+ fileName === "index.ts" ||
88
+ fileName === "index.tsx" ||
89
+ fileName === "index.jsx";
90
+
91
+ // Allow *Container.*.tsx and *Container.*.jsx patterns (e.g., MyComponentContainer.native.tsx)
92
+ const containerPattern = new RegExp(
93
+ `^${componentName}Container\\.[^.]+\\.(tsx|jsx)$`
94
+ );
95
+ const isContainer =
96
+ fileName === `${componentName}Container.tsx` ||
97
+ fileName === `${componentName}Container.jsx` ||
98
+ containerPattern.test(fileName);
99
+
100
+ // Allow *View.*.tsx and *View.*.jsx patterns (e.g., MyComponentView.web.tsx)
101
+ const viewPattern = new RegExp(
102
+ `^${componentName}View\\.[^.]+\\.(tsx|jsx)$`
103
+ );
104
+ const isView =
105
+ fileName === `${componentName}View.tsx` ||
106
+ fileName === `${componentName}View.jsx` ||
107
+ viewPattern.test(fileName);
108
+
109
+ // Report error if file is not one of the allowed types
110
+ if (!isIndex && !isContainer && !isView) {
111
+ context.report({
112
+ node: context.getSourceCode().ast,
113
+ messageId: "invalidFileInComponentDirectory",
114
+ data: { fileName, componentName },
115
+ });
116
+ return {};
117
+ }
118
+
119
+ // Check file naming
120
+ if (
121
+ fileName === "index.ts" ||
122
+ fileName === "index.tsx" ||
123
+ fileName === "index.jsx"
124
+ ) {
125
+ // Check if index.tsx exports the Container
126
+ return {
127
+ Program(node) {
128
+ const sourceCode = context.getSourceCode();
129
+ const text = sourceCode.getText();
130
+
131
+ // Check for export patterns (allow Container or View)
132
+ const defaultExportPattern = new RegExp(
133
+ `export\\s*{\\s*default\\s*}\\s*from\\s*['"\`]\\.\\/${componentName}(Container|View)['"\`]|` +
134
+ `export\\s*\\*\\s*from\\s*['"\`]\\.\\/${componentName}(Container|View)['"\`]|` +
135
+ `export\\s*{\\s*${componentName}(Container|View)\\s*as\\s*default\\s*}|` +
136
+ `export\\s*default\\s*${componentName}(Container|View)`
137
+ );
138
+
139
+ if (!defaultExportPattern.test(text)) {
140
+ context.report({
141
+ node,
142
+ messageId: "incorrectIndexExport",
143
+ data: { componentName },
144
+ });
145
+ }
146
+ },
147
+ };
148
+ } else if (
149
+ fileName.endsWith("Container.tsx") ||
150
+ fileName.endsWith("Container.jsx")
151
+ ) {
152
+ const expectedName = `${componentName}Container.tsx`;
153
+ if (
154
+ fileName !== expectedName &&
155
+ fileName !== `${componentName}Container.jsx`
156
+ ) {
157
+ context.report({
158
+ node: context.getSourceCode().ast,
159
+ messageId: "incorrectFileNaming",
160
+ data: { fileName, expectedName },
161
+ });
162
+ }
163
+ } else if (fileName.endsWith("View.tsx") || fileName.endsWith("View.jsx")) {
164
+ const expectedName = `${componentName}View.tsx`;
165
+ if (
166
+ fileName !== expectedName &&
167
+ fileName !== `${componentName}View.jsx`
168
+ ) {
169
+ context.report({
170
+ node: context.getSourceCode().ast,
171
+ messageId: "incorrectFileNaming",
172
+ data: { fileName, expectedName },
173
+ });
174
+ }
175
+ }
176
+
177
+ // Check for required files in the directory (only once per directory)
178
+ // We'll do this check only for the first file we encounter
179
+ const cache = new Map();
180
+ const checkRequiredFiles = () => {
181
+ try {
182
+ const files = cache.has(dirPath)
183
+ ? cache.get(dirPath)
184
+ : (() => {
185
+ const dirFiles = fs.readdirSync(dirPath);
186
+ cache.set(dirPath, dirFiles);
187
+ return dirFiles;
188
+ })();
189
+ const hasContainer = files.some(
190
+ f =>
191
+ f === `${componentName}Container.tsx` ||
192
+ f === `${componentName}Container.jsx`
193
+ );
194
+ const hasView = files.some(
195
+ f =>
196
+ f === `${componentName}View.tsx` || f === `${componentName}View.jsx`
197
+ );
198
+ const hasIndex = files.some(
199
+ f => f === "index.tsx" || f === "index.jsx"
200
+ );
201
+
202
+ if (!hasContainer) {
203
+ context.report({
204
+ node: context.getSourceCode().ast,
205
+ messageId: "missingContainer",
206
+ data: { componentName },
207
+ });
208
+ }
209
+ if (!hasView) {
210
+ context.report({
211
+ node: context.getSourceCode().ast,
212
+ messageId: "missingView",
213
+ data: { componentName },
214
+ });
215
+ }
216
+ if (!hasIndex) {
217
+ context.report({
218
+ node: context.getSourceCode().ast,
219
+ messageId: "missingIndex",
220
+ data: { componentName },
221
+ });
222
+ }
223
+ } catch (_err) {
224
+ // Directory might not exist or be accessible
225
+ }
226
+ };
227
+
228
+ // Only check once per file
229
+ if (fileName === "index.tsx" || fileName === "index.jsx") {
230
+ checkRequiredFiles();
231
+ }
232
+
233
+ return {};
234
+ },
235
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * This file is managed by Lisa.
3
+ * Do not edit directly — changes will be overwritten on the next `lisa` run.
4
+ */
5
+
6
+ module.exports = {
7
+ meta: {
8
+ type: "problem",
9
+ docs: {
10
+ description:
11
+ "Disallow return statements in View components - use arrow function shorthand",
12
+ category: "Best Practices",
13
+ recommended: true,
14
+ },
15
+ fixable: null,
16
+ schema: [],
17
+ messages: {
18
+ noReturnInView:
19
+ "View components should use arrow function shorthand: () => (...) instead of () => { return (...) }. Hoist any definitions outside of the arrow function body or into the corresponding Container.",
20
+ },
21
+ },
22
+
23
+ create(context) {
24
+ const filename = context.getFilename();
25
+ const normalizedPath = filename.replace(/\\/g, "/");
26
+
27
+ // Only check View.tsx and View.jsx files
28
+ if (!filename.endsWith("View.tsx") && !filename.endsWith("View.jsx")) {
29
+ return {};
30
+ }
31
+
32
+ // Check if file is in features/**/components, features/**/screens, or components directory
33
+ const isFeatureComponent =
34
+ normalizedPath.includes("features/") &&
35
+ normalizedPath.includes("/components/");
36
+ const isFeatureScreen =
37
+ normalizedPath.includes("features/") &&
38
+ normalizedPath.includes("/screens/");
39
+ const isComponentsDir =
40
+ normalizedPath.includes("/components/") &&
41
+ !normalizedPath.includes("/components/ui/") &&
42
+ !normalizedPath.includes("/components/custom/ui/");
43
+
44
+ if (!isFeatureComponent && !isFeatureScreen && !isComponentsDir) {
45
+ return {};
46
+ }
47
+
48
+ return {
49
+ ArrowFunctionExpression(node) {
50
+ // Check if this is a component (starts with capital letter or is exported)
51
+ const parent = node.parent;
52
+
53
+ // Helper to check if variable has a View component name
54
+ const isViewComponent = name =>
55
+ /^[A-Z]/.test(name) && name.includes("View");
56
+
57
+ // Determine if this is a component
58
+ const isComponent = (() => {
59
+ // Check if it's a default export
60
+ if (parent.type === "ExportDefaultDeclaration") {
61
+ return true;
62
+ }
63
+
64
+ // Check if it's a variable declaration with PascalCase name
65
+ if (
66
+ parent.type === "VariableDeclarator" &&
67
+ parent.id.type === "Identifier"
68
+ ) {
69
+ return isViewComponent(parent.id.name);
70
+ }
71
+
72
+ // Check if it's part of an export statement
73
+ if (
74
+ parent.type === "VariableDeclarator" &&
75
+ parent.parent.type === "VariableDeclaration" &&
76
+ parent.parent.parent.type === "ExportNamedDeclaration"
77
+ ) {
78
+ return isViewComponent(parent.id.name);
79
+ }
80
+
81
+ return false;
82
+ })();
83
+
84
+ if (!isComponent) return;
85
+
86
+ // Check if the arrow function has a block body (any BlockStatement is forbidden)
87
+ if (node.body.type === "BlockStatement") {
88
+ context.report({
89
+ node,
90
+ messageId: "noReturnInView",
91
+ });
92
+ }
93
+ },
94
+ };
95
+ },
96
+ };