@filipebraida/adonis-function-points 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +427 -0
- package/bin/cli.js +4 -0
- package/build/commands/fp_calibrate.d.ts +16 -0
- package/build/commands/fp_count.d.ts +11 -0
- package/build/commands/fp_diff.d.ts +16 -0
- package/build/commands/fp_explain.d.ts +15 -0
- package/build/commands/fp_inventory.d.ts +9 -0
- package/build/commands/main.d.ts +5 -0
- package/build/commands/main.js +120 -0
- package/build/commands/printer.d.ts +9 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +10 -0
- package/build/define_config-DOqWyPwV.js +19 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +4 -0
- package/build/pipeline-BzP-ITGN.js +2306 -0
- package/build/resolvers-CU9HKYpn.js +555 -0
- package/build/runners-Bt8tbISi.js +630 -0
- package/build/scripts/smoke_package.d.ts +1 -0
- package/build/src/albrecht/calibration.d.ts +62 -0
- package/build/src/albrecht/counter.d.ts +48 -0
- package/build/src/albrecht/data_functions.d.ts +32 -0
- package/build/src/albrecht/diff.d.ts +66 -0
- package/build/src/albrecht/index.d.ts +13 -0
- package/build/src/albrecht/tables.d.ts +19 -0
- package/build/src/albrecht/technical_filter.d.ts +25 -0
- package/build/src/albrecht/transactional_functions.d.ts +35 -0
- package/build/src/cli/load_config.d.ts +28 -0
- package/build/src/cli/print.d.ts +19 -0
- package/build/src/cli/runners.d.ts +52 -0
- package/build/src/cli.d.ts +19 -0
- package/build/src/cli.js +198 -0
- package/build/src/define_config.d.ts +137 -0
- package/build/src/inventory/app_context.d.ts +73 -0
- package/build/src/inventory/detectors/lucid.d.ts +77 -0
- package/build/src/inventory/graph/call_graph.d.ts +80 -0
- package/build/src/inventory/graph/noise.d.ts +9 -0
- package/build/src/inventory/index.d.ts +15 -0
- package/build/src/inventory/paths.d.ts +22 -0
- package/build/src/inventory/resolvers/action_object.d.ts +14 -0
- package/build/src/inventory/resolvers/index.d.ts +23 -0
- package/build/src/inventory/resolvers/index.js +2 -0
- package/build/src/inventory/resolvers/job_dispatch.d.ts +18 -0
- package/build/src/inventory/resolvers/module_function.d.ts +11 -0
- package/build/src/inventory/resolvers/property_service.d.ts +18 -0
- package/build/src/inventory/resolvers/same_class_method.d.ts +17 -0
- package/build/src/inventory/resolvers/static_service.d.ts +13 -0
- package/build/src/inventory/resolvers/transformer.d.ts +25 -0
- package/build/src/inventory/resolvers/types.d.ts +65 -0
- package/build/src/inventory/source.d.ts +39 -0
- package/build/src/inventory/sources/data_stores.d.ts +31 -0
- package/build/src/inventory/sources/json_schemas.d.ts +32 -0
- package/build/src/inventory/sources/routes_ast.d.ts +28 -0
- package/build/src/metrics/structure.d.ts +72 -0
- package/build/src/pipeline.d.ts +44 -0
- package/build/src/pipeline.js +2 -0
- package/build/src/reporters/table.d.ts +6 -0
- package/build/src/types.d.ts +256 -0
- package/build/src/types.js +1 -0
- package/build/stubs/config.stub +37 -0
- package/build/tmp/probe.d.ts +1 -0
- package/build/tmp/probe_cli.d.ts +1 -0
- package/build/tmp/probe_cmp.d.ts +1 -0
- package/build/tmp/probe_count.d.ts +1 -0
- package/build/tmp/probe_data.d.ts +1 -0
- package/build/tmp/probe_diff.d.ts +1 -0
- package/build/tmp/probe_gap.d.ts +1 -0
- package/build/tmp/probe_graph.d.ts +1 -0
- package/build/tmp/probe_metrics.d.ts +1 -0
- package/build/tmp/probe_miss.d.ts +1 -0
- package/build/tmp/probe_names.d.ts +1 -0
- package/build/tmp/probe_nodata.d.ts +1 -0
- package/build/tmp/probe_one.d.ts +1 -0
- package/build/tmp/probe_perf.d.ts +1 -0
- package/build/tmp/probe_routes.d.ts +1 -0
- package/build/tmp/probe_unres.d.ts +1 -0
- package/build/tmp/probe_vazquez.d.ts +1 -0
- package/build/tsdown.config.d.ts +2 -0
- package/package.json +133 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ComplexityTable } from './albrecht/tables.js';
|
|
2
|
+
import type { CallResolver } from './inventory/resolvers/types.js';
|
|
3
|
+
import type { Complexity, FunctionType } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Package configuration.
|
|
6
|
+
*
|
|
7
|
+
* **Every option here has an effect, and a test proving it.** Configuration the
|
|
8
|
+
* code does not honour is worse than no configuration at all: whoever sets it
|
|
9
|
+
* believes something changed when nothing did, and the number goes into an
|
|
10
|
+
* invoice.
|
|
11
|
+
*
|
|
12
|
+
* That is why the list is short. What remains configurable is what is a
|
|
13
|
+
* **business decision** that no heuristic should make — the application
|
|
14
|
+
* boundary, what is maintained externally, the calibrated complexity bands.
|
|
15
|
+
* The rest the package discovers.
|
|
16
|
+
*/
|
|
17
|
+
export type FunctionPointsConfig = {
|
|
18
|
+
/**
|
|
19
|
+
* The application boundary. A business decision, not a technical one — review
|
|
20
|
+
* it with whoever signs the contract, not only with the team.
|
|
21
|
+
*/
|
|
22
|
+
boundary: {
|
|
23
|
+
/**
|
|
24
|
+
* Infrastructure stores, excluded from the count: session tokens, audit
|
|
25
|
+
* trails, queues, caches.
|
|
26
|
+
*
|
|
27
|
+
* Complements the automatic technical-data filter (AFP §6.5.2.1.1), which
|
|
28
|
+
* already catches session, error, search and template names. Exclusions
|
|
29
|
+
* made here also appear in the report, with the reason.
|
|
30
|
+
*/
|
|
31
|
+
infrastructure?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Stores maintained by another system: counted as EIF instead of ILF.
|
|
34
|
+
*
|
|
35
|
+
* For example, tables mirrored from an external ERP.
|
|
36
|
+
*/
|
|
37
|
+
externallyMaintained?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Entry points with no functional value to the user, by route name or by
|
|
40
|
+
* identity (`GET /health`).
|
|
41
|
+
*
|
|
42
|
+
* Rarely needed in practice: an infrastructure route reaches no data store
|
|
43
|
+
* and already drops out. It stays as a safety net and to make the intent
|
|
44
|
+
* explicit in the report.
|
|
45
|
+
*/
|
|
46
|
+
ignoreEntryPoints?: string[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Strategy for RET, the logical subgroups of an ILF/EIF.
|
|
50
|
+
*
|
|
51
|
+
* `constant` pins it at 1, which is honest: what a user recognises as a
|
|
52
|
+
* subgroup is not derivable from code. `composition` derives it from
|
|
53
|
+
* composition relations; less accurate in general, but captures real
|
|
54
|
+
* aggregates.
|
|
55
|
+
*/
|
|
56
|
+
retStrategy: 'constant' | 'composition';
|
|
57
|
+
/**
|
|
58
|
+
* Maximum depth in the call graph, starting at the handler.
|
|
59
|
+
*
|
|
60
|
+
* Too deep and a large shared service contaminates its callers; too shallow
|
|
61
|
+
* and the write is missed.
|
|
62
|
+
*/
|
|
63
|
+
maxDepth: number;
|
|
64
|
+
/**
|
|
65
|
+
* Extra DET for the confirmation or error message.
|
|
66
|
+
*
|
|
67
|
+
* The IFPUG manual counts one, AFP does not — a known systematic divergence
|
|
68
|
+
* of −1 DET per transaction against manual counts. The default follows AFP.
|
|
69
|
+
*/
|
|
70
|
+
messageDet: number;
|
|
71
|
+
/** Complexity bands, for calibrating against manual counts. */
|
|
72
|
+
complexityTables?: Partial<Record<FunctionType, ComplexityTable>>;
|
|
73
|
+
/** Weights per type and complexity. */
|
|
74
|
+
weights?: Partial<Record<FunctionType, Record<Complexity, number>>>;
|
|
75
|
+
/**
|
|
76
|
+
* Custom tracing strategies, added to the built-in ones and running
|
|
77
|
+
* **before** them.
|
|
78
|
+
*
|
|
79
|
+
* This is what makes the architecture's central claim true: AdonisJS imposes
|
|
80
|
+
* no code organisation, so a project with its own convention registers it
|
|
81
|
+
* here.
|
|
82
|
+
*/
|
|
83
|
+
resolvers?: {
|
|
84
|
+
call?: CallResolver[];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Minimum tracing coverage, from 0 to 1.
|
|
88
|
+
*
|
|
89
|
+
* Below it the analysis **fails** instead of emitting a number that looks
|
|
90
|
+
* right. A total resting on many unresolved calls should not become an
|
|
91
|
+
* invoice.
|
|
92
|
+
*/
|
|
93
|
+
minCoverage?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Declared DET or RET/FTR for a function the analysis cannot read, keyed by
|
|
96
|
+
* the name it has in the count (`Petition`, `POST /books`).
|
|
97
|
+
*
|
|
98
|
+
* The case this exists for is a schema-driven application: when the fields a
|
|
99
|
+
* user fills live in a JSON column whose schema is stored in the database,
|
|
100
|
+
* there is nothing for static analysis to read and the column counts as 1 DET
|
|
101
|
+
* (counting-decisions §8). The person who knows the form knows the number.
|
|
102
|
+
*
|
|
103
|
+
* `reason` is required, and that is the whole point. A declared number is
|
|
104
|
+
* reproducible — it lives in a versioned file, so the same revision yields
|
|
105
|
+
* the same count — and auditable, because `fp:explain` prints it with its
|
|
106
|
+
* justification. A number the tool guessed would be neither.
|
|
107
|
+
*
|
|
108
|
+
* Use sparingly. If overriding becomes a habit the count stops coming from
|
|
109
|
+
* the code, and the report says how much of the total came from here so that
|
|
110
|
+
* cannot grow unnoticed.
|
|
111
|
+
*/
|
|
112
|
+
overrides?: Record<string, FunctionOverride>;
|
|
113
|
+
};
|
|
114
|
+
export type FunctionOverride = {
|
|
115
|
+
/** declared DET count, replacing what the analysis found */
|
|
116
|
+
det?: number;
|
|
117
|
+
/**
|
|
118
|
+
* Name of a JSON Schema declared in the application's code, whose fields are
|
|
119
|
+
* counted by the §7 leaf rules and replace the single DET the opaque column
|
|
120
|
+
* contributed.
|
|
121
|
+
*
|
|
122
|
+
* Prefer this to `det`. A declared number freezes: someone adds a field, the
|
|
123
|
+
* count does not move, and `fp:diff` reports no change for real functional
|
|
124
|
+
* growth — undercounting silently and progressively. Naming the schema keeps
|
|
125
|
+
* the number coming from the code; the only thing maintained by hand is the
|
|
126
|
+
* mapping, which changes when a form is born rather than when a field is.
|
|
127
|
+
*
|
|
128
|
+
* A name that matches no schema is a warning, never a silent fallback.
|
|
129
|
+
*/
|
|
130
|
+
detFromSchema?: string;
|
|
131
|
+
/** declared RET (data function) or FTR (transaction) */
|
|
132
|
+
refs?: number;
|
|
133
|
+
/** why — required, and printed by `fp:explain` beside the number */
|
|
134
|
+
reason: string;
|
|
135
|
+
};
|
|
136
|
+
export declare const DEFAULTS: FunctionPointsConfig;
|
|
137
|
+
export declare function defineConfig(config: Partial<FunctionPointsConfig>): FunctionPointsConfig;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovers the shape of the analysed application instead of assuming a
|
|
3
|
+
* convention.
|
|
4
|
+
*
|
|
5
|
+
* Folder layout, subpath aliases and model style all vary between AdonisJS
|
|
6
|
+
* applications. What does not vary is that the facts are discoverable — which
|
|
7
|
+
* is why this package needs almost no configuration.
|
|
8
|
+
*/
|
|
9
|
+
export type AppLayout = 'flat' | 'module-per-domain' | 'unknown';
|
|
10
|
+
export type AppContext = {
|
|
11
|
+
/** application root (where adonisrc.ts lives) */
|
|
12
|
+
root: string;
|
|
13
|
+
/**
|
|
14
|
+
* The package.json `imports` map, normalised.
|
|
15
|
+
*
|
|
16
|
+
* It must be READ, never inferred: two incompatible conventions are in use —
|
|
17
|
+
* by artefact type (`#models/*`) and by domain module (`#catalog/*`).
|
|
18
|
+
*/
|
|
19
|
+
subpathImports: Map<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Generated artefacts found.
|
|
22
|
+
*
|
|
23
|
+
* Absence is a reportable fact, not something to work around silently:
|
|
24
|
+
* without the registry the count falls back to the route parser, which is
|
|
25
|
+
* less precise, and the report must say so.
|
|
26
|
+
*/
|
|
27
|
+
generated: {
|
|
28
|
+
routeRegistry?: string;
|
|
29
|
+
controllersMap?: string;
|
|
30
|
+
dataSchema?: string;
|
|
31
|
+
};
|
|
32
|
+
/** detected; used to GROUP the report, never to find files */
|
|
33
|
+
layout: AppLayout;
|
|
34
|
+
/**
|
|
35
|
+
* Files that register routes, starting from the adonisrc `preloads` and
|
|
36
|
+
* following the static imports they reach.
|
|
37
|
+
*
|
|
38
|
+
* The preload list is the authoritative source, not a path convention. Four
|
|
39
|
+
* topologies occur in practice: a single file, one per module, a directory,
|
|
40
|
+
* and a hub that only re-exports.
|
|
41
|
+
*/
|
|
42
|
+
routeFiles: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Directories to scan, derived from the alias targets.
|
|
45
|
+
*
|
|
46
|
+
* Not simply `app/`: applications exist where all writes live under `src/`.
|
|
47
|
+
* Roots nested inside other roots are collapsed.
|
|
48
|
+
*/
|
|
49
|
+
scanRoots: string[];
|
|
50
|
+
/** versions and ORM — they pick the strategy and go into the report */
|
|
51
|
+
framework: FrameworkInfo;
|
|
52
|
+
/** `#catalog/models/book` -> absolute path, or null */
|
|
53
|
+
resolveSpecifier(specifier: string): string | null;
|
|
54
|
+
/** module a file belongs to, for grouping the report */
|
|
55
|
+
moduleOf(absPath: string): string;
|
|
56
|
+
};
|
|
57
|
+
export type FrameworkInfo = {
|
|
58
|
+
/** major of @adonisjs/core, when declared */
|
|
59
|
+
core?: number;
|
|
60
|
+
/** major of @adonisjs/lucid, when declared */
|
|
61
|
+
lucid?: number;
|
|
62
|
+
orm: 'lucid' | 'kysely' | 'unknown';
|
|
63
|
+
/** Tuyau provides typed input DETs; optional */
|
|
64
|
+
tuyau: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Within the v1 scope (core 7 + Lucid 22).
|
|
67
|
+
*
|
|
68
|
+
* Outside it the package reports instead of counting — counting wrong in
|
|
69
|
+
* silence is the worst possible failure for a number that becomes an invoice.
|
|
70
|
+
*/
|
|
71
|
+
supported: boolean;
|
|
72
|
+
};
|
|
73
|
+
export declare function discoverApp(root: string): Promise<AppContext>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Node } from 'ts-morph';
|
|
2
|
+
import type { CallExpression } from 'ts-morph';
|
|
3
|
+
/**
|
|
4
|
+
* Recognises Lucid data access at a call site.
|
|
5
|
+
*
|
|
6
|
+
* **Call-site level, never file level.** A large domain service can hold dozens
|
|
7
|
+
* of writes; asking "does this file contain a write?" would mark every importer
|
|
8
|
+
* as a writer.
|
|
9
|
+
*
|
|
10
|
+
* False positives deliberately avoided here:
|
|
11
|
+
*
|
|
12
|
+
* `.related('x')` a relation accessor, used both to read and to write — it
|
|
13
|
+
* only counts when it ends in attach/detach/sync/save/create
|
|
14
|
+
* `.create(` also appears in `vine.create(` and various builders; it
|
|
15
|
+
* only counts when the receiver resolves to a known store
|
|
16
|
+
* `invite.save()` a lower-case instance does not match the model name under
|
|
17
|
+
* textual comparison — hence the symbol map
|
|
18
|
+
*/
|
|
19
|
+
export type AccessMode = 'read' | 'write';
|
|
20
|
+
export type PersistenceAccess = {
|
|
21
|
+
mode: AccessMode;
|
|
22
|
+
/** id of the data store reached */
|
|
23
|
+
store: string;
|
|
24
|
+
method: string;
|
|
25
|
+
line: number;
|
|
26
|
+
/**
|
|
27
|
+
* Store reached through a RELATION rather than directly.
|
|
28
|
+
*
|
|
29
|
+
* `Book.query().preload('author')` reads the authors table. Under AFP that is
|
|
30
|
+
* an FTR on `Author`, and ignoring it would drop a table read only through a
|
|
31
|
+
* relation out of the count (§6.5.4) when it is a legitimate EIF.
|
|
32
|
+
*/
|
|
33
|
+
viaRelation?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Does this access fire the model's hooks?
|
|
36
|
+
*
|
|
37
|
+
* Lucid fires instance hooks for `document.delete()` and does NOT fire them
|
|
38
|
+
* for `Document.query().where(…).delete()` — both of which land on
|
|
39
|
+
* `method === 'delete'`. Following hooks for the bulk form would invent an
|
|
40
|
+
* FTR, and counting more than is there is worse than counting less: an
|
|
41
|
+
* invented FTR moves a complexity band and goes onto an invoice.
|
|
42
|
+
*/
|
|
43
|
+
firesHooks: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** decorators this package knows how to follow */
|
|
46
|
+
export declare const HOOK_DECORATORS: Set<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Hook decorators fired by an access, or `[]` when it fires none.
|
|
49
|
+
*
|
|
50
|
+
* `truncate`, `increment`, `decrement` and the pivot operations change rows
|
|
51
|
+
* without instantiating a model, so no hook runs.
|
|
52
|
+
*/
|
|
53
|
+
export declare function hooksFiredBy(access: PersistenceAccess): string[];
|
|
54
|
+
/**
|
|
55
|
+
* Symbols that resolve to a data store within a body's scope.
|
|
56
|
+
*
|
|
57
|
+
* Includes the model name (`Invite`) and local variables derived from it
|
|
58
|
+
* (`const invite = await Invite.findOrFail(...)`).
|
|
59
|
+
*/
|
|
60
|
+
export type StoreSymbols = Map<string, string>;
|
|
61
|
+
/** store -> its declared relations */
|
|
62
|
+
export type RelationMap = Map<string, Record<string, string>>;
|
|
63
|
+
export declare function detectAccess(call: CallExpression, symbols: StoreSymbols, relations?: RelationMap): PersistenceAccess | null;
|
|
64
|
+
/**
|
|
65
|
+
* Dotted path of a receiver made only of property accesses: `input.invite`
|
|
66
|
+
* yields "input.invite". Any call in between invalidates the path, because the
|
|
67
|
+
* value stops being statically traceable.
|
|
68
|
+
*/
|
|
69
|
+
export declare function pathSymbolOf(node: Node): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Root of an `a.b().c()` chain — the left-most identifier.
|
|
72
|
+
*
|
|
73
|
+
* It must traverse `await`, calls, property access and `new`, otherwise
|
|
74
|
+
* `await new Action().handle()` and `Invite.query().where().update()` stop at
|
|
75
|
+
* the first node and the write disappears.
|
|
76
|
+
*/
|
|
77
|
+
export declare function rootSymbolOf(node: Node): string | null;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ClassDeclaration, SourceFile } from 'ts-morph';
|
|
2
|
+
import type { AppContext } from '../app_context.js';
|
|
3
|
+
import type { CollectedDataStore } from '../sources/data_stores.js';
|
|
4
|
+
import type { CallResolver } from '../resolvers/types.js';
|
|
5
|
+
import type { HandlerRef, TraceStep, UnresolvedCall } from '../../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* The transaction → data function graph. It is the backbone of the count.
|
|
8
|
+
*
|
|
9
|
+
* Three of the four boundary decisions are settled here: a static route does
|
|
10
|
+
* not count because it reaches no data; a route from a package likewise; a
|
|
11
|
+
* model hook counts because it lies on the path. And AFP requires aggregating
|
|
12
|
+
* ALL reachable paths:
|
|
13
|
+
*
|
|
14
|
+
* "When the static code analyzer finds multiple optional paths in the context
|
|
15
|
+
* of a transaction, it shall consider these multiple optional paths to be
|
|
16
|
+
* part of the same transaction." — AFP §6.5.3
|
|
17
|
+
*
|
|
18
|
+
* Traversal is at METHOD level, never at file level: a domain service holds
|
|
19
|
+
* many writes, and asking about the file would mark everyone importing it as a
|
|
20
|
+
* writer.
|
|
21
|
+
*/
|
|
22
|
+
export type ScopeEntry = {
|
|
23
|
+
file: string;
|
|
24
|
+
member?: string;
|
|
25
|
+
/** hash of the normalised AST — counting-decisions §5 */
|
|
26
|
+
bodyHash: string;
|
|
27
|
+
};
|
|
28
|
+
export type Behavior = {
|
|
29
|
+
writes: boolean;
|
|
30
|
+
/** data stores reached */
|
|
31
|
+
touches: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Declared input fields: `request.validateUsing(x)` resolved down to the
|
|
34
|
+
* fields of the VineJS schema — counting-decisions §7.
|
|
35
|
+
*/
|
|
36
|
+
inputFields: string[];
|
|
37
|
+
trace: TraceStep[];
|
|
38
|
+
/** bodies reached, for `fp:diff` */
|
|
39
|
+
scope: ScopeEntry[];
|
|
40
|
+
unresolved: UnresolvedCall[];
|
|
41
|
+
};
|
|
42
|
+
export type GraphOptions = {
|
|
43
|
+
/** how far to follow from the handler; the default comes from configuration */
|
|
44
|
+
maxDepth?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Custom strategies, added to the built-in ones and ordered by `order`.
|
|
47
|
+
*
|
|
48
|
+
* This is what makes tracing extensible: AdonisJS imposes no organisation
|
|
49
|
+
* pattern, so a project with its own convention registers it here.
|
|
50
|
+
*/
|
|
51
|
+
callResolvers?: CallResolver[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Analyzer with state shared across handlers.
|
|
55
|
+
*
|
|
56
|
+
* The ts-morph `Project` and the symbol cache are expensive to build and
|
|
57
|
+
* identical for every handler of the same application. Creating one per handler
|
|
58
|
+
* multiplies the cost by the number of routes, which is the difference between
|
|
59
|
+
* minutes and seconds on an application of a few hundred routes.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createAnalyzer(app: AppContext, stores: CollectedDataStore[], options?: GraphOptions): {
|
|
62
|
+
analyze: (handler: HandlerRef) => Behavior;
|
|
63
|
+
/** how many files the project loaded — used to prove it does not grow */
|
|
64
|
+
fileCount: () => number;
|
|
65
|
+
};
|
|
66
|
+
/** Convenience for a single handler; for several, use `createAnalyzer`. */
|
|
67
|
+
export declare function analyzeHandler(app: AppContext, stores: CollectedDataStore[], handler: HandlerRef, options?: GraphOptions): Behavior;
|
|
68
|
+
/**
|
|
69
|
+
* Injected dependencies visible in the body: property name -> file.
|
|
70
|
+
*
|
|
71
|
+
* Two forms, both with the type annotated explicitly — `@inject()` does not
|
|
72
|
+
* work without it:
|
|
73
|
+
*
|
|
74
|
+
* constructor(protected billing: BillingService) {}
|
|
75
|
+
* private declare billing: BillingService
|
|
76
|
+
*
|
|
77
|
+
* Since the type is an imported identifier, it resolves through the same path
|
|
78
|
+
* as any import. No type checker is needed.
|
|
79
|
+
*/
|
|
80
|
+
export declare function injectedFor(owner: ClassDeclaration | undefined, file: SourceFile, app: AppContext): Map<string, string>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CallExpression, ClassDeclaration } from 'ts-morph';
|
|
2
|
+
/** Is this call one that cannot reach a data store? */
|
|
3
|
+
export declare function isNoise(call: CallExpression, owner?: ClassDeclaration): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The body-not-found path: a symbol resolved to an application file whose
|
|
6
|
+
* member is not there. For a framework service that is expected — the
|
|
7
|
+
* implementation is in the package — and says nothing about tracing quality.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isNoiseMember(file: string, member?: string): boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inventory layer: extracts raw facts from the application.
|
|
3
|
+
*
|
|
4
|
+
* ARCHITECTURAL RULE: nothing here imports from `src/albrecht/**`. The
|
|
5
|
+
* inventory does not know what an ILF is. That separation is what would allow
|
|
6
|
+
* extracting this layer into its own package if the structural metrics grow.
|
|
7
|
+
*
|
|
8
|
+
* SOURCE PRECEDENCE, most to least reliable:
|
|
9
|
+
* 1. generated artefacts — route registry, generated data schema
|
|
10
|
+
* 2. runtime — router.toJSON(), Lucid metadata
|
|
11
|
+
* 3. AST — call graph, write detection
|
|
12
|
+
* 4. folder convention — report grouping only, never for finding things
|
|
13
|
+
*/
|
|
14
|
+
export * from './resolvers/index.js';
|
|
15
|
+
export type { AppContext } from './app_context.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One canonical spelling for every path the inventory emits.
|
|
3
|
+
*
|
|
4
|
+
* Two path styles meet in this package. ts-morph always returns forward
|
|
5
|
+
* slashes, including on Windows; node's `path.join` returns backslashes there.
|
|
6
|
+
* Both end up in `HandlerRef.file`, and the call graph uses that string as a
|
|
7
|
+
* cache key:
|
|
8
|
+
*
|
|
9
|
+
* const key = `${ref.file}#${ref.member ?? ref.line ?? '*'}`
|
|
10
|
+
*
|
|
11
|
+
* Two spellings of the same file are two keys, so the same body would be
|
|
12
|
+
* analysed twice and pushed twice into the trace and the implementation scope
|
|
13
|
+
* — and a repeated scope entry changes the hash `fp:diff` compares.
|
|
14
|
+
*
|
|
15
|
+
* Forward slashes win because ts-morph cannot be told otherwise, node's `fs`
|
|
16
|
+
* accepts them on Windows, and `path.relative` normalises mixed input anyway.
|
|
17
|
+
* Normalising at the boundary where a path is created costs one call; leaving
|
|
18
|
+
* it to each comparison costs vigilance forever.
|
|
19
|
+
*/
|
|
20
|
+
export declare const toPosix: (value: string) => string;
|
|
21
|
+
/** Compares two paths that may have come from different sources. */
|
|
22
|
+
export declare const samePath: (a: string | undefined, b: string | undefined) => boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CallResolver } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* "Action object" pattern: the transaction delegates to an action instantiated
|
|
4
|
+
* at the call site.
|
|
5
|
+
*
|
|
6
|
+
* await new ExpireInvite().handle({ invite })
|
|
7
|
+
*
|
|
8
|
+
* const mark = new MarkContentChanged()
|
|
9
|
+
* await mark.handle({ documentId })
|
|
10
|
+
*
|
|
11
|
+
* The second form keeps the instance in a local variable, so the declaration
|
|
12
|
+
* has to be followed back to the `new` — that is what `classOfReceiver` does.
|
|
13
|
+
*/
|
|
14
|
+
export declare const actionObjectResolver: CallResolver;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CallResolver } from './types.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Built-in strategies, ordered from most to least specific.
|
|
5
|
+
*
|
|
6
|
+
* They cover the patterns that appear in real AdonisJS applications. No closed
|
|
7
|
+
* list can cover them all — a project with its own convention registers it in
|
|
8
|
+
* `config/function_points.ts`, and it runs before these.
|
|
9
|
+
*/
|
|
10
|
+
export declare const BUILTIN_CALL_RESOLVERS: CallResolver[];
|
|
11
|
+
/**
|
|
12
|
+
* The FIRST strategy that claims a call wins.
|
|
13
|
+
*
|
|
14
|
+
* This is not an implementation detail: syntactically identical shapes carry
|
|
15
|
+
* different meanings. `CreateUserJob.dispatch(p)`, `UserService.create(p)` and
|
|
16
|
+
* `User.find(p)` are all `Identifier.method(args)`, and only ordering tells
|
|
17
|
+
* them apart. Hence specific strategies declare a lower `order` than generic
|
|
18
|
+
* ones, and `module-function` comes last — it would match almost anything.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveCall(call: import('ts-morph').CallExpression, ctx: import('./types.js').ResolverContext, resolvers?: CallResolver[]): {
|
|
21
|
+
by: string;
|
|
22
|
+
refs: import('../../types.js').HandlerRef[];
|
|
23
|
+
} | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CallResolver } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* "Job" pattern: the write happens asynchronously.
|
|
4
|
+
*
|
|
5
|
+
* await CreateUserJob.dispatch({ userId })
|
|
6
|
+
*
|
|
7
|
+
* Runs before `static-service` on purpose: the syntactic shape is identical
|
|
8
|
+
* (`Identifier.method(args)`) and the generic strategy would swallow the job.
|
|
9
|
+
* The distinction is semantic, and it matters because the counting decision
|
|
10
|
+
* differs.
|
|
11
|
+
*
|
|
12
|
+
* COUNTING DECISION: a job dispatched by a handler is followed as part of the
|
|
13
|
+
* SAME transactional function, because IFPUG counts what the user recognises —
|
|
14
|
+
* they click and the effect happens, even if execution is asynchronous. A
|
|
15
|
+
* SCHEDULED job, which nobody dispatches, is a different thing: it is an entry
|
|
16
|
+
* point of its own, and out of v1 scope — only HTTP routes are collected.
|
|
17
|
+
*/
|
|
18
|
+
export declare const jobDispatchResolver: CallResolver;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CallResolver } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* "Module function" pattern: no class at all.
|
|
4
|
+
*
|
|
5
|
+
* await createUser(payload)
|
|
6
|
+
* await syncWithProvider(order)
|
|
7
|
+
*
|
|
8
|
+
* Runs last: it matches any call to an imported identifier and would otherwise
|
|
9
|
+
* swallow the more precise patterns.
|
|
10
|
+
*/
|
|
11
|
+
export declare const moduleFunctionResolver: CallResolver;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CallResolver } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* "Injected dependency" pattern: the call leaves through a class property.
|
|
4
|
+
*
|
|
5
|
+
* @inject()
|
|
6
|
+
* class InvoiceController {
|
|
7
|
+
* constructor(protected billing: BillingService) {}
|
|
8
|
+
* async queue() { await this.billing.enqueue(invoice) }
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* This is the official AdonisJS pattern, and in applications that use it, it is
|
|
12
|
+
* frequently the only path from a route down to a write.
|
|
13
|
+
*
|
|
14
|
+
* **No type checker required.** `@inject()` only works with an explicit type
|
|
15
|
+
* annotation — that annotation is how the container knows what to inject — so
|
|
16
|
+
* the type is always in the AST as an imported identifier.
|
|
17
|
+
*/
|
|
18
|
+
export declare const propertyServiceResolver: CallResolver;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CallResolver } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* "Same class method" pattern: `this.privateMethod()`.
|
|
4
|
+
*
|
|
5
|
+
* async expire(uuid: string) {
|
|
6
|
+
* const invite = await this.findByUuid(uuid)
|
|
7
|
+
* await this.persistExpiration(invite)
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* A public method delegating to private ones of the same class is where writes
|
|
11
|
+
* often live. No other strategy covers it — `property-service` requires
|
|
12
|
+
* `this.dependency.method()`, with two levels of access.
|
|
13
|
+
*
|
|
14
|
+
* Runs before `property-service` because it is more specific: the receiver is
|
|
15
|
+
* exactly `this`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const sameClassMethodResolver: CallResolver;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CallResolver } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* "Static service" pattern: a class method called without instantiating.
|
|
4
|
+
*
|
|
5
|
+
* await UserService.create(payload)
|
|
6
|
+
* await OrderService.finalize(order)
|
|
7
|
+
*
|
|
8
|
+
* Careful: `Order.findByOrFail(...)` has exactly the same syntactic shape. The
|
|
9
|
+
* difference is semantic — a model is a data store, not a body to walk into,
|
|
10
|
+
* and the persistence detector handles it. Hence this resolver depends on
|
|
11
|
+
* `ctx.dataStoresBySymbol` already being populated.
|
|
12
|
+
*/
|
|
13
|
+
export declare const staticServiceResolver: CallResolver;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CallResolver } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* "Transformer" pattern: the package supplies the API, the application
|
|
4
|
+
* supplies the body.
|
|
5
|
+
*
|
|
6
|
+
* class InviteTransformer extends BaseTransformer<Invite> {
|
|
7
|
+
* toObject() { … }
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* InviteTransformer.transform(invite)
|
|
11
|
+
*
|
|
12
|
+
* `transform()` and `paginate()` live in `@adonisjs/core`, so resolving the
|
|
13
|
+
* symbol lands on the application file and finds no body there. The naive
|
|
14
|
+
* reading is that the tracer must step into node_modules; it does not. Those
|
|
15
|
+
* methods call BACK into `toObject()`, which the application writes, so the
|
|
16
|
+
* body worth analysing was in the application all along.
|
|
17
|
+
*
|
|
18
|
+
* It is the same shape as `job-dispatch`, where `dispatch` enqueues and
|
|
19
|
+
* `handle` executes.
|
|
20
|
+
*
|
|
21
|
+
* COUNTING DECISION: the write a transformer performs belongs to the
|
|
22
|
+
* transaction that serialised through it. Without this, a table written only
|
|
23
|
+
* inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
|
|
24
|
+
*/
|
|
25
|
+
export declare const transformerResolver: CallResolver;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CallExpression, SourceFile } from 'ts-morph';
|
|
2
|
+
import type { DataStore, HandlerRef } from '../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* AdonisJS does not impose a code organisation. The same transaction can be
|
|
5
|
+
* written in very different ways:
|
|
6
|
+
*
|
|
7
|
+
* fat controller await User.create(payload)
|
|
8
|
+
* action object await new CreateUser().handle(payload)
|
|
9
|
+
* static service await UserService.create(payload)
|
|
10
|
+
* injected service await this.users.create(payload)
|
|
11
|
+
* repository await this.repo.persist(user)
|
|
12
|
+
* job await CreateUserJob.dispatch(payload)
|
|
13
|
+
* query builder await db.table('users').insert(payload)
|
|
14
|
+
*
|
|
15
|
+
* No closed list covers that. Tracing is therefore assembled from registrable
|
|
16
|
+
* strategies, and whatever none of them resolves is REPORTED — never silently
|
|
17
|
+
* treated as a read.
|
|
18
|
+
*
|
|
19
|
+
* That is the most important contract in the package: saying "I don't know" is
|
|
20
|
+
* preferable to producing a number that looks right.
|
|
21
|
+
*/
|
|
22
|
+
export type ResolverContext = {
|
|
23
|
+
/** file containing the call site */
|
|
24
|
+
file: SourceFile;
|
|
25
|
+
/** current depth in the call graph */
|
|
26
|
+
depth: number;
|
|
27
|
+
/** file imports: local identifier -> resolved absolute path */
|
|
28
|
+
imports: Map<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* Injected dependencies visible in this body: property name -> class file.
|
|
31
|
+
* For example `billing` -> `.../billing_service.ts`.
|
|
32
|
+
*
|
|
33
|
+
* No type checker needed: AdonisJS `@inject()` **requires** an explicit type
|
|
34
|
+
* annotation for the container to resolve the dependency, so
|
|
35
|
+
* `constructor(protected billing: BillingService)` always carries the
|
|
36
|
+
* type as an identifier — imported like any other.
|
|
37
|
+
*/
|
|
38
|
+
injected: Map<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* Known data stores, keyed by symbol name (e.g. 'User').
|
|
41
|
+
*
|
|
42
|
+
* ORDERING INVARIANT: data stores are collected BEFORE any handler analysis.
|
|
43
|
+
* Without that, `UserService.create()` and `User.create()` are
|
|
44
|
+
* indistinguishable by shape, and a resolver would walk into the model as if
|
|
45
|
+
* it were business code.
|
|
46
|
+
*/
|
|
47
|
+
dataStoresBySymbol: Map<string, DataStore>;
|
|
48
|
+
/** resolves an AdonisJS specifier (`#collect/models/invite`) to a path */
|
|
49
|
+
resolveSpecifier(specifier: string): string | null;
|
|
50
|
+
/** loads a file into the project, if it exists */
|
|
51
|
+
sourceFile(absPath: string): SourceFile | null;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Follows a call site to the next body to analyse.
|
|
55
|
+
*
|
|
56
|
+
* Returns `[]` when the strategy does not recognise the call — that is not an
|
|
57
|
+
* error, another strategy may recognise it. Only when none does should the call
|
|
58
|
+
* land in `unresolved`.
|
|
59
|
+
*/
|
|
60
|
+
export interface CallResolver {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
/** lower runs first; specific strategies before generic ones */
|
|
63
|
+
readonly order?: number;
|
|
64
|
+
resolve(call: CallExpression, ctx: ResolverContext): HandlerRef[];
|
|
65
|
+
}
|