@timothyrusso/eslint-plugin-arch 0.1.0

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/dist/configs/recommended.d.ts +33 -0
  4. package/dist/configs/recommended.d.ts.map +1 -0
  5. package/dist/configs/recommended.js +103 -0
  6. package/dist/configs/recommended.js.map +1 -0
  7. package/dist/createRule.d.ts +6 -0
  8. package/dist/createRule.d.ts.map +1 -0
  9. package/dist/createRule.js +4 -0
  10. package/dist/createRule.js.map +1 -0
  11. package/dist/index.d.ts +19 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +13 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/kitConfig.d.ts +26 -0
  16. package/dist/kitConfig.d.ts.map +1 -0
  17. package/dist/kitConfig.js +2 -0
  18. package/dist/kitConfig.js.map +1 -0
  19. package/dist/paths.d.ts +13 -0
  20. package/dist/paths.d.ts.map +1 -0
  21. package/dist/paths.js +47 -0
  22. package/dist/paths.js.map +1 -0
  23. package/dist/plugin.d.ts +10 -0
  24. package/dist/plugin.d.ts.map +1 -0
  25. package/dist/plugin.js +13 -0
  26. package/dist/plugin.js.map +1 -0
  27. package/dist/rules/index.d.ts +43 -0
  28. package/dist/rules/index.d.ts.map +1 -0
  29. package/dist/rules/index.js +24 -0
  30. package/dist/rules/index.js.map +1 -0
  31. package/dist/rules/noDashes.d.ts +8 -0
  32. package/dist/rules/noDashes.d.ts.map +1 -0
  33. package/dist/rules/noDashes.js +59 -0
  34. package/dist/rules/noDashes.js.map +1 -0
  35. package/dist/rules/noEffectInViews.d.ts +9 -0
  36. package/dist/rules/noEffectInViews.d.ts.map +1 -0
  37. package/dist/rules/noEffectInViews.js +43 -0
  38. package/dist/rules/noEffectInViews.js.map +1 -0
  39. package/dist/rules/noEffectRunInFacades.d.ts +9 -0
  40. package/dist/rules/noEffectRunInFacades.d.ts.map +1 -0
  41. package/dist/rules/noEffectRunInFacades.js +50 -0
  42. package/dist/rules/noEffectRunInFacades.js.map +1 -0
  43. package/dist/rules/noInlineComments.d.ts +8 -0
  44. package/dist/rules/noInlineComments.d.ts.map +1 -0
  45. package/dist/rules/noInlineComments.js +128 -0
  46. package/dist/rules/noInlineComments.js.map +1 -0
  47. package/dist/rules/noJsxCommentText.d.ts +9 -0
  48. package/dist/rules/noJsxCommentText.d.ts.map +1 -0
  49. package/dist/rules/noJsxCommentText.js +31 -0
  50. package/dist/rules/noJsxCommentText.js.map +1 -0
  51. package/dist/rules/noLiteralGutter.d.ts +17 -0
  52. package/dist/rules/noLiteralGutter.d.ts.map +1 -0
  53. package/dist/rules/noLiteralGutter.js +155 -0
  54. package/dist/rules/noLiteralGutter.js.map +1 -0
  55. package/dist/rules/noRelativeImports.d.ts +13 -0
  56. package/dist/rules/noRelativeImports.d.ts.map +1 -0
  57. package/dist/rules/noRelativeImports.js +51 -0
  58. package/dist/rules/noRelativeImports.js.map +1 -0
  59. package/dist/rules/preferViewmodel.d.ts +12 -0
  60. package/dist/rules/preferViewmodel.d.ts.map +1 -0
  61. package/dist/rules/preferViewmodel.js +93 -0
  62. package/dist/rules/preferViewmodel.js.map +1 -0
  63. package/dist/rules/stableRowHandlers.d.ts +10 -0
  64. package/dist/rules/stableRowHandlers.d.ts.map +1 -0
  65. package/dist/rules/stableRowHandlers.js +162 -0
  66. package/dist/rules/stableRowHandlers.js.map +1 -0
  67. package/dist/rules/viewmodelReturnShape.d.ts +5 -0
  68. package/dist/rules/viewmodelReturnShape.d.ts.map +1 -0
  69. package/dist/rules/viewmodelReturnShape.js +150 -0
  70. package/dist/rules/viewmodelReturnShape.js.map +1 -0
  71. package/package.json +46 -0
@@ -0,0 +1,51 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { createRule } from '../createRule.js';
3
+ import { matchesAny, relativePath } from '../paths.js';
4
+ const RELATIVE = /^\.\.?(?:\/|$)/;
5
+ /**
6
+ * Imports go through the path alias (`@/features/...`), never `./` or `../`, so a file can move
7
+ * without rewriting its imports. `allow` lists folders or files, as globs relative to the ESLint
8
+ * working directory, where relative imports are fine.
9
+ */
10
+ export const noRelativeImports = createRule({
11
+ name: 'no-relative-imports',
12
+ meta: {
13
+ type: 'problem',
14
+ docs: { description: 'Forbids `./` and `../` imports outside an allowlist of folders.' },
15
+ schema: [
16
+ {
17
+ type: 'object',
18
+ properties: { allow: { type: 'array', items: { type: 'string' } } },
19
+ additionalProperties: false,
20
+ },
21
+ ],
22
+ messages: {
23
+ relative: 'Import {{source}} through the path alias, not a relative path.',
24
+ },
25
+ },
26
+ defaultOptions: [{ allow: [] }],
27
+ create(context, [options]) {
28
+ const allow = options.allow ?? [];
29
+ if (allow.length > 0 && matchesAny(relativePath(context.cwd, context.filename), allow))
30
+ return {};
31
+ function check(source) {
32
+ if (source?.type !== AST_NODE_TYPES.Literal || typeof source.value !== 'string')
33
+ return;
34
+ if (RELATIVE.test(source.value)) {
35
+ context.report({ node: source, messageId: 'relative', data: { source: source.value } });
36
+ }
37
+ }
38
+ return {
39
+ ImportDeclaration: node => check(node.source),
40
+ ExportNamedDeclaration: node => check(node.source),
41
+ ExportAllDeclaration: node => check(node.source),
42
+ ImportExpression: node => check(node.source),
43
+ TSImportType: node => check(node.source),
44
+ CallExpression(node) {
45
+ if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === 'require')
46
+ check(node.arguments[0]);
47
+ },
48
+ };
49
+ },
50
+ });
51
+ //# sourceMappingURL=noRelativeImports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noRelativeImports.js","sourceRoot":"","sources":["../../src/rules/noRelativeImports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAiB,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIvD,MAAM,QAAQ,GAAG,gBAAgB,CAAC;AAElC;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,UAAU,CAAsB;IAC/D,IAAI,EAAE,qBAAqB;IAC3B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,WAAW,EAAE,iEAAiE,EAAE;QACxF,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;gBACnE,oBAAoB,EAAE,KAAK;aAC5B;SACF;QACD,QAAQ,EAAE;YACR,QAAQ,EAAE,gEAAgE;SAC3E;KACF;IACD,cAAc,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC/B,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC;QACvB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QAClC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAElG,SAAS,KAAK,CAAC,MAAwC;YACrD,IAAI,MAAM,EAAE,IAAI,KAAK,cAAc,CAAC,OAAO,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;gBAAE,OAAO;YACxF,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC1F,CAAC;QACH,CAAC;QAED,OAAO;YACL,iBAAiB,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;YAC7C,sBAAsB,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;YAClD,oBAAoB,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;YAChD,gBAAgB,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;YAC5C,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;YACxC,cAAc,CAAC,IAAI;gBACjB,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS;oBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YACjH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ type Options = [{
2
+ allow?: string[];
3
+ }];
4
+ /**
5
+ * A `.tsx` backed by a ViewModel (it imports its sibling `.logic` module) calls only its own
6
+ * ViewModel hook, at most once. `allow` names hooks a view may still call directly.
7
+ */
8
+ export declare const preferViewmodel: import("@typescript-eslint/utils/ts-eslint").RuleModule<"foreignHook" | "calledTwice", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
9
+ name: string;
10
+ };
11
+ export {};
12
+ //# sourceMappingURL=preferViewmodel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preferViewmodel.d.ts","sourceRoot":"","sources":["../../src/rules/preferViewmodel.ts"],"names":[],"mappings":"AAIA,KAAK,OAAO,GAAG,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAAC;AAgBtC;;;GAGG;AACH,eAAO,MAAM,eAAe;;CA8E1B,CAAC"}
@@ -0,0 +1,93 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { createRule } from '../createRule.js';
3
+ import { isTestFile } from '../paths.js';
4
+ function isHookCall(name) {
5
+ return /^use[A-Z]/.test(name);
6
+ }
7
+ function basename(path, suffix) {
8
+ return path.slice(path.lastIndexOf('/') + 1).replace(suffix, '');
9
+ }
10
+ /**
11
+ * A `.tsx` backed by a ViewModel (it imports its sibling `.logic` module) calls only its own
12
+ * ViewModel hook, at most once. `allow` names hooks a view may still call directly.
13
+ */
14
+ export const preferViewmodel = createRule({
15
+ name: 'prefer-viewmodel',
16
+ meta: {
17
+ type: 'problem',
18
+ docs: {
19
+ description: 'A .tsx backed by a ViewModel (a `.logic` import) may call only its own ViewModel hook, at most once, and no other hooks.',
20
+ },
21
+ schema: [
22
+ {
23
+ type: 'object',
24
+ properties: {
25
+ allow: { type: 'array', items: { type: 'string' } },
26
+ },
27
+ additionalProperties: false,
28
+ },
29
+ ],
30
+ messages: {
31
+ foreignHook: 'A ViewModel-backed component may only call its own ViewModel hook ({{expected}}). Move this call ({{found}}) into the ViewModel.',
32
+ calledTwice: 'The ViewModel hook {{expected}} must be called at most once.',
33
+ },
34
+ },
35
+ defaultOptions: [{ allow: [] }],
36
+ create(context, [options]) {
37
+ const filename = context.filename.split('\\').join('/');
38
+ if (!filename.endsWith('.tsx') || isTestFile(filename))
39
+ return {};
40
+ // NOTE: a view's own ViewModel is the sibling `.logic` module with the same basename
41
+ // (SelectDatesPage.tsx and SelectDatesPage.logic). A hook from any other `.logic` module is
42
+ // another view's ViewModel, and therefore foreign.
43
+ const fileBase = basename(filename, /\.tsx$/);
44
+ const allow = new Set(options.allow ?? []);
45
+ const viewModelHooks = new Set();
46
+ // NOTE: classification waits for Program:exit, once every import is known, so a call that
47
+ // appears before its hoisted `.logic` import is not misread as foreign.
48
+ const allCalls = [];
49
+ return {
50
+ ImportDeclaration(node) {
51
+ const source = node.source.value;
52
+ if (!/\.logic$/.test(source) || basename(source, /\.logic$/) !== fileBase)
53
+ return;
54
+ for (const spec of node.specifiers) {
55
+ if (spec.type === AST_NODE_TYPES.ImportSpecifier || spec.type === AST_NODE_TYPES.ImportDefaultSpecifier) {
56
+ viewModelHooks.add(spec.local.name);
57
+ }
58
+ }
59
+ },
60
+ CallExpression(node) {
61
+ const isMember = node.callee.type === AST_NODE_TYPES.MemberExpression;
62
+ const callee = node.callee.type === AST_NODE_TYPES.MemberExpression ? node.callee.property : node.callee;
63
+ if (callee.type !== AST_NODE_TYPES.Identifier || !isHookCall(callee.name))
64
+ return;
65
+ allCalls.push({ node, name: callee.name, isMember });
66
+ },
67
+ 'Program:exit'() {
68
+ if (viewModelHooks.size === 0)
69
+ return;
70
+ const expected = [...viewModelHooks].join(' | ');
71
+ const perHook = new Map();
72
+ const secondCalls = [];
73
+ for (const call of allCalls) {
74
+ // NOTE: the ViewModel hook is an imported binding called as a bare identifier. A member
75
+ // call (`X.useY()`) is a different binding, so it is always foreign.
76
+ if (!call.isMember && viewModelHooks.has(call.name)) {
77
+ const count = (perHook.get(call.name) ?? 0) + 1;
78
+ perHook.set(call.name, count);
79
+ if (count > 1)
80
+ secondCalls.push(call);
81
+ }
82
+ else if (!allow.has(call.name)) {
83
+ context.report({ node: call.node, messageId: 'foreignHook', data: { expected, found: call.name } });
84
+ }
85
+ }
86
+ for (const call of secondCalls) {
87
+ context.report({ node: call.node, messageId: 'calledTwice', data: { expected: call.name } });
88
+ }
89
+ },
90
+ };
91
+ },
92
+ });
93
+ //# sourceMappingURL=preferViewmodel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preferViewmodel.js","sourceRoot":"","sources":["../../src/rules/preferViewmodel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ /**
3
+ * In a file that renders a `FlashList` or `FlatList`, every function passed to a row component (one
4
+ * rendered by `renderItem`) or to a `memo` component is a `useCallback` result or a module-level
5
+ * function. An inline arrow is a new prop on every render and defeats the row's `memo`.
6
+ */
7
+ export declare const stableRowHandlers: TSESLint.RuleModule<"inline" | "unstable", [], unknown, TSESLint.RuleListener> & {
8
+ name: string;
9
+ };
10
+ //# sourceMappingURL=stableRowHandlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stableRowHandlers.d.ts","sourceRoot":"","sources":["../../src/rules/stableRowHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,QAAQ,EAAiB,MAAM,0BAA0B,CAAC;AAiF7F;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;;CA4E5B,CAAC"}
@@ -0,0 +1,162 @@
1
+ import { AST_NODE_TYPES, ASTUtils, TSESLint } from '@typescript-eslint/utils';
2
+ import { createRule } from '../createRule.js';
3
+ const { DefinitionType } = TSESLint.Scope;
4
+ const LISTS = new Set(['FlashList', 'FlatList']);
5
+ function isFunction(node) {
6
+ return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
7
+ node?.type === AST_NODE_TYPES.FunctionExpression ||
8
+ node?.type === AST_NODE_TYPES.FunctionDeclaration);
9
+ }
10
+ /** `name(...)` or `X.name(...)`. */
11
+ function isCallTo(node, name) {
12
+ if (node?.type !== AST_NODE_TYPES.CallExpression)
13
+ return false;
14
+ const callee = node.callee;
15
+ if (callee.type === AST_NODE_TYPES.Identifier)
16
+ return callee.name === name;
17
+ return (callee.type === AST_NODE_TYPES.MemberExpression &&
18
+ callee.property.type === AST_NODE_TYPES.Identifier &&
19
+ callee.property.name === name);
20
+ }
21
+ function elementName(name) {
22
+ if (name.type === AST_NODE_TYPES.JSXIdentifier)
23
+ return name.name;
24
+ if (name.type === AST_NODE_TYPES.JSXMemberExpression)
25
+ return `${elementName(name.object)}.${name.property.name}`;
26
+ return `${name.namespace.name}:${name.name.name}`;
27
+ }
28
+ function lastSegment(name) {
29
+ return name.slice(name.lastIndexOf('.') + 1);
30
+ }
31
+ function isNode(value) {
32
+ return typeof value === 'object' && value !== null && typeof value.type === 'string';
33
+ }
34
+ /**
35
+ * The outermost elements a render function returns: the first JSX element on every path, looking
36
+ * through fragments. Elements nested inside a row are the row's own business. The nodes are
37
+ * collected, not their names, so a host element such as `View` elsewhere in the file is untouched.
38
+ */
39
+ function rowElementsIn(root, out) {
40
+ if (root.type === AST_NODE_TYPES.JSXElement) {
41
+ if (/^[A-Z]/.test(lastSegment(elementName(root.openingElement.name))))
42
+ out.add(root.openingElement);
43
+ return;
44
+ }
45
+ if (isFunction(root))
46
+ return;
47
+ for (const [key, value] of Object.entries(root)) {
48
+ if (key === 'parent')
49
+ continue;
50
+ const children = Array.isArray(value) ? value : [value];
51
+ for (const child of children)
52
+ if (isNode(child))
53
+ rowElementsIn(child, out);
54
+ }
55
+ }
56
+ /** The function a `renderItem` value names: inline, wrapped in `useCallback`, or a local binding. */
57
+ function renderFunction(scope, value) {
58
+ if (isFunction(value))
59
+ return value;
60
+ if (isCallTo(value, 'useCallback')) {
61
+ const first = value.arguments[0];
62
+ return isFunction(first) ? first : undefined;
63
+ }
64
+ if (value.type !== AST_NODE_TYPES.Identifier)
65
+ return undefined;
66
+ const def = ASTUtils.findVariable(scope, value.name)?.defs[0];
67
+ if (def?.type === DefinitionType.FunctionName)
68
+ return def.node.type === AST_NODE_TYPES.FunctionDeclaration ? def.node : undefined;
69
+ if (def?.type === DefinitionType.Variable && def.node.init)
70
+ return renderFunction(scope, def.node.init);
71
+ return undefined;
72
+ }
73
+ /**
74
+ * In a file that renders a `FlashList` or `FlatList`, every function passed to a row component (one
75
+ * rendered by `renderItem`) or to a `memo` component is a `useCallback` result or a module-level
76
+ * function. An inline arrow is a new prop on every render and defeats the row's `memo`.
77
+ */
78
+ export const stableRowHandlers = createRule({
79
+ name: 'stable-row-handlers',
80
+ meta: {
81
+ type: 'problem',
82
+ docs: {
83
+ description: 'In a list file, functions passed to row and memo components are useCallback results or module-level functions, never inline.',
84
+ },
85
+ schema: [],
86
+ messages: {
87
+ inline: '{{prop}} on {{component}} is created on every render. Pass a useCallback result or a module-level function that takes the id.',
88
+ unstable: '{{prop}} on {{component}} is {{name}}, a function recreated on every render. Wrap it in useCallback or move it to module level.',
89
+ },
90
+ },
91
+ defaultOptions: [],
92
+ create(context) {
93
+ const sourceCode = context.sourceCode;
94
+ const usages = [];
95
+ const lists = [];
96
+ const memoComponents = new Set();
97
+ function checkAttribute(attribute, component, scope) {
98
+ if (attribute.value?.type !== AST_NODE_TYPES.JSXExpressionContainer)
99
+ return;
100
+ const value = attribute.value.expression;
101
+ if (value.type === AST_NODE_TYPES.JSXEmptyExpression)
102
+ return;
103
+ const prop = elementName(attribute.name);
104
+ if (isFunction(value) || isCallTo(value, 'bind')) {
105
+ context.report({ node: value, messageId: 'inline', data: { prop, component } });
106
+ return;
107
+ }
108
+ if (value.type !== AST_NODE_TYPES.Identifier)
109
+ return;
110
+ const variable = ASTUtils.findVariable(scope, value.name);
111
+ const def = variable?.defs[0];
112
+ if (!variable || !def || variable.scope.type === 'module' || variable.scope.type === 'global')
113
+ return;
114
+ const unstable = (def.type === DefinitionType.FunctionName && isFunction(def.node)) ||
115
+ (def.type === DefinitionType.Variable && isFunction(def.node.init));
116
+ if (unstable) {
117
+ context.report({ node: value, messageId: 'unstable', data: { prop, component, name: value.name } });
118
+ }
119
+ }
120
+ return {
121
+ VariableDeclarator(node) {
122
+ if (node.id.type === AST_NODE_TYPES.Identifier && isCallTo(node.init, 'memo'))
123
+ memoComponents.add(node.id.name);
124
+ },
125
+ JSXOpeningElement(node) {
126
+ const usage = { element: node, scope: sourceCode.getScope(node) };
127
+ if (LISTS.has(lastSegment(elementName(node.name))))
128
+ lists.push(usage);
129
+ usages.push(usage);
130
+ },
131
+ 'Program:exit'() {
132
+ if (lists.length === 0)
133
+ return;
134
+ const rows = new Set();
135
+ for (const { element, scope } of lists) {
136
+ for (const attribute of element.attributes) {
137
+ if (attribute.type !== AST_NODE_TYPES.JSXAttribute || attribute.name.name !== 'renderItem')
138
+ continue;
139
+ if (attribute.value?.type !== AST_NODE_TYPES.JSXExpressionContainer)
140
+ continue;
141
+ const expression = attribute.value.expression;
142
+ if (expression.type === AST_NODE_TYPES.JSXEmptyExpression)
143
+ continue;
144
+ const render = renderFunction(scope, expression);
145
+ if (render)
146
+ rowElementsIn(render.body, rows);
147
+ }
148
+ }
149
+ for (const { element, scope } of usages) {
150
+ const component = elementName(element.name);
151
+ if (!rows.has(element) && !memoComponents.has(component))
152
+ continue;
153
+ for (const attribute of element.attributes) {
154
+ if (attribute.type === AST_NODE_TYPES.JSXAttribute)
155
+ checkAttribute(attribute, component, scope);
156
+ }
157
+ }
158
+ },
159
+ };
160
+ },
161
+ });
162
+ //# sourceMappingURL=stableRowHandlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stableRowHandlers.js","sourceRoot":"","sources":["../../src/rules/stableRowHandlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** A ViewModel (`*.logic.ts`) hook returns `{ state, derived?, effects }` or nothing. */
2
+ export declare const viewmodelReturnShape: import("@typescript-eslint/utils/ts-eslint").RuleModule<"notObject" | "wrongShape", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
3
+ name: string;
4
+ };
5
+ //# sourceMappingURL=viewmodelReturnShape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewmodelReturnShape.d.ts","sourceRoot":"","sources":["../../src/rules/viewmodelReturnShape.ts"],"names":[],"mappings":"AAyEA,yFAAyF;AACzF,eAAO,MAAM,oBAAoB;;CAgF/B,CAAC"}
@@ -0,0 +1,150 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { createRule } from '../createRule.js';
3
+ import { isTestFile } from '../paths.js';
4
+ const ALLOWED = new Set(['state', 'derived', 'effects']);
5
+ const SHAPE = '{ state, derived?, effects }';
6
+ const NESTED_FUNCTION_TYPES = new Set([
7
+ AST_NODE_TYPES.FunctionDeclaration,
8
+ AST_NODE_TYPES.FunctionExpression,
9
+ AST_NODE_TYPES.ArrowFunctionExpression,
10
+ ]);
11
+ /**
12
+ * Peels `x as const`, `x satisfies T` and `x!` off a value: the TypeScript parser nests the object
13
+ * literal under `.expression`, and without this `return { state } as const` reads as a non-object.
14
+ */
15
+ function unwrapTs(node) {
16
+ let current = node;
17
+ while (current.type === AST_NODE_TYPES.TSAsExpression ||
18
+ current.type === AST_NODE_TYPES.TSSatisfiesExpression ||
19
+ current.type === AST_NODE_TYPES.TSNonNullExpression) {
20
+ current = current.expression;
21
+ }
22
+ return current;
23
+ }
24
+ /** A hook is any function whose name starts with `use` and an uppercase letter. */
25
+ function isHookName(name) {
26
+ return /^use[A-Z]/.test(name);
27
+ }
28
+ function isNode(value) {
29
+ return typeof value === 'object' && value !== null && typeof value.type === 'string';
30
+ }
31
+ /**
32
+ * The return statements that belong to the hook itself. Nested function bodies are skipped: their
33
+ * returns belong to those functions (a `useEffect` cleanup, a callback), not to the hook.
34
+ */
35
+ function collectOwnReturns(body) {
36
+ const returns = [];
37
+ function walk(node) {
38
+ if (node.type === AST_NODE_TYPES.ReturnStatement)
39
+ returns.push(node);
40
+ for (const [key, value] of Object.entries(node)) {
41
+ if (key === 'parent')
42
+ continue;
43
+ const children = Array.isArray(value) ? value : [value];
44
+ for (const child of children) {
45
+ if (!isNode(child) || NESTED_FUNCTION_TYPES.has(child.type))
46
+ continue;
47
+ walk(child);
48
+ }
49
+ }
50
+ }
51
+ walk(body);
52
+ return returns;
53
+ }
54
+ function keysOf(object) {
55
+ const keys = [];
56
+ for (const property of object.properties) {
57
+ if (property.type !== AST_NODE_TYPES.Property || property.computed)
58
+ continue;
59
+ if (property.key.type === AST_NODE_TYPES.Identifier)
60
+ keys.push(property.key.name);
61
+ else if (property.key.type === AST_NODE_TYPES.Literal)
62
+ keys.push(String(property.key.value));
63
+ }
64
+ return keys;
65
+ }
66
+ /** A ViewModel (`*.logic.ts`) hook returns `{ state, derived?, effects }` or nothing. */
67
+ export const viewmodelReturnShape = createRule({
68
+ name: 'viewmodel-return-shape',
69
+ meta: {
70
+ type: 'problem',
71
+ docs: {
72
+ description: 'A ViewModel (*.logic.ts) hook must return an object literal whose keys are a non-empty subset of { state, derived, effects }, or return nothing.',
73
+ },
74
+ schema: [],
75
+ messages: {
76
+ notObject: "ViewModel hook '{{name}}' must return an object literal shaped as {{shape}} (or nothing). Found a non-object return.",
77
+ wrongShape: "ViewModel hook '{{name}}' must return only a non-empty subset of {{shape}}. Disallowed keys: {{bad}}.",
78
+ },
79
+ },
80
+ defaultOptions: [],
81
+ create(context) {
82
+ const filename = context.filename;
83
+ if (!filename.endsWith('.logic.ts') || isTestFile(filename))
84
+ return {};
85
+ function validateObject(reportNode, hookName, object) {
86
+ const keys = keysOf(object);
87
+ const bad = keys.filter(key => !ALLOWED.has(key));
88
+ // NOTE: a spread could inject any key and a computed key is not statically known, so a
89
+ // ViewModel return uses plain literal keys only.
90
+ const hasSpread = object.properties.some(p => p.type === AST_NODE_TYPES.SpreadElement);
91
+ const hasComputed = object.properties.some(p => p.type === AST_NODE_TYPES.Property && p.computed);
92
+ if (bad.length > 0 || hasSpread || hasComputed) {
93
+ const markers = [...bad];
94
+ if (hasSpread)
95
+ markers.push('(spread)');
96
+ if (hasComputed)
97
+ markers.push('(computed key)');
98
+ context.report({
99
+ node: reportNode,
100
+ messageId: 'wrongShape',
101
+ data: { name: hookName, shape: SHAPE, bad: markers.join(', ') },
102
+ });
103
+ }
104
+ else if (keys.length === 0) {
105
+ context.report({
106
+ node: reportNode,
107
+ messageId: 'wrongShape',
108
+ data: { name: hookName, shape: SHAPE, bad: '(empty object)' },
109
+ });
110
+ }
111
+ }
112
+ function checkHook(hookName, fn) {
113
+ const body = fn.body;
114
+ if (body.type !== AST_NODE_TYPES.BlockStatement) {
115
+ const value = unwrapTs(body);
116
+ if (value.type === AST_NODE_TYPES.ObjectExpression)
117
+ validateObject(value, hookName, value);
118
+ else
119
+ context.report({ node: fn, messageId: 'notObject', data: { name: hookName, shape: SHAPE } });
120
+ return;
121
+ }
122
+ for (const ret of collectOwnReturns(body)) {
123
+ if (!ret.argument)
124
+ continue;
125
+ const value = unwrapTs(ret.argument);
126
+ if (value.type !== AST_NODE_TYPES.ObjectExpression) {
127
+ context.report({ node: ret, messageId: 'notObject', data: { name: hookName, shape: SHAPE } });
128
+ }
129
+ else {
130
+ validateObject(ret, hookName, value);
131
+ }
132
+ }
133
+ }
134
+ return {
135
+ VariableDeclarator(node) {
136
+ if (node.id.type !== AST_NODE_TYPES.Identifier || !node.init || !isHookName(node.id.name))
137
+ return;
138
+ if (node.init.type === AST_NODE_TYPES.ArrowFunctionExpression ||
139
+ node.init.type === AST_NODE_TYPES.FunctionExpression) {
140
+ checkHook(node.id.name, node.init);
141
+ }
142
+ },
143
+ FunctionDeclaration(node) {
144
+ if (node.id && isHookName(node.id.name))
145
+ checkHook(node.id.name, node);
146
+ },
147
+ };
148
+ },
149
+ });
150
+ //# sourceMappingURL=viewmodelReturnShape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewmodelReturnShape.js","sourceRoot":"","sources":["../../src/rules/viewmodelReturnShape.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@timothyrusso/eslint-plugin-arch",
3
+ "version": "0.1.0",
4
+ "description": "ESLint rules for the agentic-kit architecture: ViewModel contract, comments, dashes, layout, stable row handlers, Effect in views.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/timothyrusso/agentic-kit.git",
10
+ "directory": "packages/eslint-plugin-arch"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.build.json",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings=ExperimentalWarning\" jest"
33
+ },
34
+ "dependencies": {
35
+ "@typescript-eslint/parser": "^8.70.1",
36
+ "@typescript-eslint/utils": "^8.70.1"
37
+ },
38
+ "devDependencies": {
39
+ "@typescript-eslint/rule-tester": "^8.70.1",
40
+ "eslint": "^10.11.0"
41
+ },
42
+ "peerDependencies": {
43
+ "eslint": "^9.15.0 || ^10.0.0",
44
+ "typescript": ">=4.8.4 <6.1.0"
45
+ }
46
+ }