@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.
- package/CHANGELOG.md +497 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +389 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +553 -0
- package/src/rules/public-internal-deep-imports.ts +517 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/error-mapping-completeness.trail.ts +29 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +194 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +213 -0
- package/src/workspaces.ts +238 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -221
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -163
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -231
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -46
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -75
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -188
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/rules/specs.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface ObjectProperty extends ParsedEntry {
|
|
|
18
18
|
|
|
19
19
|
export interface TrailLikeSpec {
|
|
20
20
|
readonly id: string;
|
|
21
|
-
readonly kind: '
|
|
21
|
+
readonly kind: 'signal' | 'trail';
|
|
22
22
|
readonly line: number;
|
|
23
23
|
readonly properties: ReadonlyMap<string, ObjectProperty>;
|
|
24
24
|
readonly specText: string;
|
|
@@ -31,7 +31,11 @@ export interface SchemaFieldInfo {
|
|
|
31
31
|
readonly required: boolean;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// Match `trail(...)` / `signal(...)` declaration sites, not method calls.
|
|
35
|
+
// The negative lookbehind excludes `foo.signal(...)`, `foo?.signal(...)`, and
|
|
36
|
+
// similar member-expression call sites (including optional chaining with
|
|
37
|
+
// whitespace) where `signal` / `trail` is a property name, not a factory.
|
|
38
|
+
const TRAIL_LIKE_PATTERN = /(?<![.?])\b(trail|signal)\s*\(/g;
|
|
35
39
|
|
|
36
40
|
const PROPERTY_PATTERN =
|
|
37
41
|
/^(?:readonly\s+)?(?:(["'`])([^"'`]+)\1|([A-Za-z_$][\w$]*))\s*:\s*([\s\S]+)$/;
|
|
@@ -214,7 +218,7 @@ const resolveSpecId = (
|
|
|
214
218
|
|
|
215
219
|
const buildTrailLikeSpec = (
|
|
216
220
|
sourceCode: string,
|
|
217
|
-
kind: '
|
|
221
|
+
kind: 'signal' | 'trail',
|
|
218
222
|
specArg: SplitEntry,
|
|
219
223
|
specStart: number,
|
|
220
224
|
id: string,
|
|
@@ -276,7 +280,7 @@ const resolveTrailLikeSpec = (
|
|
|
276
280
|
|
|
277
281
|
const parseTrailLikeMatch = (
|
|
278
282
|
sourceCode: string,
|
|
279
|
-
kind: '
|
|
283
|
+
kind: 'signal' | 'trail',
|
|
280
284
|
callStart: number
|
|
281
285
|
): TrailLikeSpec | null => {
|
|
282
286
|
const resolved = resolveTrailLikeSpec(sourceCode, callStart);
|
|
@@ -352,7 +356,7 @@ export const findTrailLikeSpecs = (
|
|
|
352
356
|
continue;
|
|
353
357
|
}
|
|
354
358
|
|
|
355
|
-
const kind = match[1] === '
|
|
359
|
+
const kind = match[1] === 'signal' ? 'signal' : 'trail';
|
|
356
360
|
const spec = parseTrailLikeMatch(sourceCode, kind, callStart);
|
|
357
361
|
if (spec !== null) {
|
|
358
362
|
specs.push(spec);
|
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefers static resource definition helpers over dynamic context lookups.
|
|
3
|
+
*
|
|
4
|
+
* The rule intentionally stays advisory and narrow: it only warns when the
|
|
5
|
+
* trail already has a statically declared resource definition in `resources`.
|
|
6
|
+
* Dynamic IDs and generic framework internals remain outside its scope.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
collectNamedResourceIds,
|
|
11
|
+
extractFirstStringArg,
|
|
12
|
+
findBlazeBodies,
|
|
13
|
+
findConfigProperty,
|
|
14
|
+
findTrailDefinitions,
|
|
15
|
+
getStringValue,
|
|
16
|
+
identifierName,
|
|
17
|
+
isStringLiteral,
|
|
18
|
+
offsetToLine,
|
|
19
|
+
parse,
|
|
20
|
+
walk,
|
|
21
|
+
walkScope,
|
|
22
|
+
walkWithScopes,
|
|
23
|
+
} from './ast.js';
|
|
24
|
+
import type { AstNode } from './ast.js';
|
|
25
|
+
import { isFrameworkInternalFile, isTestFile } from './scan.js';
|
|
26
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
27
|
+
|
|
28
|
+
const RULE_NAME = 'static-resource-accessor-preference';
|
|
29
|
+
|
|
30
|
+
const MEMBER_TYPES = new Set(['StaticMemberExpression', 'MemberExpression']);
|
|
31
|
+
|
|
32
|
+
const NAMED_DEPENDENCY_CONSTRUCTORS = new Map<string, ReadonlySet<string>>([
|
|
33
|
+
['@prisma/client', new Set(['PrismaClient'])],
|
|
34
|
+
['pg', new Set(['Pool', 'Client'])],
|
|
35
|
+
['mongodb', new Set(['MongoClient'])],
|
|
36
|
+
['ioredis', new Set(['Redis'])],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
interface DeclaredStaticResource {
|
|
40
|
+
readonly id: string | null;
|
|
41
|
+
readonly name: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ResourceLookup {
|
|
45
|
+
readonly id: string | null;
|
|
46
|
+
readonly name: string | null;
|
|
47
|
+
readonly rendered: string;
|
|
48
|
+
readonly shadowedDeclaredNames: ReadonlySet<string>;
|
|
49
|
+
readonly start: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface InlineDependencyConstruction {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly rendered: string;
|
|
55
|
+
readonly start: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isShadowedModuleBinding = (
|
|
59
|
+
name: string | null,
|
|
60
|
+
scopes: readonly ReadonlySet<string>[]
|
|
61
|
+
): boolean => {
|
|
62
|
+
if (!name) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < scopes.length - 1; i += 1) {
|
|
66
|
+
const frame = scopes[i];
|
|
67
|
+
if (frame?.has(name)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const extractMemberPair = (
|
|
75
|
+
callee: AstNode
|
|
76
|
+
): { readonly objName: string; readonly propName: string } | null => {
|
|
77
|
+
if (!MEMBER_TYPES.has(callee.type)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const objName = identifierName(
|
|
82
|
+
(callee as unknown as { object?: AstNode }).object
|
|
83
|
+
);
|
|
84
|
+
const propName = identifierName(
|
|
85
|
+
(callee as unknown as { property?: AstNode }).property
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return objName && propName ? { objName, propName } : null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getResourceElements = (config: AstNode): readonly AstNode[] => {
|
|
92
|
+
const resourcesProp = findConfigProperty(config, 'resources');
|
|
93
|
+
if (!resourcesProp) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const arrayNode = resourcesProp.value;
|
|
98
|
+
if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
((arrayNode as AstNode)['elements'] as readonly AstNode[] | undefined) ?? []
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const extractDeclaredStaticResources = (
|
|
108
|
+
config: AstNode,
|
|
109
|
+
resourceIdsByName: ReadonlyMap<string, string>
|
|
110
|
+
): readonly DeclaredStaticResource[] =>
|
|
111
|
+
getResourceElements(config).flatMap((element) => {
|
|
112
|
+
if (element.type !== 'Identifier') {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const name = identifierName(element);
|
|
117
|
+
return name ? [{ id: resourceIdsByName.get(name) ?? null, name }] : [];
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const extractContextParamNode = (blazeBody: AstNode): AstNode | null => {
|
|
121
|
+
const params = blazeBody['params'] as readonly AstNode[] | undefined;
|
|
122
|
+
if (!params || params.length < 2) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return params[1] ?? null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const extractContextParamName = (blazeBody: AstNode): string | null => {
|
|
129
|
+
const param = extractContextParamNode(blazeBody);
|
|
130
|
+
if (!param) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (param.type === 'AssignmentPattern') {
|
|
134
|
+
return identifierName((param as unknown as { left?: AstNode }).left);
|
|
135
|
+
}
|
|
136
|
+
return identifierName(param);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const extractResourceAlias = (property: AstNode): string | null => {
|
|
140
|
+
if (property.type !== 'Property') {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const keyName = identifierName(
|
|
145
|
+
(property as unknown as { key?: AstNode }).key
|
|
146
|
+
);
|
|
147
|
+
if (keyName !== 'resource') {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
identifierName((property as unknown as { value?: AstNode }).value) ??
|
|
153
|
+
keyName
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const collectParamResourceAliases = (body: AstNode): ReadonlySet<string> => {
|
|
158
|
+
const param = extractContextParamNode(body);
|
|
159
|
+
if (!param || param.type !== 'ObjectPattern') {
|
|
160
|
+
return new Set();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const aliases = new Set<string>();
|
|
164
|
+
const properties = param['properties'] as readonly AstNode[] | undefined;
|
|
165
|
+
for (const property of properties ?? []) {
|
|
166
|
+
const alias = extractResourceAlias(property);
|
|
167
|
+
if (alias) {
|
|
168
|
+
aliases.add(alias);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return aliases;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const buildCtxNames = (body: AstNode): ReadonlySet<string> => {
|
|
175
|
+
const ctxNames = new Set<string>();
|
|
176
|
+
const paramName = extractContextParamName(body);
|
|
177
|
+
if (paramName) {
|
|
178
|
+
ctxNames.add(paramName);
|
|
179
|
+
}
|
|
180
|
+
return ctxNames;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const extractObjectPatternAliases = (
|
|
184
|
+
pattern: AstNode | undefined
|
|
185
|
+
): readonly string[] => {
|
|
186
|
+
if (pattern?.type !== 'ObjectPattern') {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const properties = pattern['properties'] as readonly AstNode[] | undefined;
|
|
191
|
+
return (properties ?? []).flatMap((property) => {
|
|
192
|
+
const alias = extractResourceAlias(property);
|
|
193
|
+
return alias ? [alias] : [];
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const collectResourceAliases = (
|
|
198
|
+
body: AstNode,
|
|
199
|
+
ctxNames: ReadonlySet<string>
|
|
200
|
+
): ReadonlySet<string> => {
|
|
201
|
+
const aliases = new Set<string>();
|
|
202
|
+
|
|
203
|
+
walkScope(body, (node) => {
|
|
204
|
+
if (node.type !== 'VariableDeclarator') {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { id, init } = node as unknown as {
|
|
209
|
+
readonly id?: AstNode;
|
|
210
|
+
readonly init?: AstNode;
|
|
211
|
+
};
|
|
212
|
+
const initName = identifierName(init);
|
|
213
|
+
if (!initName || !ctxNames.has(initName)) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const alias of extractObjectPatternAliases(id)) {
|
|
218
|
+
aliases.add(alias);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return aliases;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const extractCallCallee = (node: AstNode): AstNode | null => {
|
|
226
|
+
if (node.type !== 'CallExpression') {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return ((node as unknown as { callee?: AstNode }).callee ??
|
|
230
|
+
null) as AstNode | null;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const extractFirstArg = (node: AstNode): AstNode | null => {
|
|
234
|
+
if (node.type !== 'CallExpression') {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const args = node['arguments'] as readonly AstNode[] | undefined;
|
|
238
|
+
return args?.[0] ?? null;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const renderStringArg = (value: string): string =>
|
|
242
|
+
`'${value.replaceAll("'", "\\'")}'`;
|
|
243
|
+
|
|
244
|
+
const renderResourceArg = (node: AstNode | null): string | null => {
|
|
245
|
+
if (!node) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const name = identifierName(node);
|
|
249
|
+
if (name) {
|
|
250
|
+
return name;
|
|
251
|
+
}
|
|
252
|
+
return isStringLiteral(node)
|
|
253
|
+
? renderStringArg(getStringValue(node) ?? '')
|
|
254
|
+
: null;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const extractFirstIdentifierArg = (node: AstNode): string | null =>
|
|
258
|
+
identifierName(extractFirstArg(node) ?? undefined);
|
|
259
|
+
|
|
260
|
+
const isMemberResourceCall = (
|
|
261
|
+
callee: AstNode,
|
|
262
|
+
ctxNames: ReadonlySet<string>
|
|
263
|
+
): { readonly ctxName: string } | null => {
|
|
264
|
+
const pair = extractMemberPair(callee);
|
|
265
|
+
return pair && ctxNames.has(pair.objName) && pair.propName === 'resource'
|
|
266
|
+
? { ctxName: pair.objName }
|
|
267
|
+
: null;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const extractResourceLookup = (
|
|
271
|
+
node: AstNode,
|
|
272
|
+
ctxNames: ReadonlySet<string>,
|
|
273
|
+
resourceAliases: ReadonlySet<string>
|
|
274
|
+
): ResourceLookup | null => {
|
|
275
|
+
const callee = extractCallCallee(node);
|
|
276
|
+
if (!callee) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const arg = extractFirstArg(node);
|
|
281
|
+
const renderedArg = renderResourceArg(arg);
|
|
282
|
+
if (!renderedArg) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const memberCall = isMemberResourceCall(callee, ctxNames);
|
|
287
|
+
if (memberCall) {
|
|
288
|
+
return {
|
|
289
|
+
id: extractFirstStringArg(node),
|
|
290
|
+
name: extractFirstIdentifierArg(node),
|
|
291
|
+
rendered: `${memberCall.ctxName}.resource(${renderedArg})`,
|
|
292
|
+
shadowedDeclaredNames: new Set(),
|
|
293
|
+
start: node.start,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const calleeName = identifierName(callee);
|
|
298
|
+
if (calleeName && resourceAliases.has(calleeName)) {
|
|
299
|
+
return {
|
|
300
|
+
id: extractFirstStringArg(node),
|
|
301
|
+
name: extractFirstIdentifierArg(node),
|
|
302
|
+
rendered: `${calleeName}(${renderedArg})`,
|
|
303
|
+
shadowedDeclaredNames: new Set(),
|
|
304
|
+
start: node.start,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const buildDeclaredNameSet = (
|
|
312
|
+
resources: readonly DeclaredStaticResource[]
|
|
313
|
+
): ReadonlySet<string> => new Set(resources.map((resource) => resource.name));
|
|
314
|
+
|
|
315
|
+
const collectShadowedNames = (
|
|
316
|
+
names: ReadonlySet<string>,
|
|
317
|
+
scopes: readonly ReadonlySet<string>[]
|
|
318
|
+
): ReadonlySet<string> => {
|
|
319
|
+
const shadowed = new Set<string>();
|
|
320
|
+
for (const name of names) {
|
|
321
|
+
if (isShadowedModuleBinding(name, scopes)) {
|
|
322
|
+
shadowed.add(name);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return shadowed;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const buildDeclaredNameById = (
|
|
329
|
+
resources: readonly DeclaredStaticResource[]
|
|
330
|
+
): ReadonlyMap<string, string> =>
|
|
331
|
+
new Map(
|
|
332
|
+
resources.flatMap((resource) =>
|
|
333
|
+
resource.id ? [[resource.id, resource.name] as const] : []
|
|
334
|
+
)
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const collectResourceLookups = (
|
|
338
|
+
config: AstNode,
|
|
339
|
+
declaredNames: ReadonlySet<string>
|
|
340
|
+
): readonly ResourceLookup[] => {
|
|
341
|
+
const lookups: ResourceLookup[] = [];
|
|
342
|
+
|
|
343
|
+
for (const body of findBlazeBodies(config)) {
|
|
344
|
+
const ctxNames = buildCtxNames(body);
|
|
345
|
+
const resourceAliases = new Set([
|
|
346
|
+
...collectParamResourceAliases(body),
|
|
347
|
+
...collectResourceAliases(body, ctxNames),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
walkWithScopes(
|
|
351
|
+
body,
|
|
352
|
+
(node, scopes) => {
|
|
353
|
+
const lookup = extractResourceLookup(node, ctxNames, resourceAliases);
|
|
354
|
+
if (lookup && !isShadowedModuleBinding(lookup.name, scopes)) {
|
|
355
|
+
lookups.push({
|
|
356
|
+
...lookup,
|
|
357
|
+
shadowedDeclaredNames: collectShadowedNames(declaredNames, scopes),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
initialScopes: [declaredNames],
|
|
363
|
+
stopAtNestedFunctions: true,
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return lookups;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const getImportSourceValue = (node: AstNode): string | null => {
|
|
372
|
+
const sourceNode = (node as unknown as { source?: AstNode }).source;
|
|
373
|
+
const value = sourceNode
|
|
374
|
+
? (sourceNode as unknown as { value?: unknown }).value
|
|
375
|
+
: null;
|
|
376
|
+
return typeof value === 'string' ? value : null;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const addNamedDependencyConstructors = (
|
|
380
|
+
source: string,
|
|
381
|
+
specifier: AstNode,
|
|
382
|
+
constructors: Set<string>
|
|
383
|
+
): void => {
|
|
384
|
+
if (specifier.type !== 'ImportSpecifier') {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const { imported, local } = specifier as unknown as {
|
|
389
|
+
readonly imported?: AstNode;
|
|
390
|
+
readonly local?: AstNode;
|
|
391
|
+
};
|
|
392
|
+
const importedName =
|
|
393
|
+
identifierName(imported) ??
|
|
394
|
+
(imported && isStringLiteral(imported) ? getStringValue(imported) : null);
|
|
395
|
+
const localName = identifierName(local);
|
|
396
|
+
if (!importedName || !localName) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (
|
|
401
|
+
source.startsWith('@aws-sdk/client-') &&
|
|
402
|
+
importedName.endsWith('Client')
|
|
403
|
+
) {
|
|
404
|
+
constructors.add(localName);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const names = NAMED_DEPENDENCY_CONSTRUCTORS.get(source);
|
|
409
|
+
if (names?.has(importedName)) {
|
|
410
|
+
constructors.add(localName);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const addDefaultDependencyConstructors = (
|
|
415
|
+
source: string,
|
|
416
|
+
specifier: AstNode,
|
|
417
|
+
constructors: Set<string>
|
|
418
|
+
): void => {
|
|
419
|
+
if (specifier.type !== 'ImportDefaultSpecifier' || source !== 'ioredis') {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const localName = identifierName(
|
|
424
|
+
(specifier as unknown as { local?: AstNode }).local
|
|
425
|
+
);
|
|
426
|
+
if (localName) {
|
|
427
|
+
constructors.add(localName);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const collectDependencyConstructors = (ast: AstNode): ReadonlySet<string> => {
|
|
432
|
+
const constructors = new Set<string>();
|
|
433
|
+
|
|
434
|
+
walk(ast, (node) => {
|
|
435
|
+
if (node.type !== 'ImportDeclaration') {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const source = getImportSourceValue(node);
|
|
440
|
+
if (!source) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const specifiers = node['specifiers'] as readonly AstNode[] | undefined;
|
|
445
|
+
for (const specifier of specifiers ?? []) {
|
|
446
|
+
addNamedDependencyConstructors(source, specifier, constructors);
|
|
447
|
+
addDefaultDependencyConstructors(source, specifier, constructors);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
return constructors;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const extractInlineDependencyConstruction = (
|
|
455
|
+
node: AstNode,
|
|
456
|
+
dependencyConstructors: ReadonlySet<string>
|
|
457
|
+
): InlineDependencyConstruction | null => {
|
|
458
|
+
if (node.type !== 'NewExpression') {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const ctorName = identifierName(
|
|
463
|
+
(node as unknown as { callee?: AstNode }).callee
|
|
464
|
+
);
|
|
465
|
+
return ctorName && dependencyConstructors.has(ctorName)
|
|
466
|
+
? { name: ctorName, rendered: `new ${ctorName}(...)`, start: node.start }
|
|
467
|
+
: null;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const collectInlineDependencyConstructions = (
|
|
471
|
+
config: AstNode,
|
|
472
|
+
dependencyConstructors: ReadonlySet<string>
|
|
473
|
+
): readonly InlineDependencyConstruction[] => {
|
|
474
|
+
const constructions: InlineDependencyConstruction[] = [];
|
|
475
|
+
|
|
476
|
+
for (const body of findBlazeBodies(config)) {
|
|
477
|
+
walkWithScopes(
|
|
478
|
+
body,
|
|
479
|
+
(node, scopes) => {
|
|
480
|
+
const construction = extractInlineDependencyConstruction(
|
|
481
|
+
node,
|
|
482
|
+
dependencyConstructors
|
|
483
|
+
);
|
|
484
|
+
if (
|
|
485
|
+
construction &&
|
|
486
|
+
!isShadowedModuleBinding(construction.name, scopes)
|
|
487
|
+
) {
|
|
488
|
+
constructions.push(construction);
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
initialScopes: [dependencyConstructors],
|
|
493
|
+
stopAtNestedFunctions: true,
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return constructions;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const buildAccessorDiagnostic = (
|
|
502
|
+
trailId: string,
|
|
503
|
+
lookup: ResourceLookup,
|
|
504
|
+
resourceName: string,
|
|
505
|
+
filePath: string,
|
|
506
|
+
sourceCode: string
|
|
507
|
+
): WardenDiagnostic => ({
|
|
508
|
+
filePath,
|
|
509
|
+
line: offsetToLine(sourceCode, lookup.start),
|
|
510
|
+
message:
|
|
511
|
+
`Trail "${trailId}": ${lookup.rendered} uses a dynamic resource accessor ` +
|
|
512
|
+
`for statically declared resource '${resourceName}'. Prefer ${resourceName}.from(ctx) ` +
|
|
513
|
+
'so the dependency stays type-directed.',
|
|
514
|
+
rule: RULE_NAME,
|
|
515
|
+
severity: 'warn',
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const buildInlineDependencyDiagnostic = (
|
|
519
|
+
trailId: string,
|
|
520
|
+
construction: InlineDependencyConstruction,
|
|
521
|
+
filePath: string,
|
|
522
|
+
sourceCode: string
|
|
523
|
+
): WardenDiagnostic => ({
|
|
524
|
+
filePath,
|
|
525
|
+
line: offsetToLine(sourceCode, construction.start),
|
|
526
|
+
message:
|
|
527
|
+
`Trail "${trailId}": ${construction.rendered} constructs an external dependency ` +
|
|
528
|
+
'inside blaze logic. Move the client behind a resource definition and declare it in resources.',
|
|
529
|
+
rule: RULE_NAME,
|
|
530
|
+
severity: 'warn',
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const reportAccessorLookups = (
|
|
534
|
+
trailId: string,
|
|
535
|
+
filePath: string,
|
|
536
|
+
sourceCode: string,
|
|
537
|
+
declaredResources: readonly DeclaredStaticResource[],
|
|
538
|
+
lookups: readonly ResourceLookup[],
|
|
539
|
+
diagnostics: WardenDiagnostic[]
|
|
540
|
+
): void => {
|
|
541
|
+
const declaredNames = buildDeclaredNameSet(declaredResources);
|
|
542
|
+
const declaredNameById = buildDeclaredNameById(declaredResources);
|
|
543
|
+
|
|
544
|
+
for (const lookup of lookups) {
|
|
545
|
+
const resourceName =
|
|
546
|
+
(lookup.name && declaredNames.has(lookup.name) ? lookup.name : null) ??
|
|
547
|
+
(lookup.id ? (declaredNameById.get(lookup.id) ?? null) : null);
|
|
548
|
+
|
|
549
|
+
if (!resourceName) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (lookup.shadowedDeclaredNames.has(resourceName)) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
diagnostics.push(
|
|
557
|
+
buildAccessorDiagnostic(
|
|
558
|
+
trailId,
|
|
559
|
+
lookup,
|
|
560
|
+
resourceName,
|
|
561
|
+
filePath,
|
|
562
|
+
sourceCode
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const reportInlineDependencyConstructions = (
|
|
569
|
+
trailId: string,
|
|
570
|
+
filePath: string,
|
|
571
|
+
sourceCode: string,
|
|
572
|
+
constructions: readonly InlineDependencyConstruction[],
|
|
573
|
+
diagnostics: WardenDiagnostic[]
|
|
574
|
+
): void => {
|
|
575
|
+
for (const construction of constructions) {
|
|
576
|
+
diagnostics.push(
|
|
577
|
+
buildInlineDependencyDiagnostic(
|
|
578
|
+
trailId,
|
|
579
|
+
construction,
|
|
580
|
+
filePath,
|
|
581
|
+
sourceCode
|
|
582
|
+
)
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const checkTrailDefinition = (
|
|
588
|
+
def: { readonly config: AstNode; readonly id: string },
|
|
589
|
+
filePath: string,
|
|
590
|
+
sourceCode: string,
|
|
591
|
+
resourceIdsByName: ReadonlyMap<string, string>,
|
|
592
|
+
dependencyConstructors: ReadonlySet<string>,
|
|
593
|
+
diagnostics: WardenDiagnostic[]
|
|
594
|
+
): void => {
|
|
595
|
+
const declaredResources = extractDeclaredStaticResources(
|
|
596
|
+
def.config,
|
|
597
|
+
resourceIdsByName
|
|
598
|
+
);
|
|
599
|
+
const lookups = collectResourceLookups(
|
|
600
|
+
def.config,
|
|
601
|
+
buildDeclaredNameSet(declaredResources)
|
|
602
|
+
);
|
|
603
|
+
reportAccessorLookups(
|
|
604
|
+
def.id,
|
|
605
|
+
filePath,
|
|
606
|
+
sourceCode,
|
|
607
|
+
declaredResources,
|
|
608
|
+
lookups,
|
|
609
|
+
diagnostics
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const constructions = collectInlineDependencyConstructions(
|
|
613
|
+
def.config,
|
|
614
|
+
dependencyConstructors
|
|
615
|
+
);
|
|
616
|
+
reportInlineDependencyConstructions(
|
|
617
|
+
def.id,
|
|
618
|
+
filePath,
|
|
619
|
+
sourceCode,
|
|
620
|
+
constructions,
|
|
621
|
+
diagnostics
|
|
622
|
+
);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
export const staticResourceAccessorPreference: WardenRule = {
|
|
626
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
627
|
+
if (isTestFile(filePath) || isFrameworkInternalFile(filePath)) {
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const ast = parse(filePath, sourceCode);
|
|
632
|
+
if (!ast) {
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
637
|
+
const resourceIdsByName = collectNamedResourceIds(ast);
|
|
638
|
+
const dependencyConstructors = collectDependencyConstructors(ast);
|
|
639
|
+
|
|
640
|
+
for (const def of findTrailDefinitions(ast)) {
|
|
641
|
+
checkTrailDefinition(
|
|
642
|
+
def,
|
|
643
|
+
filePath,
|
|
644
|
+
sourceCode,
|
|
645
|
+
resourceIdsByName,
|
|
646
|
+
dependencyConstructors,
|
|
647
|
+
diagnostics
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return diagnostics;
|
|
652
|
+
},
|
|
653
|
+
description:
|
|
654
|
+
'Prefer static resource.from(ctx) helpers over dynamic ctx.resource() lookups when the resource definition is already in scope.',
|
|
655
|
+
name: RULE_NAME,
|
|
656
|
+
severity: 'warn',
|
|
657
|
+
};
|