@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21

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 (249) hide show
  1. package/CHANGELOG.md +497 -6
  2. package/README.md +77 -26
  3. package/bin/warden.ts +50 -0
  4. package/package.json +27 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +28 -0
  7. package/src/cli.ts +1374 -103
  8. package/src/command.ts +953 -0
  9. package/src/config.ts +184 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +106 -22
  12. package/src/fix.ts +120 -0
  13. package/src/formatters.ts +79 -9
  14. package/src/guide.ts +245 -0
  15. package/src/index.ts +206 -14
  16. package/src/project-context.ts +163 -0
  17. package/src/resolve.ts +530 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/ast.ts +3176 -85
  20. package/src/rules/circular-refs.ts +154 -0
  21. package/src/rules/composes-declarations.ts +704 -0
  22. package/src/rules/context-no-surface-types.ts +68 -8
  23. package/src/rules/contour-exists.ts +251 -0
  24. package/src/rules/contour-ids.ts +15 -0
  25. package/src/rules/dead-internal-trail.ts +154 -0
  26. package/src/rules/draft-file-marking.ts +160 -0
  27. package/src/rules/draft-visible-debt.ts +87 -0
  28. package/src/rules/error-mapping-completeness.ts +288 -0
  29. package/src/rules/example-valid.ts +401 -0
  30. package/src/rules/fires-declarations.ts +758 -0
  31. package/src/rules/implementation-returns-result.ts +1265 -95
  32. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  33. package/src/rules/incomplete-crud.ts +580 -0
  34. package/src/rules/index.ts +219 -18
  35. package/src/rules/intent-propagation.ts +127 -0
  36. package/src/rules/layer-field-name-drift.ts +96 -0
  37. package/src/rules/metadata.ts +654 -0
  38. package/src/rules/missing-reconcile.ts +98 -0
  39. package/src/rules/missing-visibility.ts +110 -0
  40. package/src/rules/no-destructured-compose.ts +192 -0
  41. package/src/rules/no-dev-permit-in-source.ts +99 -0
  42. package/src/rules/no-direct-implementation-call.ts +7 -7
  43. package/src/rules/no-legacy-layer-imports.ts +211 -0
  44. package/src/rules/no-native-error-result.ts +111 -0
  45. package/src/rules/no-redundant-result-error-wrap.ts +331 -0
  46. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  47. package/src/rules/no-sync-result-assumption.ts +1134 -99
  48. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  49. package/src/rules/no-throw-in-implementation.ts +10 -9
  50. package/src/rules/no-top-level-surface.ts +389 -0
  51. package/src/rules/on-references-exist.ts +194 -0
  52. package/src/rules/orphaned-signal.ts +150 -0
  53. package/src/rules/owner-projection-parity.ts +146 -0
  54. package/src/rules/permit-governance.ts +25 -0
  55. package/src/rules/public-export-example-coverage.ts +553 -0
  56. package/src/rules/public-internal-deep-imports.ts +517 -0
  57. package/src/rules/public-output-schema.ts +29 -0
  58. package/src/rules/public-union-output-discriminants.ts +150 -0
  59. package/src/rules/read-intent-fires.ts +187 -0
  60. package/src/rules/reference-exists.ts +98 -0
  61. package/src/rules/registry-names.ts +145 -0
  62. package/src/rules/resolved-import-boundary.ts +146 -0
  63. package/src/rules/resource-declarations.ts +704 -0
  64. package/src/rules/resource-exists.ts +179 -0
  65. package/src/rules/resource-id-grammar.ts +65 -0
  66. package/src/rules/resource-mock-coverage.ts +115 -0
  67. package/src/rules/scan.ts +38 -25
  68. package/src/rules/scheduled-destroy-intent.ts +44 -0
  69. package/src/rules/signal-graph-coaching.ts +191 -0
  70. package/src/rules/specs.ts +9 -5
  71. package/src/rules/static-resource-accessor-preference.ts +657 -0
  72. package/src/rules/surface-facet-coherence.ts +370 -0
  73. package/src/rules/trail-versioning-source.ts +1094 -0
  74. package/src/rules/trail-versioning-topo.ts +172 -0
  75. package/src/rules/types.ts +270 -6
  76. package/src/rules/unmaterialized-activation-source.ts +84 -0
  77. package/src/rules/unreachable-detour-shadowing.ts +344 -0
  78. package/src/rules/valid-describe-refs.ts +160 -32
  79. package/src/rules/valid-detour-contract.ts +78 -0
  80. package/src/rules/warden-export-symmetry.ts +533 -0
  81. package/src/rules/warden-rules-use-ast.ts +996 -0
  82. package/src/rules/webhook-route-collision.ts +243 -0
  83. package/src/trails/activation-orphan.trail.ts +84 -0
  84. package/src/trails/circular-refs.trail.ts +29 -0
  85. package/src/trails/composes-declarations.trail.ts +22 -0
  86. package/src/trails/context-no-surface-types.trail.ts +21 -0
  87. package/src/trails/contour-exists.trail.ts +21 -0
  88. package/src/trails/dead-internal-trail.trail.ts +26 -0
  89. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  90. package/src/trails/draft-file-marking.trail.ts +16 -0
  91. package/src/trails/draft-visible-debt.trail.ts +16 -0
  92. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  93. package/src/trails/example-valid.trail.ts +25 -0
  94. package/src/trails/fires-declarations.trail.ts +23 -0
  95. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  96. package/src/trails/implementation-returns-result.trail.ts +20 -0
  97. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  98. package/src/trails/incomplete-crud.trail.ts +39 -0
  99. package/src/trails/index.ts +78 -0
  100. package/src/trails/intent-propagation.trail.ts +30 -0
  101. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  102. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  103. package/src/trails/missing-reconcile.trail.ts +33 -0
  104. package/src/trails/missing-visibility.trail.ts +22 -0
  105. package/src/trails/no-destructured-compose.trail.ts +44 -0
  106. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  107. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  108. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  109. package/src/trails/no-native-error-result.trail.ts +18 -0
  110. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  111. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  112. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  113. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  114. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  115. package/src/trails/no-top-level-surface.trail.ts +43 -0
  116. package/src/trails/on-references-exist.trail.ts +21 -0
  117. package/src/trails/orphaned-signal.trail.ts +36 -0
  118. package/src/trails/owner-projection-parity.trail.ts +26 -0
  119. package/src/trails/pending-force.trail.ts +21 -0
  120. package/src/trails/permit-governance.trail.ts +51 -0
  121. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  122. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  123. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  124. package/src/trails/public-output-schema.trail.ts +55 -0
  125. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  126. package/src/trails/read-intent-fires.trail.ts +20 -0
  127. package/src/trails/reference-exists.trail.ts +25 -0
  128. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  129. package/src/trails/resource-declarations.trail.ts +25 -0
  130. package/src/trails/resource-exists.trail.ts +27 -0
  131. package/src/trails/resource-id-grammar.trail.ts +39 -0
  132. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  133. package/src/trails/run.ts +162 -0
  134. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  135. package/src/trails/schema.ts +194 -0
  136. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  137. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  138. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  139. package/src/trails/topo.ts +6 -0
  140. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  141. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  142. package/src/trails/valid-describe-refs.trail.ts +18 -0
  143. package/src/trails/valid-detour-contract.trail.ts +71 -0
  144. package/src/trails/version-gap.trail.ts +35 -0
  145. package/src/trails/version-pinned-compose.trail.ts +23 -0
  146. package/src/trails/version-without-examples.trail.ts +38 -0
  147. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  148. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  149. package/src/trails/webhook-route-collision.trail.ts +50 -0
  150. package/src/trails/wrap-rule.ts +213 -0
  151. package/src/workspaces.ts +238 -0
  152. package/.turbo/turbo-build.log +0 -1
  153. package/.turbo/turbo-lint.log +0 -3
  154. package/.turbo/turbo-typecheck.log +0 -1
  155. package/dist/cli.d.ts +0 -46
  156. package/dist/cli.d.ts.map +0 -1
  157. package/dist/cli.js +0 -221
  158. package/dist/cli.js.map +0 -1
  159. package/dist/drift.d.ts +0 -26
  160. package/dist/drift.d.ts.map +0 -1
  161. package/dist/drift.js +0 -27
  162. package/dist/drift.js.map +0 -1
  163. package/dist/formatters.d.ts +0 -29
  164. package/dist/formatters.d.ts.map +0 -1
  165. package/dist/formatters.js +0 -87
  166. package/dist/formatters.js.map +0 -1
  167. package/dist/index.d.ts +0 -26
  168. package/dist/index.d.ts.map +0 -1
  169. package/dist/index.js +0 -26
  170. package/dist/index.js.map +0 -1
  171. package/dist/rules/ast.d.ts +0 -41
  172. package/dist/rules/ast.d.ts.map +0 -1
  173. package/dist/rules/ast.js +0 -163
  174. package/dist/rules/ast.js.map +0 -1
  175. package/dist/rules/context-no-surface-types.d.ts +0 -12
  176. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  177. package/dist/rules/context-no-surface-types.js +0 -96
  178. package/dist/rules/context-no-surface-types.js.map +0 -1
  179. package/dist/rules/implementation-returns-result.d.ts +0 -13
  180. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  181. package/dist/rules/implementation-returns-result.js +0 -231
  182. package/dist/rules/implementation-returns-result.js.map +0 -1
  183. package/dist/rules/index.d.ts +0 -22
  184. package/dist/rules/index.d.ts.map +0 -1
  185. package/dist/rules/index.js +0 -41
  186. package/dist/rules/index.js.map +0 -1
  187. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  188. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  189. package/dist/rules/no-direct-impl-in-route.js +0 -46
  190. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  191. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  192. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  193. package/dist/rules/no-direct-implementation-call.js +0 -39
  194. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  195. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  196. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  197. package/dist/rules/no-sync-result-assumption.js +0 -98
  198. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  199. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  200. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  201. package/dist/rules/no-throw-in-detour-target.js +0 -87
  202. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  203. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  204. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  205. package/dist/rules/no-throw-in-implementation.js +0 -34
  206. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  207. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  208. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  209. package/dist/rules/prefer-schema-inference.js +0 -86
  210. package/dist/rules/prefer-schema-inference.js.map +0 -1
  211. package/dist/rules/scan.d.ts +0 -8
  212. package/dist/rules/scan.d.ts.map +0 -1
  213. package/dist/rules/scan.js +0 -32
  214. package/dist/rules/scan.js.map +0 -1
  215. package/dist/rules/specs.d.ts +0 -29
  216. package/dist/rules/specs.d.ts.map +0 -1
  217. package/dist/rules/specs.js +0 -192
  218. package/dist/rules/specs.js.map +0 -1
  219. package/dist/rules/structure.d.ts +0 -13
  220. package/dist/rules/structure.d.ts.map +0 -1
  221. package/dist/rules/structure.js +0 -142
  222. package/dist/rules/structure.js.map +0 -1
  223. package/dist/rules/types.d.ts +0 -52
  224. package/dist/rules/types.d.ts.map +0 -1
  225. package/dist/rules/types.js +0 -2
  226. package/dist/rules/types.js.map +0 -1
  227. package/dist/rules/valid-describe-refs.d.ts +0 -7
  228. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  229. package/dist/rules/valid-describe-refs.js +0 -51
  230. package/dist/rules/valid-describe-refs.js.map +0 -1
  231. package/dist/rules/valid-detour-refs.d.ts +0 -6
  232. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  233. package/dist/rules/valid-detour-refs.js +0 -116
  234. package/dist/rules/valid-detour-refs.js.map +0 -1
  235. package/src/__tests__/cli.test.ts +0 -198
  236. package/src/__tests__/drift.test.ts +0 -74
  237. package/src/__tests__/formatters.test.ts +0 -157
  238. package/src/__tests__/implementation-returns-result.test.ts +0 -75
  239. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  240. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  241. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  242. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  243. package/src/__tests__/rules.test.ts +0 -188
  244. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  245. package/src/rules/no-direct-impl-in-route.ts +0 -77
  246. package/src/rules/no-throw-in-detour-target.ts +0 -150
  247. package/src/rules/valid-detour-refs.ts +0 -187
  248. package/tsconfig.json +0 -9
  249. package/tsconfig.tsbuildinfo +0 -1
@@ -1,69 +1,49 @@
1
1
  /**
2
2
  * Finds implementations that return raw values instead of `Result`.
3
3
  *
4
- * Uses AST parsing to find `implementation:` bodies and check that
5
- * every return statement returns Result.ok(), Result.err(), ctx.follow(),
4
+ * Uses AST parsing to find `blaze:` bodies and check that
5
+ * every return statement returns Result.ok(), Result.err(), ctx.compose(),
6
6
  * or a tracked Result-typed variable.
7
7
  */
8
8
 
9
+ import { dirname, isAbsolute, resolve } from 'node:path';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import type { AstNode } from './ast.js';
9
12
  import {
10
- findImplementationBodies,
13
+ collectScopeFrameBindings,
14
+ findBlazeBodies,
11
15
  findTrailDefinitions,
16
+ getMemberExpression,
17
+ identifierName,
12
18
  offsetToLine,
13
19
  parse,
14
20
  walk,
21
+ walkWithScopes,
15
22
  } from './ast.js';
16
23
  import { isTestFile } from './scan.js';
17
24
  import type { WardenDiagnostic, WardenRule } from './types.js';
18
25
 
19
- // ---------------------------------------------------------------------------
20
- // Types
21
- // ---------------------------------------------------------------------------
22
-
23
- interface AstNode {
24
- readonly type: string;
25
- readonly start: number;
26
- readonly end: number;
27
- readonly [key: string]: unknown;
28
- }
26
+ const buildUnrecognizedResultMessage = (label: string, id: string): string =>
27
+ `${label} "${id}": return value is not a recognized Result expression. Return Result.ok(...), Result.err(...), or a Result-producing expression such as await ctx.compose(...). If you are returning a composed/helper Result, keep the provenance visible or add a Result return annotation Warden can trace.`;
29
28
 
30
29
  // ---------------------------------------------------------------------------
31
30
  // Member expression helpers
32
31
  // ---------------------------------------------------------------------------
33
32
 
34
- /** Extract object.property names from a MemberExpression callee. */
35
- const extractMemberNames = (
36
- callee: AstNode
37
- ): { objName: string | undefined; propName: string | undefined } => {
38
- const obj = (callee as unknown as { object?: AstNode }).object;
39
- const prop = (callee as unknown as { property?: AstNode }).property;
40
- const objName =
41
- obj?.type === 'Identifier'
42
- ? (obj as unknown as { name: string }).name
43
- : undefined;
44
- const propName =
45
- prop?.type === 'Identifier'
46
- ? (prop as unknown as { name: string }).name
47
- : undefined;
48
- return { objName, propName };
49
- };
50
-
51
- const isMemberExpression = (callee: AstNode): boolean =>
52
- callee.type === 'StaticMemberExpression' ||
53
- callee.type === 'MemberExpression';
54
-
55
33
  const isResultMemberCall = (callee: AstNode): boolean => {
56
- if (!isMemberExpression(callee)) {
34
+ const member = getMemberExpression(callee);
35
+ if (!member) {
57
36
  return false;
58
37
  }
59
- const { objName, propName } = extractMemberNames(callee);
38
+ const objName = identifierName(member.object) ?? undefined;
39
+ const propName = identifierName(member.property) ?? undefined;
60
40
  if (objName === 'Result' && (propName === 'ok' || propName === 'err')) {
61
41
  return true;
62
42
  }
63
- if (objName === 'ctx' && propName === 'follow') {
43
+ if (objName === 'ctx' && propName === 'compose') {
64
44
  return true;
65
45
  }
66
- return propName === 'implementation';
46
+ return propName === 'blaze';
67
47
  };
68
48
 
69
49
  // ---------------------------------------------------------------------------
@@ -71,7 +51,7 @@ const isResultMemberCall = (callee: AstNode): boolean => {
71
51
  // ---------------------------------------------------------------------------
72
52
 
73
53
  /** Check if an expression node is an allowed Result-returning expression. */
74
- const isResultExpression = (node: AstNode): boolean => {
54
+ export const isResultExpression = (node: AstNode): boolean => {
75
55
  if (node.type === 'CallExpression') {
76
56
  const callee = node['callee'] as AstNode | undefined;
77
57
  if (!callee) {
@@ -88,10 +68,67 @@ const isResultExpression = (node: AstNode): boolean => {
88
68
  return false;
89
69
  };
90
70
 
71
+ /** Map of namespace-import local name to the set of Result-helper names exported by the target module. */
72
+ export type NamespaceHelperMap = ReadonlyMap<string, ReadonlySet<string>>;
73
+
74
+ /** Map of lexical scope frames to local helper bindings with explicit Result return types. */
75
+ export type ScopedHelperMap = ReadonlyMap<
76
+ ReadonlySet<string>,
77
+ ReadonlySet<string>
78
+ >;
79
+
80
+ export type MutableScopedHelperMap = Map<ReadonlySet<string>, Set<string>>;
81
+
82
+ export const findNearestBindingScope = (
83
+ name: string,
84
+ scopes: readonly ReadonlySet<string>[]
85
+ ): ReadonlySet<string> | null =>
86
+ scopes.find((scope) => scope.has(name)) ?? null;
87
+
88
+ const isScopedHelperBinding = (
89
+ name: string,
90
+ scope: ReadonlySet<string>,
91
+ scopedHelpers: ScopedHelperMap
92
+ ): boolean => scopedHelpers.get(scope)?.has(name) ?? false;
93
+
94
+ /**
95
+ * Check whether a namespace-member call like `ns.helper(...)` resolves to a
96
+ * known Result helper.
97
+ *
98
+ * When a non-empty `scopes` stack is provided, the namespace binding must not
99
+ * be shadowed by a parameter or local declaration in any enclosing scope at
100
+ * the call site. Without this check, any local `ns` (e.g. a blaze parameter
101
+ * named `ns`, or `const ns = ...` inside the body) would be misread as the
102
+ * module-scope namespace import.
103
+ */
104
+ const isNamespaceHelperMemberCall = (
105
+ callee: AstNode,
106
+ namespaceHelpers: NamespaceHelperMap,
107
+ scopes: readonly ReadonlySet<string>[] = []
108
+ ): boolean => {
109
+ const member = getMemberExpression(callee);
110
+ if (!member) {
111
+ return false;
112
+ }
113
+ const objName = identifierName(member.object) ?? undefined;
114
+ const propName = identifierName(member.property) ?? undefined;
115
+ if (!(objName && propName)) {
116
+ return false;
117
+ }
118
+ // Nearest binding is a local, not the namespace import.
119
+ if (scopes.some((scope) => scope.has(objName))) {
120
+ return false;
121
+ }
122
+ return namespaceHelpers.get(objName)?.has(propName) ?? false;
123
+ };
124
+
91
125
  /** Check if a node is a call to a known Result-returning helper. */
92
- const isHelperCall = (
126
+ export const isHelperCall = (
93
127
  node: AstNode,
94
- helperNames: ReadonlySet<string>
128
+ helperNames: ReadonlySet<string>,
129
+ namespaceHelpers: NamespaceHelperMap = new Map(),
130
+ scopes: readonly ReadonlySet<string>[] = [],
131
+ scopedHelpers: ScopedHelperMap = new Map()
95
132
  ): boolean => {
96
133
  const target =
97
134
  node.type === 'AwaitExpression'
@@ -105,10 +142,19 @@ const isHelperCall = (
105
142
  const callee = target['callee'] as AstNode | undefined;
106
143
  if (callee?.type === 'Identifier') {
107
144
  const { name } = callee as unknown as { name: string };
145
+ const bindingScope = findNearestBindingScope(name, scopes);
146
+ if (
147
+ bindingScope &&
148
+ !isScopedHelperBinding(name, bindingScope, scopedHelpers)
149
+ ) {
150
+ return false;
151
+ }
108
152
  return helperNames.has(name);
109
153
  }
110
154
 
111
- return false;
155
+ return callee
156
+ ? isNamespaceHelperMemberCall(callee, namespaceHelpers, scopes)
157
+ : false;
112
158
  };
113
159
 
114
160
  /** Unwrap an optional AwaitExpression to get the inner identifier name. */
@@ -129,12 +175,17 @@ const resolveIdentifierName = (node: AstNode): string | null => {
129
175
  const isAllowedReturnArgument = (
130
176
  argument: AstNode,
131
177
  helperNames: ReadonlySet<string>,
132
- resultVars: ReadonlySet<string>
178
+ resultVars: ReadonlySet<string>,
179
+ namespaceHelpers: NamespaceHelperMap,
180
+ scopes: readonly ReadonlySet<string>[] = [],
181
+ scopedHelpers: ScopedHelperMap = new Map()
133
182
  ): boolean => {
134
183
  if (isResultExpression(argument)) {
135
184
  return true;
136
185
  }
137
- if (isHelperCall(argument, helperNames)) {
186
+ if (
187
+ isHelperCall(argument, helperNames, namespaceHelpers, scopes, scopedHelpers)
188
+ ) {
138
189
  return true;
139
190
  }
140
191
 
@@ -142,17 +193,143 @@ const isAllowedReturnArgument = (
142
193
  return varName !== null && resultVars.has(varName);
143
194
  };
144
195
 
196
+ // ---------------------------------------------------------------------------
197
+ // Result helper name collection
198
+ // ---------------------------------------------------------------------------
199
+
200
+ const getImportSourceValue = (node: AstNode): string | null => {
201
+ const sourceNode = (node as unknown as { source?: AstNode }).source;
202
+ const sourceValue = sourceNode
203
+ ? (sourceNode as unknown as { value?: unknown }).value
204
+ : undefined;
205
+ return typeof sourceValue === 'string' ? sourceValue : null;
206
+ };
207
+
208
+ const extractIdentifierName = (node: AstNode | undefined): string | null =>
209
+ node?.type === 'Identifier'
210
+ ? ((node as unknown as { name: string }).name ?? null)
211
+ : null;
212
+
213
+ const DEFAULT_RESULT_TYPE_NAMES = new Set(['Result']);
214
+
215
+ const escapeRegExp = (value: string): string =>
216
+ value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
217
+
218
+ const hasGenericTypeReference = (
219
+ annotationText: string,
220
+ typeName: string
221
+ ): boolean =>
222
+ new RegExp(`(^|[^\\w$])${escapeRegExp(typeName)}\\s*<`).test(annotationText);
223
+
224
+ export const collectResultTypeNames = (ast: AstNode): ReadonlySet<string> => {
225
+ const names = new Set(DEFAULT_RESULT_TYPE_NAMES);
226
+ walk(ast, (node) => {
227
+ if (
228
+ node.type !== 'ImportDeclaration' ||
229
+ getImportSourceValue(node) !== '@ontrails/core'
230
+ ) {
231
+ return;
232
+ }
233
+ const specifiers =
234
+ (node['specifiers'] as readonly AstNode[] | undefined) ?? [];
235
+ for (const specifier of specifiers) {
236
+ if (specifier.type !== 'ImportSpecifier') {
237
+ continue;
238
+ }
239
+ const { imported, local } = specifier as unknown as {
240
+ imported?: AstNode;
241
+ local?: AstNode;
242
+ };
243
+ if (extractIdentifierName(imported) !== 'Result') {
244
+ continue;
245
+ }
246
+ names.add(extractIdentifierName(local) ?? 'Result');
247
+ }
248
+ });
249
+ return names;
250
+ };
251
+
252
+ /** Check if a return type annotation mentions Result or an imported Result alias. */
253
+ const hasResultReturnType = (
254
+ node: AstNode,
255
+ sourceCode: string,
256
+ resultTypeNames: ReadonlySet<string> = DEFAULT_RESULT_TYPE_NAMES
257
+ ): boolean => {
258
+ const { returnType } = node as unknown as { returnType?: AstNode };
259
+ if (!returnType) {
260
+ return false;
261
+ }
262
+ const annotationText = sourceCode.slice(returnType.start, returnType.end);
263
+ for (const name of resultTypeNames) {
264
+ if (hasGenericTypeReference(annotationText, name)) {
265
+ return true;
266
+ }
267
+ }
268
+ return false;
269
+ };
270
+
271
+ const isFunctionLikeExpression = (node: AstNode): boolean =>
272
+ node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression';
273
+
274
+ const addScopedHelper = (
275
+ scopedHelpers: MutableScopedHelperMap,
276
+ scope: ReadonlySet<string>,
277
+ name: string
278
+ ): void => {
279
+ const existing = scopedHelpers.get(scope);
280
+ if (existing) {
281
+ existing.add(name);
282
+ return;
283
+ }
284
+ scopedHelpers.set(scope, new Set([name]));
285
+ };
286
+
287
+ /** Record `const helper = (): Result<...> => ...` declarations for the current lexical scope. */
288
+ export const trackScopedResultHelperDeclaration = (
289
+ node: AstNode,
290
+ scopes: readonly ReadonlySet<string>[],
291
+ sourceCode: string,
292
+ resultTypeNames: ReadonlySet<string>,
293
+ scopedHelpers: MutableScopedHelperMap
294
+ ): void => {
295
+ if (node.type !== 'VariableDeclarator') {
296
+ return;
297
+ }
298
+ const { id, init } = node as unknown as { id?: AstNode; init?: AstNode };
299
+ const name = extractIdentifierName(id);
300
+ if (!(name && init && isFunctionLikeExpression(init))) {
301
+ return;
302
+ }
303
+ if (!hasResultReturnType(init, sourceCode, resultTypeNames)) {
304
+ return;
305
+ }
306
+ const bindingScope = findNearestBindingScope(name, scopes);
307
+ if (bindingScope) {
308
+ addScopedHelper(scopedHelpers, bindingScope, name);
309
+ }
310
+ };
311
+
145
312
  // ---------------------------------------------------------------------------
146
313
  // Variable tracking
147
314
  // ---------------------------------------------------------------------------
148
315
 
149
316
  /** Track a VariableDeclarator, adding to resultVars if it produces a Result. */
150
- const trackResultVariable = (node: AstNode, resultVars: Set<string>): void => {
317
+ const trackResultVariable = (
318
+ node: AstNode,
319
+ resultVars: Set<string>,
320
+ helperNames: ReadonlySet<string>,
321
+ namespaceHelpers: NamespaceHelperMap,
322
+ scopes: readonly ReadonlySet<string>[],
323
+ scopedHelpers: ScopedHelperMap
324
+ ): void => {
151
325
  const { init } = node as unknown as { init?: AstNode };
152
326
  const { id } = node as unknown as { id?: AstNode };
153
327
  if (init && id?.type === 'Identifier') {
154
328
  const { name } = id as unknown as { name: string };
155
- if (isResultExpression(init)) {
329
+ if (
330
+ isResultExpression(init) ||
331
+ isHelperCall(init, helperNames, namespaceHelpers, scopes, scopedHelpers)
332
+ ) {
156
333
  resultVars.add(name);
157
334
  }
158
335
  }
@@ -169,62 +346,78 @@ const checkReturnStatements = (
169
346
  filePath: string,
170
347
  sourceCode: string,
171
348
  helperNames: ReadonlySet<string>,
172
- diagnostics: WardenDiagnostic[]
349
+ namespaceHelpers: NamespaceHelperMap,
350
+ resultTypeNames: ReadonlySet<string>,
351
+ diagnostics: WardenDiagnostic[],
352
+ implScope: ReadonlySet<string> = new Set<string>()
173
353
  ): void => {
174
354
  const resultVars = new Set<string>();
355
+ const scopedHelpers: MutableScopedHelperMap = new Map();
356
+ const initialScopes = implScope.size > 0 ? [implScope] : [];
175
357
 
176
- walk(blockBody, (node) => {
177
- if (node.type === 'VariableDeclarator') {
178
- trackResultVariable(node, resultVars);
179
- }
180
-
181
- if (node.type !== 'ReturnStatement') {
182
- return;
183
- }
184
-
185
- const { argument } = node as unknown as { argument?: AstNode };
186
- // Bare return — not a value return
187
- if (!argument) {
188
- return;
189
- }
358
+ walkWithScopes(
359
+ blockBody,
360
+ (node, currentScopes) => {
361
+ if (node.type === 'VariableDeclarator') {
362
+ trackScopedResultHelperDeclaration(
363
+ node,
364
+ currentScopes,
365
+ sourceCode,
366
+ resultTypeNames,
367
+ scopedHelpers
368
+ );
369
+ trackResultVariable(
370
+ node,
371
+ resultVars,
372
+ helperNames,
373
+ namespaceHelpers,
374
+ currentScopes,
375
+ scopedHelpers
376
+ );
377
+ }
190
378
 
191
- if (isAllowedReturnArgument(argument, helperNames, resultVars)) {
192
- return;
193
- }
379
+ if (node.type !== 'ReturnStatement') {
380
+ return;
381
+ }
194
382
 
195
- diagnostics.push({
196
- filePath,
197
- line: offsetToLine(sourceCode, node.start),
198
- message: `${trailInfo.label} "${trailInfo.id}" implementation must return Result.ok(...) or Result.err(...), not a raw value.`,
199
- rule: 'implementation-returns-result',
200
- severity: 'error',
201
- });
202
- });
203
- };
383
+ const { argument } = node as unknown as { argument?: AstNode };
384
+ // Bare return is not a value return.
385
+ if (!argument) {
386
+ return;
387
+ }
204
388
 
205
- // ---------------------------------------------------------------------------
206
- // Result helper name collection
207
- // ---------------------------------------------------------------------------
389
+ if (
390
+ isAllowedReturnArgument(
391
+ argument,
392
+ helperNames,
393
+ resultVars,
394
+ namespaceHelpers,
395
+ currentScopes,
396
+ scopedHelpers
397
+ )
398
+ ) {
399
+ return;
400
+ }
208
401
 
209
- /** Check if a return type annotation mentions Result. */
210
- const hasResultReturnType = (node: AstNode, sourceCode: string): boolean => {
211
- const { returnType } = node as unknown as { returnType?: AstNode };
212
- if (!returnType) {
213
- return false;
214
- }
215
- const annotationText = sourceCode.slice(returnType.start, returnType.end);
216
- return /\bResult\s*</.test(annotationText);
402
+ diagnostics.push({
403
+ filePath,
404
+ line: offsetToLine(sourceCode, node.start),
405
+ message: buildUnrecognizedResultMessage(trailInfo.label, trailInfo.id),
406
+ rule: 'implementation-returns-result',
407
+ severity: 'error',
408
+ });
409
+ },
410
+ { initialScopes, stopAtNestedFunctions: true }
411
+ );
217
412
  };
218
413
 
219
- const isFunctionLikeExpression = (node: AstNode): boolean =>
220
- node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression';
221
-
222
414
  /** Collect names of top-level functions/consts with explicit Result return types. */
223
415
  const collectResultHelperNames = (
224
416
  ast: AstNode,
225
417
  sourceCode: string
226
418
  ): ReadonlySet<string> => {
227
419
  const names = new Set<string>();
420
+ const resultTypeNames = collectResultTypeNames(ast);
228
421
 
229
422
  walk(ast, (node) => {
230
423
  if (node.type === 'VariableDeclarator') {
@@ -234,7 +427,7 @@ const collectResultHelperNames = (
234
427
  id?.type === 'Identifier' &&
235
428
  init &&
236
429
  isFunctionLikeExpression(init) &&
237
- hasResultReturnType(init, sourceCode)
430
+ hasResultReturnType(init, sourceCode, resultTypeNames)
238
431
  ) {
239
432
  names.add((id as unknown as { name: string }).name);
240
433
  }
@@ -242,7 +435,10 @@ const collectResultHelperNames = (
242
435
 
243
436
  if (node.type === 'FunctionDeclaration') {
244
437
  const { id } = node as unknown as { id?: AstNode };
245
- if (id?.type === 'Identifier' && hasResultReturnType(node, sourceCode)) {
438
+ if (
439
+ id?.type === 'Identifier' &&
440
+ hasResultReturnType(node, sourceCode, resultTypeNames)
441
+ ) {
246
442
  names.add((id as unknown as { name: string }).name);
247
443
  }
248
444
  }
@@ -251,6 +447,962 @@ const collectResultHelperNames = (
251
447
  return names;
252
448
  };
253
449
 
450
+ // ---------------------------------------------------------------------------
451
+ // Imported Result helper resolution
452
+ // ---------------------------------------------------------------------------
453
+
454
+ /**
455
+ * Per-target-file cache of exported Result-helper names keyed by the absolute
456
+ * target path. Saves re-parsing when multiple rule invocations resolve the
457
+ * same file during a single warden run.
458
+ *
459
+ * @remarks
460
+ * Long-running processes calling `implementationReturnsResult.check` after
461
+ * source files change (e.g. watch mode, editor language servers) should call
462
+ * `clearImplementationReturnsResultCache()` between runs to avoid returning
463
+ * stale helper-name sets. The cache is intentionally not auto-invalidated per
464
+ * invocation — that would defeat its purpose within a single warden run.
465
+ */
466
+ const targetFileResultExportCache = new Map<string, ReadonlySet<string>>();
467
+
468
+ /**
469
+ * Clear the module-level cache used by the `implementation-returns-result`
470
+ * rule to remember which exported names on a target file carry a `Result<...>`
471
+ * return annotation.
472
+ *
473
+ * Call this between runs in long-lived processes where the set of Trails
474
+ * source files may have changed on disk since the last check.
475
+ */
476
+ export const clearImplementationReturnsResultCache = (): void => {
477
+ targetFileResultExportCache.clear();
478
+ };
479
+
480
+ interface ImportBinding {
481
+ /** Local alias used in the importing file. */
482
+ readonly localName: string;
483
+ /** Original exported name from the target module. */
484
+ readonly importedName: string;
485
+ /** Raw import source specifier (e.g. './foo.js'). */
486
+ readonly source: string;
487
+ }
488
+
489
+ const buildDefaultImportBinding = (
490
+ specifier: AstNode,
491
+ source: string
492
+ ): ImportBinding | null => {
493
+ const { local } = specifier as unknown as { local?: AstNode };
494
+ const localName = extractIdentifierName(local);
495
+ if (!localName) {
496
+ return null;
497
+ }
498
+ return { importedName: 'default', localName, source };
499
+ };
500
+
501
+ const buildNamedImportBinding = (
502
+ specifier: AstNode,
503
+ source: string
504
+ ): ImportBinding | null => {
505
+ const { local, imported } = specifier as unknown as {
506
+ local?: AstNode;
507
+ imported?: AstNode;
508
+ };
509
+ const localName = extractIdentifierName(local);
510
+ const importedName = extractIdentifierName(imported) ?? localName;
511
+ if (!(localName && importedName)) {
512
+ return null;
513
+ }
514
+ return { importedName, localName, source };
515
+ };
516
+
517
+ /**
518
+ * @remarks
519
+ * `import foo from './bar.js'` is treated as a re-export of `default` so the
520
+ * target file's `export default` declaration is considered as a potential
521
+ * Result helper. `import * as ns from './bar.js'` is handled separately by
522
+ * `collectNamespaceHelperImports`, which maps the namespace binding to the
523
+ * target's exported Result-helper names so `ns.helper(...)` member calls are
524
+ * recognized.
525
+ */
526
+ const buildImportBinding = (
527
+ specifier: AstNode,
528
+ source: string
529
+ ): ImportBinding | null => {
530
+ if (specifier.type === 'ImportDefaultSpecifier') {
531
+ return buildDefaultImportBinding(specifier, source);
532
+ }
533
+ if (specifier.type === 'ImportSpecifier') {
534
+ return buildNamedImportBinding(specifier, source);
535
+ }
536
+ return null;
537
+ };
538
+
539
+ const collectBindingsFromImportDeclaration = (
540
+ node: AstNode
541
+ ): readonly ImportBinding[] => {
542
+ const source = getImportSourceValue(node);
543
+ if (!source) {
544
+ return [];
545
+ }
546
+ const specifiers =
547
+ (node['specifiers'] as readonly AstNode[] | undefined) ?? [];
548
+ return specifiers.flatMap((specifier) => {
549
+ const binding = buildImportBinding(specifier, source);
550
+ return binding ? [binding] : [];
551
+ });
552
+ };
553
+
554
+ /** Collect `import { foo as bar } from './...'` bindings keyed by local name. */
555
+ const collectResolvableImports = (ast: AstNode): readonly ImportBinding[] => {
556
+ const imports: ImportBinding[] = [];
557
+ walk(ast, (node) => {
558
+ if (node.type === 'ImportDeclaration') {
559
+ imports.push(...collectBindingsFromImportDeclaration(node));
560
+ }
561
+ });
562
+ return imports;
563
+ };
564
+
565
+ /**
566
+ * Resolve a relative import source specifier to an absolute on-disk file path,
567
+ * or null when the source is not a relative path we can resolve locally.
568
+ *
569
+ * Handles `.js` -> `.ts` rewriting (the convention in this repo), plain `.ts`
570
+ * imports, and extensionless paths.
571
+ */
572
+ const buildResolutionCandidates = (resolved: string): readonly string[] => {
573
+ if (resolved.endsWith('.ts') || resolved.endsWith('.tsx')) {
574
+ return [resolved];
575
+ }
576
+ if (resolved.endsWith('.js')) {
577
+ return [
578
+ resolved.replace(/\.js$/, '.ts'),
579
+ resolved.replace(/\.js$/, '.tsx'),
580
+ resolved,
581
+ ];
582
+ }
583
+ if (resolved.endsWith('.jsx')) {
584
+ return [resolved.replace(/\.jsx$/, '.tsx'), resolved];
585
+ }
586
+ return [`${resolved}.ts`, `${resolved}.tsx`];
587
+ };
588
+
589
+ const resolveRelativeImportPath = (
590
+ source: string,
591
+ fromFile: string
592
+ ): string | null => {
593
+ if (!(source.startsWith('./') || source.startsWith('../'))) {
594
+ return null;
595
+ }
596
+ const baseDir = isAbsolute(fromFile)
597
+ ? dirname(fromFile)
598
+ : dirname(resolve(fromFile));
599
+ const resolved = resolve(baseDir, source);
600
+ return (
601
+ buildResolutionCandidates(resolved).find((candidate) =>
602
+ existsSync(candidate)
603
+ ) ?? null
604
+ );
605
+ };
606
+
607
+ /** Extract the declaration wrapped by an ExportNamedDeclaration, if any. */
608
+ const getExportedDeclaration = (node: AstNode): AstNode | null => {
609
+ if (node.type !== 'ExportNamedDeclaration') {
610
+ return null;
611
+ }
612
+ const decl = (node as unknown as { declaration?: AstNode }).declaration;
613
+ return decl ?? null;
614
+ };
615
+
616
+ const addExportedVariableResultHelper = (
617
+ decl: AstNode,
618
+ source: string,
619
+ collected: Set<string>,
620
+ resultTypeNames: ReadonlySet<string>
621
+ ): void => {
622
+ const declarations =
623
+ (decl['declarations'] as readonly AstNode[] | undefined) ?? [];
624
+ for (const declarator of declarations) {
625
+ const { id, init } = declarator as unknown as {
626
+ id?: AstNode;
627
+ init?: AstNode;
628
+ };
629
+ const name = extractIdentifierName(id);
630
+ if (
631
+ name &&
632
+ init &&
633
+ isFunctionLikeExpression(init) &&
634
+ hasResultReturnType(init, source, resultTypeNames)
635
+ ) {
636
+ collected.add(name);
637
+ }
638
+ }
639
+ };
640
+
641
+ const addExportedFunctionResultHelper = (
642
+ decl: AstNode,
643
+ source: string,
644
+ collected: Set<string>,
645
+ resultTypeNames: ReadonlySet<string>
646
+ ): void => {
647
+ const name = extractIdentifierName((decl as unknown as { id?: AstNode }).id);
648
+ if (name && hasResultReturnType(decl, source, resultTypeNames)) {
649
+ collected.add(name);
650
+ }
651
+ };
652
+
653
+ // ---------------------------------------------------------------------------
654
+ // Same-file declaration index (for specifier re-exports without a source)
655
+ // ---------------------------------------------------------------------------
656
+
657
+ /**
658
+ * Index a file's top-level function-like declarations (both exported-inline
659
+ * and plain) by name to the declaration node, so we can look up the original
660
+ * binding referenced by a specifier re-export like `export { helper }`.
661
+ *
662
+ * Each entry carries the init/declaration node so the caller can check the
663
+ * return-type annotation without re-walking.
664
+ */
665
+ type DeclarationIndex = ReadonlyMap<string, AstNode>;
666
+
667
+ const indexVariableDeclarationInto = (
668
+ decl: AstNode,
669
+ index: Map<string, AstNode>
670
+ ): void => {
671
+ const declarators =
672
+ (decl['declarations'] as readonly AstNode[] | undefined) ?? [];
673
+ for (const declarator of declarators) {
674
+ const { id, init } = declarator as unknown as {
675
+ id?: AstNode;
676
+ init?: AstNode;
677
+ };
678
+ const name = extractIdentifierName(id);
679
+ if (name && init && isFunctionLikeExpression(init)) {
680
+ index.set(name, init);
681
+ }
682
+ }
683
+ };
684
+
685
+ const indexFunctionDeclarationInto = (
686
+ decl: AstNode,
687
+ index: Map<string, AstNode>
688
+ ): void => {
689
+ const name = extractIdentifierName((decl as unknown as { id?: AstNode }).id);
690
+ if (name) {
691
+ index.set(name, decl);
692
+ }
693
+ };
694
+
695
+ const indexDeclarationInto = (
696
+ decl: AstNode | null | undefined,
697
+ index: Map<string, AstNode>
698
+ ): void => {
699
+ if (!decl) {
700
+ return;
701
+ }
702
+ if (decl.type === 'VariableDeclaration') {
703
+ indexVariableDeclarationInto(decl, index);
704
+ } else if (decl.type === 'FunctionDeclaration') {
705
+ indexFunctionDeclarationInto(decl, index);
706
+ }
707
+ };
708
+
709
+ const indexBodyNodeInto = (
710
+ node: AstNode,
711
+ index: Map<string, AstNode>
712
+ ): void => {
713
+ if (node.type === 'ExportNamedDeclaration') {
714
+ indexDeclarationInto(getExportedDeclaration(node), index);
715
+ return;
716
+ }
717
+ indexDeclarationInto(node, index);
718
+ };
719
+
720
+ const indexLocalDeclarations = (ast: AstNode): DeclarationIndex => {
721
+ const index = new Map<string, AstNode>();
722
+ const program = ast as unknown as { body?: readonly AstNode[] };
723
+ const bodyNodes = program.body ?? [];
724
+ for (const node of bodyNodes) {
725
+ indexBodyNodeInto(node, index);
726
+ }
727
+ return index;
728
+ };
729
+
730
+ // ---------------------------------------------------------------------------
731
+ // Export-specifier handling
732
+ // ---------------------------------------------------------------------------
733
+
734
+ interface ExportSpecifierInfo {
735
+ /** Name this export is exposed as to consumers (after `as` alias). */
736
+ readonly exportedName: string;
737
+ /** Name referenced inside the re-export (`helper` in `export { helper }`). */
738
+ readonly localName: string;
739
+ /** True when the specifier is `default` (i.e. `export { default as X }`). */
740
+ readonly isDefault: boolean;
741
+ }
742
+
743
+ const getSpecifierNameNode = (
744
+ spec: AstNode,
745
+ key: 'exported' | 'local'
746
+ ): string | null => {
747
+ const node = (spec as unknown as Record<string, AstNode | undefined>)[key];
748
+ if (!node) {
749
+ return null;
750
+ }
751
+ if (node.type === 'Identifier') {
752
+ return (node as unknown as { name?: string }).name ?? null;
753
+ }
754
+ // Support string-literal specifiers (`export { "default" as X }`, etc).
755
+ const { value } = node as unknown as { value?: unknown };
756
+ return typeof value === 'string' ? value : null;
757
+ };
758
+
759
+ const buildExportSpecifierInfo = (
760
+ spec: AstNode
761
+ ): ExportSpecifierInfo | null => {
762
+ if (spec.type !== 'ExportSpecifier') {
763
+ return null;
764
+ }
765
+ const localName = getSpecifierNameNode(spec, 'local');
766
+ const exportedName = getSpecifierNameNode(spec, 'exported') ?? localName;
767
+ if (!(localName && exportedName)) {
768
+ return null;
769
+ }
770
+ return {
771
+ exportedName,
772
+ isDefault: localName === 'default',
773
+ localName,
774
+ };
775
+ };
776
+
777
+ const getExportDefaultDeclaration = (ast: AstNode): AstNode | null => {
778
+ const program = ast as unknown as { body?: readonly AstNode[] };
779
+ const bodyNodes = program.body ?? [];
780
+ for (const node of bodyNodes) {
781
+ if (node.type === 'ExportDefaultDeclaration') {
782
+ const decl = (node as unknown as { declaration?: AstNode }).declaration;
783
+ return decl ?? null;
784
+ }
785
+ }
786
+ return null;
787
+ };
788
+
789
+ // Bounded recursion: one transitive hop through `export { ... } from`.
790
+ const MAX_RERESOLVE_DEPTH = 1;
791
+
792
+ /** Check whether a local declaration node has a `Result<...>` return annotation. */
793
+ const isResultHelperDeclaration = (
794
+ declarationNode: AstNode | undefined,
795
+ source: string,
796
+ resultTypeNames: ReadonlySet<string>
797
+ ): boolean => {
798
+ if (!declarationNode) {
799
+ return false;
800
+ }
801
+ if (isFunctionLikeExpression(declarationNode)) {
802
+ return hasResultReturnType(declarationNode, source, resultTypeNames);
803
+ }
804
+ if (declarationNode.type === 'FunctionDeclaration') {
805
+ return hasResultReturnType(declarationNode, source, resultTypeNames);
806
+ }
807
+ return false;
808
+ };
809
+
810
+ /** Resolve an `export default ...` declaration, following one identifier hop. */
811
+ const checkDefaultDeclarationIsResultHelper = (
812
+ defaultDecl: AstNode,
813
+ targetSource: string,
814
+ targetLocalDeclarations: DeclarationIndex,
815
+ resultTypeNames: ReadonlySet<string>
816
+ ): boolean => {
817
+ if (isResultHelperDeclaration(defaultDecl, targetSource, resultTypeNames)) {
818
+ return true;
819
+ }
820
+ if (defaultDecl.type === 'Identifier') {
821
+ const name = extractIdentifierName(defaultDecl);
822
+ const referenced = name ? targetLocalDeclarations.get(name) : undefined;
823
+ return isResultHelperDeclaration(referenced, targetSource, resultTypeNames);
824
+ }
825
+ return false;
826
+ };
827
+
828
+ interface LoadedTargetFile {
829
+ readonly ast: AstNode;
830
+ readonly source: string;
831
+ readonly localDeclarations: DeclarationIndex;
832
+ readonly resultTypeNames: ReadonlySet<string>;
833
+ }
834
+
835
+ const loadTargetFile = (targetPath: string): LoadedTargetFile | null => {
836
+ try {
837
+ const source = readFileSync(targetPath, 'utf8');
838
+ const ast = parse(targetPath, source) as AstNode | null;
839
+ if (!ast) {
840
+ return null;
841
+ }
842
+ return {
843
+ ast,
844
+ localDeclarations: indexLocalDeclarations(ast),
845
+ resultTypeNames: collectResultTypeNames(ast),
846
+ source,
847
+ };
848
+ } catch {
849
+ return null;
850
+ }
851
+ };
852
+
853
+ interface ReExportContext {
854
+ readonly loadedTarget: LoadedTargetFile | null;
855
+ readonly downstreamResultNames: ReadonlySet<string>;
856
+ }
857
+
858
+ const applyDefaultSpecifier = (
859
+ info: ExportSpecifierInfo,
860
+ loadedTarget: LoadedTargetFile | null,
861
+ collected: Set<string>
862
+ ): void => {
863
+ if (!loadedTarget) {
864
+ return;
865
+ }
866
+ const defaultDecl = getExportDefaultDeclaration(loadedTarget.ast);
867
+ if (!defaultDecl) {
868
+ return;
869
+ }
870
+ if (
871
+ checkDefaultDeclarationIsResultHelper(
872
+ defaultDecl,
873
+ loadedTarget.source,
874
+ loadedTarget.localDeclarations,
875
+ loadedTarget.resultTypeNames
876
+ )
877
+ ) {
878
+ collected.add(info.exportedName);
879
+ }
880
+ };
881
+
882
+ const applySpecifierInfo = (
883
+ info: ExportSpecifierInfo,
884
+ ctx: ReExportContext,
885
+ collected: Set<string>
886
+ ): void => {
887
+ if (info.isDefault) {
888
+ applyDefaultSpecifier(info, ctx.loadedTarget, collected);
889
+ return;
890
+ }
891
+ if (ctx.downstreamResultNames.has(info.localName)) {
892
+ collected.add(info.exportedName);
893
+ }
894
+ };
895
+
896
+ const resolveReExportTargetPath = (
897
+ node: AstNode,
898
+ targetPath: string,
899
+ visited: ReadonlySet<string>,
900
+ depth: number
901
+ ): string | null => {
902
+ if (depth >= MAX_RERESOLVE_DEPTH) {
903
+ return null;
904
+ }
905
+ const reSource = getImportSourceValue(node);
906
+ if (!reSource) {
907
+ return null;
908
+ }
909
+ const reTargetPath = resolveRelativeImportPath(reSource, targetPath);
910
+ if (!reTargetPath || visited.has(reTargetPath)) {
911
+ return null;
912
+ }
913
+ return reTargetPath;
914
+ };
915
+
916
+ const buildReExportContext = (
917
+ reTargetPath: string,
918
+ specifierInfos: readonly ExportSpecifierInfo[],
919
+ targetPath: string,
920
+ visited: ReadonlySet<string>,
921
+ depth: number
922
+ ): ReExportContext => {
923
+ const needsDefault = specifierInfos.some((info) => info.isDefault);
924
+ // Load once when the default specifier branch needs the target AST; the
925
+ // same loaded object is threaded into the downstream walk so it isn't
926
+ // read and parsed a second time within this check() call.
927
+ const loadedTarget = needsDefault ? loadTargetFile(reTargetPath) : null;
928
+ // eslint-disable-next-line no-use-before-define
929
+ const downstreamResultNames = collectTargetExportedResultHelperNames(
930
+ reTargetPath,
931
+ visited,
932
+ targetPath,
933
+ depth + 1,
934
+ loadedTarget
935
+ );
936
+ return {
937
+ downstreamResultNames,
938
+ loadedTarget,
939
+ };
940
+ };
941
+
942
+ /**
943
+ * Resolve a re-export with source (`export { ... } from './x.js'`) by pulling
944
+ * the matching names off the target file, honoring aliases and `default`.
945
+ */
946
+ const resolveReExportWithSource = (
947
+ node: AstNode,
948
+ specifiers: readonly AstNode[],
949
+ targetPath: string,
950
+ visited: ReadonlySet<string>,
951
+ depth: number,
952
+ collected: Set<string>
953
+ ): void => {
954
+ const reTargetPath = resolveReExportTargetPath(
955
+ node,
956
+ targetPath,
957
+ visited,
958
+ depth
959
+ );
960
+ if (!reTargetPath) {
961
+ return;
962
+ }
963
+ const specifierInfos = specifiers.flatMap((spec) => {
964
+ const info = buildExportSpecifierInfo(spec);
965
+ return info ? [info] : [];
966
+ });
967
+ const ctx = buildReExportContext(
968
+ reTargetPath,
969
+ specifierInfos,
970
+ targetPath,
971
+ visited,
972
+ depth
973
+ );
974
+ for (const info of specifierInfos) {
975
+ applySpecifierInfo(info, ctx, collected);
976
+ }
977
+ };
978
+
979
+ /** Resolve a specifier-only re-export (`export { helper };`) against same-file declarations. */
980
+ const resolveReExportWithoutSource = (
981
+ specifiers: readonly AstNode[],
982
+ localDeclarations: DeclarationIndex,
983
+ source: string,
984
+ collected: Set<string>,
985
+ resultTypeNames: ReadonlySet<string>
986
+ ): void => {
987
+ for (const spec of specifiers) {
988
+ const info = buildExportSpecifierInfo(spec);
989
+ if (!info || info.isDefault) {
990
+ continue;
991
+ }
992
+ if (
993
+ isResultHelperDeclaration(
994
+ localDeclarations.get(info.localName),
995
+ source,
996
+ resultTypeNames
997
+ )
998
+ ) {
999
+ collected.add(info.exportedName);
1000
+ }
1001
+ }
1002
+ };
1003
+
1004
+ const processInlineExportedDeclaration = (
1005
+ exportedDecl: AstNode,
1006
+ source: string,
1007
+ collected: Set<string>,
1008
+ resultTypeNames: ReadonlySet<string>
1009
+ ): boolean => {
1010
+ if (exportedDecl.type === 'VariableDeclaration') {
1011
+ addExportedVariableResultHelper(
1012
+ exportedDecl,
1013
+ source,
1014
+ collected,
1015
+ resultTypeNames
1016
+ );
1017
+ return true;
1018
+ }
1019
+ if (exportedDecl.type === 'FunctionDeclaration') {
1020
+ addExportedFunctionResultHelper(
1021
+ exportedDecl,
1022
+ source,
1023
+ collected,
1024
+ resultTypeNames
1025
+ );
1026
+ return true;
1027
+ }
1028
+ return false;
1029
+ };
1030
+
1031
+ const processExportNamedDeclaration = (
1032
+ node: AstNode,
1033
+ source: string,
1034
+ targetPath: string,
1035
+ visited: ReadonlySet<string>,
1036
+ depth: number,
1037
+ localDeclarations: DeclarationIndex,
1038
+ collected: Set<string>,
1039
+ resultTypeNames: ReadonlySet<string>
1040
+ ): void => {
1041
+ const exportedDecl = getExportedDeclaration(node);
1042
+ if (
1043
+ exportedDecl &&
1044
+ processInlineExportedDeclaration(
1045
+ exportedDecl,
1046
+ source,
1047
+ collected,
1048
+ resultTypeNames
1049
+ )
1050
+ ) {
1051
+ return;
1052
+ }
1053
+ const specifiers =
1054
+ (node['specifiers'] as readonly AstNode[] | undefined) ?? [];
1055
+ if (specifiers.length === 0) {
1056
+ return;
1057
+ }
1058
+ if (getImportSourceValue(node)) {
1059
+ resolveReExportWithSource(
1060
+ node,
1061
+ specifiers,
1062
+ targetPath,
1063
+ visited,
1064
+ depth,
1065
+ collected
1066
+ );
1067
+ return;
1068
+ }
1069
+ resolveReExportWithoutSource(
1070
+ specifiers,
1071
+ localDeclarations,
1072
+ source,
1073
+ collected,
1074
+ resultTypeNames
1075
+ );
1076
+ };
1077
+
1078
+ const processExportDefaultDeclaration = (
1079
+ node: AstNode,
1080
+ source: string,
1081
+ localDeclarations: DeclarationIndex,
1082
+ collected: Set<string>,
1083
+ resultTypeNames: ReadonlySet<string>
1084
+ ): void => {
1085
+ const defaultDecl = (node as unknown as { declaration?: AstNode })
1086
+ .declaration;
1087
+ if (!defaultDecl) {
1088
+ return;
1089
+ }
1090
+ if (
1091
+ checkDefaultDeclarationIsResultHelper(
1092
+ defaultDecl,
1093
+ source,
1094
+ localDeclarations,
1095
+ resultTypeNames
1096
+ )
1097
+ ) {
1098
+ collected.add('default');
1099
+ }
1100
+ };
1101
+
1102
+ const collectExportedResultHelpersFromAst = (
1103
+ ast: AstNode,
1104
+ source: string,
1105
+ targetPath: string,
1106
+ visited: ReadonlySet<string>,
1107
+ depth: number,
1108
+ preloadedLocalDeclarations: DeclarationIndex | null = null,
1109
+ preloadedResultTypeNames: ReadonlySet<string> | null = null
1110
+ ): ReadonlySet<string> => {
1111
+ const collected = new Set<string>();
1112
+ // Reuse preloaded indexes from `loadTargetFile` when available to avoid
1113
+ // re-walking the same AST.
1114
+ const localDeclarations =
1115
+ preloadedLocalDeclarations ?? indexLocalDeclarations(ast);
1116
+ const resultTypeNames =
1117
+ preloadedResultTypeNames ?? collectResultTypeNames(ast);
1118
+ const program = ast as unknown as { body?: readonly AstNode[] };
1119
+ const bodyNodes = program.body ?? [];
1120
+
1121
+ for (const node of bodyNodes) {
1122
+ if (node.type === 'ExportNamedDeclaration') {
1123
+ processExportNamedDeclaration(
1124
+ node,
1125
+ source,
1126
+ targetPath,
1127
+ visited,
1128
+ depth,
1129
+ localDeclarations,
1130
+ collected,
1131
+ resultTypeNames
1132
+ );
1133
+ } else if (node.type === 'ExportDefaultDeclaration') {
1134
+ processExportDefaultDeclaration(
1135
+ node,
1136
+ source,
1137
+ localDeclarations,
1138
+ collected,
1139
+ resultTypeNames
1140
+ );
1141
+ } else if (node.type === 'ExportAllDeclaration') {
1142
+ // eslint-disable-next-line no-use-before-define
1143
+ processExportAllDeclaration(node, targetPath, visited, depth, collected);
1144
+ }
1145
+ }
1146
+
1147
+ return collected;
1148
+ };
1149
+
1150
+ /**
1151
+ * Handle `export * from './x.js'` by recursing into the target module and
1152
+ * unioning its exported Result-helper names. Type-only re-exports
1153
+ * (`export type * from '...'`) contribute nothing. Bounded by
1154
+ * `MAX_RERESOLVE_DEPTH` and the visited-set cycle guard shared with the
1155
+ * specifier re-export path.
1156
+ */
1157
+ const processExportAllDeclaration = (
1158
+ node: AstNode,
1159
+ targetPath: string,
1160
+ visited: ReadonlySet<string>,
1161
+ depth: number,
1162
+ collected: Set<string>
1163
+ ): void => {
1164
+ const { exportKind } = node as unknown as { exportKind?: string };
1165
+ if (exportKind === 'type') {
1166
+ return;
1167
+ }
1168
+ const reTargetPath = resolveReExportTargetPath(
1169
+ node,
1170
+ targetPath,
1171
+ visited,
1172
+ depth
1173
+ );
1174
+ if (!reTargetPath) {
1175
+ return;
1176
+ }
1177
+ // eslint-disable-next-line no-use-before-define
1178
+ const downstream = collectTargetExportedResultHelperNames(
1179
+ reTargetPath,
1180
+ visited,
1181
+ targetPath,
1182
+ depth + 1
1183
+ );
1184
+ // `export * from` does NOT re-export the default binding, so we union
1185
+ // only the named Result helpers from the downstream module.
1186
+ for (const name of downstream) {
1187
+ if (name !== 'default') {
1188
+ collected.add(name);
1189
+ }
1190
+ }
1191
+ };
1192
+
1193
+ const parseTargetResultHelperNames = (
1194
+ targetPath: string,
1195
+ visited: ReadonlySet<string>,
1196
+ depth: number,
1197
+ preloaded: LoadedTargetFile | null = null
1198
+ ): ReadonlySet<string> => {
1199
+ const loaded = preloaded ?? loadTargetFile(targetPath);
1200
+ if (!loaded) {
1201
+ return new Set<string>();
1202
+ }
1203
+ return collectExportedResultHelpersFromAst(
1204
+ loaded.ast,
1205
+ loaded.source,
1206
+ targetPath,
1207
+ visited,
1208
+ depth,
1209
+ loaded.localDeclarations,
1210
+ loaded.resultTypeNames
1211
+ );
1212
+ };
1213
+
1214
+ const buildVisitedPathSet = (
1215
+ parentVisited: ReadonlySet<string>,
1216
+ targetPath: string,
1217
+ parentPath: string | undefined
1218
+ ): ReadonlySet<string> => {
1219
+ const seeds = [...parentVisited, targetPath];
1220
+ if (parentPath) {
1221
+ seeds.push(parentPath);
1222
+ }
1223
+ return new Set<string>(seeds);
1224
+ };
1225
+
1226
+ /**
1227
+ * Collect the set of exported names from a target file whose declaration has
1228
+ * an explicit `Result<...>` / `Promise<Result<...>>` return annotation.
1229
+ *
1230
+ * Uses a visited-set on the recursion path to guard against `export { ... }
1231
+ * from` import cycles between files. Depth is capped at a single transitive
1232
+ * hop (see `MAX_RERESOLVE_DEPTH`) — deeper chains silently fall back.
1233
+ */
1234
+ // Only the direct-import path (no parents visited) is safe to cache: the
1235
+ // computed set is a function of (targetPath, parentVisited), and
1236
+ // cycle-truncated results from transitive walks must not bleed into later
1237
+ // direct lookups. See PR #204 review.
1238
+ const readCachedResultExports = (
1239
+ targetPath: string,
1240
+ parentVisited: ReadonlySet<string>
1241
+ ): ReadonlySet<string> | undefined => {
1242
+ if (parentVisited.size !== 0) {
1243
+ return;
1244
+ }
1245
+ return targetFileResultExportCache.get(targetPath);
1246
+ };
1247
+
1248
+ // biome-ignore lint/style/useConst: declared as a function so hoisting lets `buildReExportContext` (a const declared earlier) reference it before its textual definition
1249
+ // eslint-disable-next-line func-style, no-use-before-define
1250
+ function collectTargetExportedResultHelperNames(
1251
+ targetPath: string,
1252
+ parentVisited: ReadonlySet<string> = new Set<string>(),
1253
+ parentPath?: string,
1254
+ depth = 0,
1255
+ preloaded: LoadedTargetFile | null = null
1256
+ ): ReadonlySet<string> {
1257
+ if (parentVisited.has(targetPath)) {
1258
+ return new Set<string>();
1259
+ }
1260
+ const cached = readCachedResultExports(targetPath, parentVisited);
1261
+ if (cached) {
1262
+ return cached;
1263
+ }
1264
+ const visited = buildVisitedPathSet(parentVisited, targetPath, parentPath);
1265
+ const names = parseTargetResultHelperNames(
1266
+ targetPath,
1267
+ visited,
1268
+ depth,
1269
+ preloaded
1270
+ );
1271
+ if (parentVisited.size === 0) {
1272
+ targetFileResultExportCache.set(targetPath, names);
1273
+ }
1274
+ return names;
1275
+ }
1276
+
1277
+ /**
1278
+ * Extend a local-helper-name set with Result-returning helpers imported from
1279
+ * relative modules. Falls back silently on any resolution/parse failure.
1280
+ */
1281
+ const collectImportedResultHelperNames = (
1282
+ ast: AstNode,
1283
+ filePath: string
1284
+ ): ReadonlySet<string> => {
1285
+ const names = new Set<string>();
1286
+
1287
+ for (const binding of collectResolvableImports(ast)) {
1288
+ const targetPath = resolveRelativeImportPath(binding.source, filePath);
1289
+ if (!targetPath) {
1290
+ continue;
1291
+ }
1292
+ const exportedResultNames =
1293
+ collectTargetExportedResultHelperNames(targetPath);
1294
+ if (exportedResultNames.has(binding.importedName)) {
1295
+ names.add(binding.localName);
1296
+ }
1297
+ }
1298
+
1299
+ return names;
1300
+ };
1301
+
1302
+ interface NamespaceEntry {
1303
+ readonly localName: string;
1304
+ readonly names: ReadonlySet<string>;
1305
+ }
1306
+
1307
+ /** Extract a namespace specifier's local name if it is a namespace import. */
1308
+ const getNamespaceLocalName = (spec: AstNode): string | null => {
1309
+ if (spec.type !== 'ImportNamespaceSpecifier') {
1310
+ return null;
1311
+ }
1312
+ const { local } = spec as unknown as { local?: AstNode };
1313
+ return extractIdentifierName(local);
1314
+ };
1315
+
1316
+ /**
1317
+ * Resolve a single namespace specifier to (localName, resultHelperNames), or
1318
+ * null when the specifier is not a resolvable namespace import.
1319
+ *
1320
+ * We intentionally record the namespace even when the target file exports no
1321
+ * Result helpers (empty set). `isNamespaceHelperMemberCall` can then identify
1322
+ * `ns.anything()` as a namespace member call against a non-Result-helper
1323
+ * target — which correctly falls through to the general return-value
1324
+ * diagnostic path. Dropping the entry would misclassify the call as a
1325
+ * *non-namespace* member call and skip the namespace-shadowing scope check.
1326
+ */
1327
+ const resolveNamespaceSpecifier = (
1328
+ spec: AstNode,
1329
+ source: string,
1330
+ filePath: string
1331
+ ): NamespaceEntry | null => {
1332
+ const localName = getNamespaceLocalName(spec);
1333
+ if (!localName) {
1334
+ return null;
1335
+ }
1336
+ const targetPath = resolveRelativeImportPath(source, filePath);
1337
+ if (!targetPath) {
1338
+ return null;
1339
+ }
1340
+ const names = collectTargetExportedResultHelperNames(targetPath);
1341
+ return { localName, names };
1342
+ };
1343
+
1344
+ /** Extract namespace helper entries from a single ImportDeclaration node. */
1345
+ const namespaceEntriesFromImport = (
1346
+ node: AstNode,
1347
+ filePath: string
1348
+ ): readonly NamespaceEntry[] => {
1349
+ const source = getImportSourceValue(node);
1350
+ if (!source) {
1351
+ return [];
1352
+ }
1353
+ const specifiers =
1354
+ (node['specifiers'] as readonly AstNode[] | undefined) ?? [];
1355
+ return specifiers.flatMap((spec) => {
1356
+ const entry = resolveNamespaceSpecifier(spec, source, filePath);
1357
+ return entry ? [entry] : [];
1358
+ });
1359
+ };
1360
+
1361
+ /**
1362
+ * Collect `import * as ns from './foo.js'` bindings and map each local
1363
+ * namespace name to the set of Result-returning helper names exported by the
1364
+ * resolved target module. Returns an empty map if no namespace imports are
1365
+ * found or none resolve to local files.
1366
+ */
1367
+ export const collectNamespaceHelperImports = (
1368
+ ast: AstNode,
1369
+ filePath: string
1370
+ ): NamespaceHelperMap => {
1371
+ const map = new Map<string, ReadonlySet<string>>();
1372
+ walk(ast, (node) => {
1373
+ if (node.type !== 'ImportDeclaration') {
1374
+ return;
1375
+ }
1376
+ for (const { localName, names } of namespaceEntriesFromImport(
1377
+ node,
1378
+ filePath
1379
+ )) {
1380
+ map.set(localName, names);
1381
+ }
1382
+ });
1383
+ return map;
1384
+ };
1385
+
1386
+ /**
1387
+ * Combine same-file helper names with helpers imported from relative modules.
1388
+ */
1389
+ export const collectAllResultHelperNames = (
1390
+ ast: AstNode,
1391
+ sourceCode: string,
1392
+ filePath: string
1393
+ ): ReadonlySet<string> => {
1394
+ const local = collectResultHelperNames(ast, sourceCode);
1395
+ const imported = collectImportedResultHelperNames(ast, filePath);
1396
+ if (imported.size === 0) {
1397
+ return local;
1398
+ }
1399
+ const merged = new Set<string>(local);
1400
+ for (const name of imported) {
1401
+ merged.add(name);
1402
+ }
1403
+ return merged;
1404
+ };
1405
+
254
1406
  // ---------------------------------------------------------------------------
255
1407
  // Per-implementation checking
256
1408
  // ---------------------------------------------------------------------------
@@ -261,6 +1413,8 @@ const checkImplementation = (
261
1413
  filePath: string,
262
1414
  sourceCode: string,
263
1415
  helperNames: ReadonlySet<string>,
1416
+ namespaceHelpers: NamespaceHelperMap,
1417
+ resultTypeNames: ReadonlySet<string>,
264
1418
  diagnostics: WardenDiagnostic[]
265
1419
  ): void => {
266
1420
  const fnBody = (implValue as unknown as { body?: AstNode }).body;
@@ -268,6 +1422,10 @@ const checkImplementation = (
268
1422
  return;
269
1423
  }
270
1424
 
1425
+ // Seed analysis with the implementation's own bindings so parameter names
1426
+ // and hoisted vars shadow namespace imports in both block and concise bodies.
1427
+ const implScope = collectScopeFrameBindings(implValue);
1428
+
271
1429
  if (fnBody.type === 'BlockStatement' || fnBody.type === 'FunctionBody') {
272
1430
  checkReturnStatements(
273
1431
  fnBody,
@@ -275,16 +1433,24 @@ const checkImplementation = (
275
1433
  filePath,
276
1434
  sourceCode,
277
1435
  helperNames,
278
- diagnostics
1436
+ namespaceHelpers,
1437
+ resultTypeNames,
1438
+ diagnostics,
1439
+ implScope
279
1440
  );
280
1441
  return;
281
1442
  }
282
1443
 
283
- if (!isResultExpression(fnBody) && !isHelperCall(fnBody, helperNames)) {
1444
+ const conciseScopes: readonly ReadonlySet<string>[] =
1445
+ implScope.size > 0 ? [implScope] : [];
1446
+ if (
1447
+ !isResultExpression(fnBody) &&
1448
+ !isHelperCall(fnBody, helperNames, namespaceHelpers, conciseScopes)
1449
+ ) {
284
1450
  diagnostics.push({
285
1451
  filePath,
286
1452
  line: offsetToLine(sourceCode, implValue.start),
287
- message: `${info.label} "${info.id}" implementation must return Result.ok(...) or Result.err(...), not a raw value.`,
1453
+ message: buildUnrecognizedResultMessage(info.label, info.id),
288
1454
  rule: 'implementation-returns-result',
289
1455
  severity: 'error',
290
1456
  });
@@ -301,17 +1467,21 @@ const checkAllDefinitions = (
301
1467
  sourceCode: string
302
1468
  ): WardenDiagnostic[] => {
303
1469
  const diagnostics: WardenDiagnostic[] = [];
304
- const helperNames = collectResultHelperNames(ast, sourceCode);
1470
+ const helperNames = collectAllResultHelperNames(ast, sourceCode, filePath);
1471
+ const namespaceHelpers = collectNamespaceHelperImports(ast, filePath);
1472
+ const resultTypeNames = collectResultTypeNames(ast);
305
1473
 
306
1474
  for (const def of findTrailDefinitions(ast)) {
307
- const info = { id: def.id, label: def.kind === 'hike' ? 'Hike' : 'Trail' };
308
- for (const implValue of findImplementationBodies(def.config as AstNode)) {
1475
+ const info = { id: def.id, label: 'Trail' };
1476
+ for (const implValue of findBlazeBodies(def.config as AstNode)) {
309
1477
  checkImplementation(
310
1478
  implValue,
311
1479
  info,
312
1480
  filePath,
313
1481
  sourceCode,
314
1482
  helperNames,
1483
+ namespaceHelpers,
1484
+ resultTypeNames,
315
1485
  diagnostics
316
1486
  );
317
1487
  }