@filipebraida/adonis-function-points 0.9.0 → 0.11.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/CHANGELOG.md +93 -0
- package/README.md +1 -1
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-DJspMFHc.js → fp_calibrate-U5xdt5Qe.js} +1 -1
- package/build/{fp_count-Bjviq8dz.js → fp_count-mDQgQeEq.js} +1 -1
- package/build/{fp_diff-BSK1kKLQ.js → fp_diff-BnWi2Fx8.js} +1 -1
- package/build/{fp_explain-CrFcMeXU.js → fp_explain-CQESTS5W.js} +1 -1
- package/build/{fp_inventory-B4Pv8Thz.js → fp_inventory-D3_6QCD2.js} +1 -1
- package/build/{fp_metrics-DoS5riGr.js → fp_metrics-lnnhnVaw.js} +1 -1
- package/build/index.js +2 -2
- package/build/{pipeline-DqPrDZfD.js → pipeline-CdDJRsLM.js} +388 -36
- package/build/{resolvers-CneCj3sT.js → resolvers-Bz1F-Tgf.js} +1 -1
- package/build/{runners-BuZNr-FE.js → runners-CzzlX7h-.js} +4 -3
- package/build/src/albrecht/counter.d.ts +3 -2
- package/build/src/cli.js +2 -2
- package/build/src/inventory/graph/call_graph.d.ts +16 -0
- package/build/src/inventory/graph/noise.d.ts +4 -1
- package/build/src/inventory/resolvers/index.js +1 -1
- package/build/src/inventory/sources/data_stores.d.ts +7 -0
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,99 @@ release moves the number for unchanged code, the rule set version moves with it
|
|
|
6
6
|
otherwise the difference would measure the tool's change rather than the work, and
|
|
7
7
|
that difference becomes an invoice.
|
|
8
8
|
|
|
9
|
+
## 0.11.0
|
|
10
|
+
|
|
11
|
+
Rule set unchanged (`afp@1.9.0`); no number moves. What changes is what the **coverage**
|
|
12
|
+
means: it measures what of the application the walk could not follow, and nothing else.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **A call into a package is outside the boundary — noted, not an unresolved call.** An
|
|
17
|
+
authorisation service from a package injected into the application's façade, a variant of an
|
|
18
|
+
attachment whose column type is a package's, a content collection built by a package factory,
|
|
19
|
+
a mailer method inherited from a package base: none is code the analysis should follow, so none
|
|
20
|
+
is a gap it failed to follow. What a package does with its own tables is technical (§4); what it
|
|
21
|
+
hands back is a value. Read structurally — the injected property's declared type, the model
|
|
22
|
+
property's declared type, the exported const's factory, the class's base, each resolving to a
|
|
23
|
+
specifier the application does not — never by package name. One note per site in
|
|
24
|
+
`inventory.notes`, naming the transactions that reach it; behind the same noise gate as an
|
|
25
|
+
unresolved call, so what was silent stays silent. A receiver typed by the application stays a
|
|
26
|
+
gap. Fixture `fronteira_pacote` (23 FP before and after; four unresolved calls become three
|
|
27
|
+
notes and the one gap of the application).
|
|
28
|
+
- **A `Map` or `Set` that arrives through the constructor, one level down a named type**
|
|
29
|
+
(`this.extras?.panel?.get(id)` with `extras?: Extras`, `Extras = { panel: Map<…> }`) is noise,
|
|
30
|
+
as `private names = new Map()` already was. Fixture `patterns/native_receivers`.
|
|
31
|
+
- **A command's `@flags.*` / `@args.*` property is a string, a number or a boolean**: a method on
|
|
32
|
+
it (`this.name.trim()`) is the language's, not a gap.
|
|
33
|
+
- **A method of an interface says so**: "interface method: the implementation is injected at
|
|
34
|
+
runtime and cannot be followed statically" — a gap, with the right words, where "probably
|
|
35
|
+
inherited from a package class" pointed the reader at the wrong place.
|
|
36
|
+
- **A local mixin factory whose returned class declares no `@column`** adds behaviour, not
|
|
37
|
+
attributes: a note. One that declares a column stays a gap — reading its columns off the
|
|
38
|
+
returned class is a rule for a release that moves numbers.
|
|
39
|
+
|
|
40
|
+
On the three validated applications, unresolved calls go from 13 / 8 / 12 to **0 / 0 / 1** —
|
|
41
|
+
the one an interface method, which is a gap of the application — with no point moved.
|
|
42
|
+
|
|
43
|
+
### Documented
|
|
44
|
+
|
|
45
|
+
- counting-decisions §4 gains "A call into a package is outside the boundary".
|
|
46
|
+
|
|
47
|
+
## 0.10.1
|
|
48
|
+
|
|
49
|
+
Rule set unchanged (`afp@1.9.0`); no number moves.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **A base class or a mixin from a package is a fact about the declaration, not an
|
|
54
|
+
unresolved call.** `compose(Base, Auditable)`, `withAuthFinder(…)` inline or through a local
|
|
55
|
+
const (which came out as "base class not found in the application" — code that is in the
|
|
56
|
+
application, built by a package's factory). Nobody follows a mixin, and in function points
|
|
57
|
+
it is nothing: what it adds is technical, never a user-recognisable attribute; the model's
|
|
58
|
+
own columns and the schema decide. Listed as a **note** by `fp:inventory` and in
|
|
59
|
+
`inventory.notes`, kept out of the coverage number. A base of the application that was not
|
|
60
|
+
found stays an unresolved call.
|
|
61
|
+
|
|
62
|
+
## 0.10.0
|
|
63
|
+
|
|
64
|
+
**Rule set `afp@1.9.0`.** One rule and two report fixes, from a team's third review of their
|
|
65
|
+
own count — the one they accepted as a baseline. The rule moves no point on the three
|
|
66
|
+
validated applications and no point on its fixture: it moves a gap from the wrong line to
|
|
67
|
+
the right one. A 0.9.0 baseline still has to be recounted, because the rule set says so.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **A warning names a transaction the way the table and the config keys do.** Four warnings
|
|
72
|
+
printed the route's own pattern (`GET /orders/:id`) where the table, `fp:explain` and the
|
|
73
|
+
keys of `overrides` / `boundary.ignoreEntryPoints` use the identity (`GET /orders/:param`,
|
|
74
|
+
§5). Copying from a warning into the configuration required a translation nobody was told
|
|
75
|
+
about. All of them print the identity now.
|
|
76
|
+
- **"leaves whole — page never reads X" contradicted itself.** The rule was right — a store
|
|
77
|
+
handed to a page that uses nothing of it the reader can see leaves whole, in the open, never
|
|
78
|
+
as a floor — and the sentence was not. It says now: "handed to page "x", and nothing the
|
|
79
|
+
reader can see uses X: every column counted, in the open".
|
|
80
|
+
|
|
81
|
+
### Counting
|
|
82
|
+
|
|
83
|
+
- **The value of a transaction callback is what it returns.** `const { row } = await
|
|
84
|
+
db.transaction(async (trx) => { … return { row: created } })`, then `row.save()`: the write
|
|
85
|
+
was an unreadable receiver. The callback is a body — its own locals are bound first, in
|
|
86
|
+
source order — and every `return` names the store the value holds, whole or under one key
|
|
87
|
+
of a returned literal; `Model.transaction` and `trx.transaction` are the same shape. A
|
|
88
|
+
returned number binds nothing; a raw-query row nobody can type stays reported. On the three
|
|
89
|
+
applications the value of a transaction callback is used 17 times. The 0.8 rule "a value a
|
|
90
|
+
package built is not a store" narrows to a call with no function argument: `db` is a
|
|
91
|
+
package import, and `db.transaction(cb)` hands back the application's value.
|
|
92
|
+
|
|
93
|
+
### Documented
|
|
94
|
+
|
|
95
|
+
- counting-decisions §3 gains the transaction-callback row of the bindings table.
|
|
96
|
+
- Fixtures: `transacao_valor` (18 FP — the same before and after; one unresolved call moves
|
|
97
|
+
from `row.save` to the raw-query row that deserves it); `inertia_pages` grows a page that
|
|
98
|
+
uses nothing of its rows (29 FP).
|
|
99
|
+
- Measured and set aside: a model's instance method that writes — one in 52 models on the
|
|
100
|
+
three applications, not a rule.
|
|
101
|
+
|
|
9
102
|
## 0.9.0
|
|
10
103
|
|
|
11
104
|
**Rule set `afp@1.8.0`.** Two rules from a team's second review of their own count, one
|
package/README.md
CHANGED
package/build/commands/main.js
CHANGED
|
@@ -14,27 +14,27 @@
|
|
|
14
14
|
const commands = [
|
|
15
15
|
{
|
|
16
16
|
commandName: "fp:inventory",
|
|
17
|
-
importer: () => import("../fp_inventory-
|
|
17
|
+
importer: () => import("../fp_inventory-D3_6QCD2.js")
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
commandName: "fp:metrics",
|
|
21
|
-
importer: () => import("../fp_metrics-
|
|
21
|
+
importer: () => import("../fp_metrics-lnnhnVaw.js")
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
commandName: "fp:count",
|
|
25
|
-
importer: () => import("../fp_count-
|
|
25
|
+
importer: () => import("../fp_count-mDQgQeEq.js")
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
commandName: "fp:explain",
|
|
29
|
-
importer: () => import("../fp_explain-
|
|
29
|
+
importer: () => import("../fp_explain-CQESTS5W.js")
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
commandName: "fp:diff",
|
|
33
|
-
importer: () => import("../fp_diff-
|
|
33
|
+
importer: () => import("../fp_diff-BnWi2Fx8.js")
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
commandName: "fp:calibrate",
|
|
37
|
-
importer: () => import("../fp_calibrate-
|
|
37
|
+
importer: () => import("../fp_calibrate-U5xdt5Qe.js")
|
|
38
38
|
}
|
|
39
39
|
];
|
|
40
40
|
let cache = null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as printResult, t as runCalibrate } from "./runners-
|
|
1
|
+
import { s as printResult, t as runCalibrate } from "./runners-CzzlX7h-.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, args } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_calibrate.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as runCount, s as printResult } from "./runners-
|
|
1
|
+
import { n as runCount, s as printResult } from "./runners-CzzlX7h-.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, flags } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_count.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as runDiff, s as printResult } from "./runners-
|
|
1
|
+
import { r as runDiff, s as printResult } from "./runners-CzzlX7h-.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, args } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_diff.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as runExplain, s as printResult } from "./runners-
|
|
1
|
+
import { i as runExplain, s as printResult } from "./runners-CzzlX7h-.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, args } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_explain.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as runInventory, s as printResult } from "./runners-
|
|
1
|
+
import { a as runInventory, s as printResult } from "./runners-CzzlX7h-.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, flags } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_inventory.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as runMetrics, s as printResult } from "./runners-
|
|
1
|
+
import { o as runMetrics, s as printResult } from "./runners-CzzlX7h-.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, flags } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_metrics.ts
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { configure } from "./configure.js";
|
|
2
2
|
import { a as AEP_FACTORS, c as IncomparableSourcesError, f as defineConfig, i as measureStructure, l as SISP_FACTORS, n as parseSamples, o as FACTOR_PRESETS, r as measureConformance, s as IncomparableRulesetsError, t as calibrate, u as diffCounts } from "./calibration-DVIf8hcE.js";
|
|
3
3
|
import "./src/types.js";
|
|
4
|
-
import { n as ignoreCalls, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-
|
|
5
|
-
import { a as DEFAULT_TECHNICAL_PATTERNS, i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-
|
|
4
|
+
import { n as ignoreCalls, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-Bz1F-Tgf.js";
|
|
5
|
+
import { a as DEFAULT_TECHNICAL_PATTERNS, i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-CdDJRsLM.js";
|
|
6
6
|
export { AEP_FACTORS, BUILTIN_CALL_RESOLVERS, CoverageTooLowError, DEFAULT_TECHNICAL_PATTERNS, FACTOR_PRESETS, IncomparableRulesetsError, IncomparableSourcesError, RULESET, RULESET_VERSION, SISP_FACTORS, analyze, calibrate, configure, defineConfig, diffCounts, ignoreCalls, measureConformance, measureStructure, parseSamples };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as isSeeder, a as chainShapeOf, b as toPosix, c as transformerResourceOf, d as EXECUTION_METHODS, f as collectEventBindings, g as isApplicationCode, h as rootSymbolOf, i as resolveCall, l as unwrap$1, m as hooksFiredBy, o as mappedLiteralOf, p as detectAccess, r as isTechnicalWrite, s as outputFieldsIn, t as BUILTIN_CALL_RESOLVERS, u as DISPATCH_METHODS, v as relativeTo, y as samePath } from "./resolvers-Bz1F-Tgf.js";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path, { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
@@ -384,6 +384,7 @@ async function collectDataStores(app) {
|
|
|
384
384
|
if (app.generated.dataSchema) project.addSourceFileAtPathIfExists(app.generated.dataSchema);
|
|
385
385
|
const candidates = [];
|
|
386
386
|
const unresolved = [];
|
|
387
|
+
const notes = [];
|
|
387
388
|
/**
|
|
388
389
|
* Classes appearing as an ANCESTOR of some model.
|
|
389
390
|
*
|
|
@@ -395,7 +396,7 @@ async function collectDataStores(app) {
|
|
|
395
396
|
for (const file of project.getSourceFiles()) {
|
|
396
397
|
if (samePath(file.getFilePath(), app.generated.dataSchema)) continue;
|
|
397
398
|
for (const cls of file.getClasses()) {
|
|
398
|
-
const described = describeStore(cls, app, project, unresolved);
|
|
399
|
+
const described = describeStore(cls, app, project, unresolved, notes);
|
|
399
400
|
if (!described) continue;
|
|
400
401
|
candidates.push(described.store);
|
|
401
402
|
for (const ancestor of described.ancestors) ancestors.add(ancestor);
|
|
@@ -403,18 +404,20 @@ async function collectDataStores(app) {
|
|
|
403
404
|
}
|
|
404
405
|
return {
|
|
405
406
|
stores: candidates.filter((store) => !ancestors.has(store.id)).sort(byName),
|
|
406
|
-
unresolved
|
|
407
|
+
unresolved,
|
|
408
|
+
notes
|
|
407
409
|
};
|
|
408
410
|
}
|
|
409
411
|
/** stable identity of a class, to separate a base from a store */
|
|
410
412
|
const classKey = (cls) => `${cls.getSourceFile().getFilePath()}#${cls.getName()}`;
|
|
411
413
|
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
412
|
-
function describeStore(cls, app, project, unresolved) {
|
|
414
|
+
function describeStore(cls, app, project, unresolved, notes) {
|
|
413
415
|
const name = cls.getName();
|
|
414
416
|
if (!name) return null;
|
|
415
417
|
const chain = walkChain(cls, app, project);
|
|
416
418
|
if (!chain.reachesLucid) return null;
|
|
417
419
|
unresolved.push(...chain.unresolved);
|
|
420
|
+
notes.push(...chain.notes);
|
|
418
421
|
const file = cls.getSourceFile().getFilePath();
|
|
419
422
|
return {
|
|
420
423
|
store: {
|
|
@@ -455,6 +458,7 @@ function walkChain(start, app, project) {
|
|
|
455
458
|
* the coverage report.
|
|
456
459
|
*/
|
|
457
460
|
const unresolved = [];
|
|
461
|
+
const notes = [];
|
|
458
462
|
let reachesLucid = false;
|
|
459
463
|
let columnSource = "ast";
|
|
460
464
|
const visit = (cls) => {
|
|
@@ -475,6 +479,31 @@ function walkChain(start, app, project) {
|
|
|
475
479
|
visit(resolved);
|
|
476
480
|
continue;
|
|
477
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* A base or a mixin from a PACKAGE — `compose(Base, Auditable)` imported from
|
|
484
|
+
* outside, `withAuthFinder(…)` inline or through a local const — is a fact about
|
|
485
|
+
* the declaration, not a gap in the walk: nobody follows a mixin. What it adds is
|
|
486
|
+
* technical (a hashed password, audit stamps) and never a user-recognisable
|
|
487
|
+
* attribute (§7.2); the model's own columns and the schema decide. Noted, and kept
|
|
488
|
+
* out of the coverage number. A base of the application that was not found stays
|
|
489
|
+
* a gap.
|
|
490
|
+
*/
|
|
491
|
+
/**
|
|
492
|
+
* A mixin factory OF THE APPLICATION whose returned class declares no `@column`
|
|
493
|
+
* adds methods, not attributes: a fact, not a gap. One that declares columns
|
|
494
|
+
* stays a gap — reading them off the returned class is a rule for a release
|
|
495
|
+
* that moves numbers (plan 0.11 §E).
|
|
496
|
+
*/
|
|
497
|
+
const columnless = localFactoryWithoutColumns(parent, cls.getSourceFile(), app, project);
|
|
498
|
+
if (columnless) {
|
|
499
|
+
notes.push(`${cls.getName() ?? "?"}: local mixin factory ${columnless} declares no column — adds behaviour, not attributes`);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const packaged = packageOriginOf(parent, cls.getSourceFile(), app);
|
|
503
|
+
if (packaged) {
|
|
504
|
+
notes.push(`${cls.getName() ?? "?"}: base or mixin from ${packaged.specifier} (${packaged.expression}) — technical, adds no user-recognisable attribute; the model's own columns and the schema decide`);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
478
507
|
unresolved.push({
|
|
479
508
|
file: cls.getSourceFile().getFilePath(),
|
|
480
509
|
line: parent.getStartLineNumber(),
|
|
@@ -489,10 +518,77 @@ function walkChain(start, app, project) {
|
|
|
489
518
|
classes,
|
|
490
519
|
attributes: [...attributes.values()],
|
|
491
520
|
columnSource,
|
|
492
|
-
unresolved
|
|
521
|
+
unresolved,
|
|
522
|
+
notes
|
|
493
523
|
};
|
|
494
524
|
}
|
|
495
525
|
/**
|
|
526
|
+
* `compose(Base, withTracking())` where `withTracking` is a function of the application
|
|
527
|
+
* whose body returns a class expression: the factory's name when that class declares no
|
|
528
|
+
* `@column`, `null` when it declares one (or when the factory cannot be read).
|
|
529
|
+
*/
|
|
530
|
+
function localFactoryWithoutColumns(parent, file, app, project) {
|
|
531
|
+
if (!Node.isCallExpression(parent)) return null;
|
|
532
|
+
const callee = parent.getExpression();
|
|
533
|
+
if (!Node.isIdentifier(callee)) return null;
|
|
534
|
+
const name = callee.getText();
|
|
535
|
+
let declared = file.getFunction(name);
|
|
536
|
+
if (!declared) {
|
|
537
|
+
const origin = originOf$1(name, file);
|
|
538
|
+
const target = origin ? app.resolveSpecifier(origin.specifier) : null;
|
|
539
|
+
declared = (target ? project.getSourceFile(target) ?? project.addSourceFileAtPathIfExists(target) : null)?.getFunction(origin?.exportedName === "default" ? name : origin?.exportedName ?? name) ?? void 0;
|
|
540
|
+
}
|
|
541
|
+
if (!declared) return null;
|
|
542
|
+
return [...declared.getDescendantsOfKind(SyntaxKind.ClassExpression), ...declared.getDescendantsOfKind(SyntaxKind.ClassDeclaration)].some((c) => c.getProperties().some((p) => p.getDecorators().some((d) => /^column\b/.test(d.getFullName())))) ? null : `${name}()`;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Where a base or a mixin comes from, when it comes from a package: the class
|
|
546
|
+
* imported from a specifier the application does not resolve, a factory call whose
|
|
547
|
+
* callee is, or a local const initialised by such a call.
|
|
548
|
+
*/
|
|
549
|
+
function packageOriginOf(parent, file, app) {
|
|
550
|
+
const fromPackage = (local) => {
|
|
551
|
+
const origin = originOf$1(local, file);
|
|
552
|
+
return origin && !app.resolveSpecifier(origin.specifier) ? origin.specifier : null;
|
|
553
|
+
};
|
|
554
|
+
const rootOf = (node) => {
|
|
555
|
+
let current = node;
|
|
556
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
557
|
+
if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
|
|
558
|
+
current = current.getExpression();
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
return Node.isIdentifier(current) ? current.getText() : null;
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
};
|
|
565
|
+
const brief = (text) => text.replace(/\s+/g, " ").slice(0, 60);
|
|
566
|
+
if (Node.isCallExpression(parent)) {
|
|
567
|
+
const root = rootOf(parent);
|
|
568
|
+
const specifier = root ? fromPackage(root) : null;
|
|
569
|
+
return specifier ? {
|
|
570
|
+
specifier,
|
|
571
|
+
expression: brief(parent.getText())
|
|
572
|
+
} : null;
|
|
573
|
+
}
|
|
574
|
+
if (!Node.isIdentifier(parent)) return null;
|
|
575
|
+
const direct = fromPackage(parent.getText());
|
|
576
|
+
if (direct) return {
|
|
577
|
+
specifier: direct,
|
|
578
|
+
expression: parent.getText()
|
|
579
|
+
};
|
|
580
|
+
const initializer = file.getVariableDeclaration(parent.getText())?.getInitializer();
|
|
581
|
+
if (initializer && Node.isCallExpression(initializer)) {
|
|
582
|
+
const root = rootOf(initializer);
|
|
583
|
+
const specifier = root ? fromPackage(root) : null;
|
|
584
|
+
if (specifier) return {
|
|
585
|
+
specifier,
|
|
586
|
+
expression: `${parent.getText()} = ${brief(initializer.getText())}`
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
496
592
|
* Why the base class could not be resolved.
|
|
497
593
|
*
|
|
498
594
|
* The right reason matters as much as the fact: saying "outside the
|
|
@@ -1193,8 +1289,7 @@ const FRAMEWORK_SERVICES = new Set([
|
|
|
1193
1289
|
"app",
|
|
1194
1290
|
"prompt"
|
|
1195
1291
|
]);
|
|
1196
|
-
|
|
1197
|
-
function isNoise(call, owner) {
|
|
1292
|
+
function isNoise(call, owner, membersOf) {
|
|
1198
1293
|
const expression = call.getExpression();
|
|
1199
1294
|
if (!Node.isPropertyAccessExpression(expression)) return false;
|
|
1200
1295
|
if (NEVER_DATA_METHODS.has(expression.getName())) return true;
|
|
@@ -1207,7 +1302,7 @@ function isNoise(call, owner) {
|
|
|
1207
1302
|
* than a bare identifier.
|
|
1208
1303
|
*/
|
|
1209
1304
|
if (Node.isPropertyAccessExpression(receiver) && Node.isThisExpression(receiver.getExpression()) && FRAMEWORK_SERVICES.has(receiver.getName())) return true;
|
|
1210
|
-
return isNativeReceiver(receiver, owner);
|
|
1305
|
+
return isNativeReceiver(receiver, owner, membersOf);
|
|
1211
1306
|
}
|
|
1212
1307
|
/**
|
|
1213
1308
|
* Iteration over a list, checked BEFORE the resolvers run.
|
|
@@ -1233,14 +1328,40 @@ function isIteration(method, call) {
|
|
|
1233
1328
|
return first !== void 0 && (Node.isArrowFunction(first) || Node.isFunctionExpression(first));
|
|
1234
1329
|
}
|
|
1235
1330
|
/** Does the receiver resolve to a built-in, by its declaration? */
|
|
1236
|
-
function isNativeReceiver(receiver, owner) {
|
|
1331
|
+
function isNativeReceiver(receiver, owner, membersOf) {
|
|
1237
1332
|
if (Node.isArrayLiteralExpression(receiver) || Node.isStringLiteral(receiver)) return true;
|
|
1238
|
-
if (Node.isPropertyAccessExpression(receiver)
|
|
1333
|
+
if (Node.isPropertyAccessExpression(receiver)) {
|
|
1239
1334
|
const inner = receiver.getExpression();
|
|
1240
|
-
if (Node.isThisExpression(inner) && owner) return isNativeProperty(owner, receiver.getName());
|
|
1335
|
+
if (Node.isThisExpression(inner) && owner) return isNativeProperty(owner, receiver.getName()) || isPrimitiveInput(owner, receiver.getName());
|
|
1336
|
+
/**
|
|
1337
|
+
* `this.extras?.painel?.get(id)` where `constructor(protected extras?: Extras)` and
|
|
1338
|
+
* `type Extras = { painel: Map<number, Row> }`: the lookups a transformer receives
|
|
1339
|
+
* arrive one level down, through a named type. The caller resolves the type; here
|
|
1340
|
+
* only its member's type is read.
|
|
1341
|
+
*/
|
|
1342
|
+
if (Node.isPropertyAccessExpression(inner) && Node.isThisExpression(inner.getExpression()) && owner && membersOf) {
|
|
1343
|
+
const typeNode = declaredOn(owner, inner.getName())?.getTypeNode();
|
|
1344
|
+
if (!typeNode) return false;
|
|
1345
|
+
const memberType = membersOf(typeNode).get(receiver.getName());
|
|
1346
|
+
return !!memberType && NATIVE_TYPES.has(memberType.replace(/<.*/, "").trim());
|
|
1347
|
+
}
|
|
1241
1348
|
}
|
|
1242
1349
|
return false;
|
|
1243
1350
|
}
|
|
1351
|
+
/** a class property or a constructor parameter property by name */
|
|
1352
|
+
function declaredOn(owner, name) {
|
|
1353
|
+
return owner.getProperty(name) ?? owner.getConstructors()[0]?.getParameters().find((parameter) => parameter.getName() === name);
|
|
1354
|
+
}
|
|
1355
|
+
/** an ace command's `@flags.*` / `@args.*` property is a string, a number or a boolean: any method on it is the language's */
|
|
1356
|
+
function isPrimitiveInput(owner, name) {
|
|
1357
|
+
const property = owner.getProperty(name);
|
|
1358
|
+
if (!property) return false;
|
|
1359
|
+
return property.getDecorators().some((decorator) => {
|
|
1360
|
+
const callee = decorator.getCallExpression()?.getExpression();
|
|
1361
|
+
const root = callee && Node.isPropertyAccessExpression(callee) ? callee.getExpression().getText() : "";
|
|
1362
|
+
return root === "flags" || root === "args";
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1244
1365
|
function isNativeProperty(owner, name) {
|
|
1245
1366
|
/**
|
|
1246
1367
|
* Both shapes declare a field: a class property, and a constructor parameter
|
|
@@ -2168,7 +2289,7 @@ function readPages(deliveries, env) {
|
|
|
2168
2289
|
for (const { store } of readable) {
|
|
2169
2290
|
if (reading.columns.has(store) || reading.unreadable.has(store)) continue;
|
|
2170
2291
|
const members = [...reading.unknownMembers.get(store) ?? []];
|
|
2171
|
-
markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `page "${page}"
|
|
2292
|
+
markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `handed to page "${page}", and nothing the reader can see uses ${store}: every column counted, in the open`);
|
|
2172
2293
|
}
|
|
2173
2294
|
}
|
|
2174
2295
|
return reading;
|
|
@@ -3065,6 +3186,59 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3065
3186
|
}
|
|
3066
3187
|
return found.size === 1 ? [...found][0] : null;
|
|
3067
3188
|
};
|
|
3189
|
+
/**
|
|
3190
|
+
* `await db.transaction(async (trx) => { … return … })` — also `Model.transaction`
|
|
3191
|
+
* and `trx.transaction` (a savepoint): a body whose VALUE is what it returns. The
|
|
3192
|
+
* callback's own locals are bound first, in source order, so `return { row: created }`
|
|
3193
|
+
* can be read; then every return names one store, or nothing binds (plan 0.10 §B).
|
|
3194
|
+
*/
|
|
3195
|
+
const transactionCallbackOf = (call) => {
|
|
3196
|
+
const callee = call.getExpression();
|
|
3197
|
+
if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "transaction") return null;
|
|
3198
|
+
return call.getArguments().find((a) => Node.isArrowFunction(a) || Node.isFunctionExpression(a)) ?? null;
|
|
3199
|
+
};
|
|
3200
|
+
const returnsOf = (callback) => {
|
|
3201
|
+
if (!Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) return [];
|
|
3202
|
+
const callbackBody = callback.getBody();
|
|
3203
|
+
if (!Node.isBlock(callbackBody)) return [callbackBody];
|
|
3204
|
+
const returned = [];
|
|
3205
|
+
for (const statement of callbackBody.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
3206
|
+
const owner = statement.getFirstAncestor((n) => Node.isArrowFunction(n) || Node.isFunctionExpression(n) || Node.isFunctionDeclaration(n) || Node.isMethodDeclaration(n));
|
|
3207
|
+
const expression = statement.getExpression();
|
|
3208
|
+
if (owner === callback && expression) returned.push(expression);
|
|
3209
|
+
}
|
|
3210
|
+
return returned;
|
|
3211
|
+
};
|
|
3212
|
+
const bindCallbackLocals = (callback) => {
|
|
3213
|
+
for (const declaration of callback.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
3214
|
+
const nameNode = declaration.getNameNode();
|
|
3215
|
+
const initializer = declaration.getInitializer();
|
|
3216
|
+
if (!initializer || !Node.isIdentifier(nameNode)) continue;
|
|
3217
|
+
const store = storeOfValue(initializer);
|
|
3218
|
+
if (store) symbols.set(nameNode.getText(), store);
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
/** the store every return of the callback names — whole, or under one key of a returned literal */
|
|
3222
|
+
const transactionValueStore = (callback, key) => {
|
|
3223
|
+
bindCallbackLocals(callback);
|
|
3224
|
+
const returned = returnsOf(callback);
|
|
3225
|
+
if (returned.length === 0) return null;
|
|
3226
|
+
const found = /* @__PURE__ */ new Set();
|
|
3227
|
+
for (const expression of returned) {
|
|
3228
|
+
let value = unwrapAwait(expression);
|
|
3229
|
+
if (key) {
|
|
3230
|
+
if (!Node.isObjectLiteralExpression(value)) return null;
|
|
3231
|
+
const property = value.getProperty(key);
|
|
3232
|
+
if (!property) return null;
|
|
3233
|
+
value = Node.isPropertyAssignment(property) ? property.getInitializer() : Node.isShorthandPropertyAssignment(property) ? property.getNameNode() : void 0;
|
|
3234
|
+
if (!value) return null;
|
|
3235
|
+
}
|
|
3236
|
+
const store = storeOfValue(value);
|
|
3237
|
+
if (!store) return null;
|
|
3238
|
+
found.add(store);
|
|
3239
|
+
}
|
|
3240
|
+
return found.size === 1 ? [...found][0] : null;
|
|
3241
|
+
};
|
|
3068
3242
|
const isAuthUser = (node) => {
|
|
3069
3243
|
if (!authUserStore) return false;
|
|
3070
3244
|
const chain = Node.isCallExpression(node) ? node.getExpression() : node;
|
|
@@ -3097,6 +3271,8 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3097
3271
|
if (Node.isPropertyAccessExpression(node)) return storeOfExpression(node, symbols, relationsByStore);
|
|
3098
3272
|
if (Node.isCallExpression(node)) {
|
|
3099
3273
|
const callee = node.getExpression();
|
|
3274
|
+
const callback = transactionCallbackOf(node);
|
|
3275
|
+
if (callback) return transactionValueStore(callback);
|
|
3100
3276
|
if (Node.isPropertyAccessExpression(callee)) {
|
|
3101
3277
|
/**
|
|
3102
3278
|
* `q.forUpdate()`, `q.where(…)`: a query-builder chain hands the same rows on —
|
|
@@ -3144,6 +3320,14 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3144
3320
|
*/
|
|
3145
3321
|
const store = storeOfValue(initializer);
|
|
3146
3322
|
if (store) symbols.set(nameNode.getText(), store);
|
|
3323
|
+
} else if (Node.isObjectBindingPattern(nameNode)) {
|
|
3324
|
+
const call = unwrapAwait(initializer);
|
|
3325
|
+
const callback = Node.isCallExpression(call) ? transactionCallbackOf(call) : null;
|
|
3326
|
+
if (!callback) return;
|
|
3327
|
+
for (const element of nameNode.getElements()) {
|
|
3328
|
+
const store = transactionValueStore(callback, element.getPropertyNameNode()?.getText() ?? element.getName());
|
|
3329
|
+
if (store) symbols.set(element.getName(), store);
|
|
3330
|
+
}
|
|
3147
3331
|
}
|
|
3148
3332
|
return;
|
|
3149
3333
|
}
|
|
@@ -3189,6 +3373,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3189
3373
|
const accesses = [];
|
|
3190
3374
|
const followUps = [];
|
|
3191
3375
|
const unresolved = [];
|
|
3376
|
+
const packageCalls = [];
|
|
3192
3377
|
const reads = [];
|
|
3193
3378
|
/** calls a strategy claimed, and where they lead: a nested transformer's keys arrive through its body */
|
|
3194
3379
|
const followedCalls = /* @__PURE__ */ new Map();
|
|
@@ -3308,7 +3493,14 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3308
3493
|
* counted as EOs under a coverage of 99.5%, because a write on an unknown
|
|
3309
3494
|
* local was dropped without a word. It lowers coverage and is named.
|
|
3310
3495
|
*/
|
|
3311
|
-
|
|
3496
|
+
/**
|
|
3497
|
+
* A call INTO a package — `this.authz.can()` on an injected package type,
|
|
3498
|
+
* `row.avatar.getVariant()` on a package-typed column, `catalogue.load()` on a
|
|
3499
|
+
* package factory's product — is outside the boundary: what a package does with
|
|
3500
|
+
* its own tables is technical (§4), what it hands back is a value. Not a gap
|
|
3501
|
+
* the walk failed at; noted (plan 0.11 §A).
|
|
3502
|
+
*/
|
|
3503
|
+
if (isUnreadableWrite(call, symbols, imports, packageImports, body) && !isNoise(call, owner, (typeNode) => membersOfType(typeNode, file, app))) {
|
|
3312
3504
|
unresolved.push({
|
|
3313
3505
|
file: ref.file,
|
|
3314
3506
|
line: call.getStartLineNumber(),
|
|
@@ -3317,12 +3509,24 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3317
3509
|
});
|
|
3318
3510
|
continue;
|
|
3319
3511
|
}
|
|
3320
|
-
if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner))
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3512
|
+
if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner, (typeNode) => membersOfType(typeNode, file, app))) {
|
|
3513
|
+
const specifier = packageTargetOf(call, owner, file, symbols, app, storesByName, sourceFile);
|
|
3514
|
+
if (specifier) {
|
|
3515
|
+
packageCalls.push({
|
|
3516
|
+
file: ref.file,
|
|
3517
|
+
line: call.getStartLineNumber(),
|
|
3518
|
+
expression: call.getExpression().getText().replace(/\s+/g, ""),
|
|
3519
|
+
specifier
|
|
3520
|
+
});
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
unresolved.push({
|
|
3524
|
+
file: ref.file,
|
|
3525
|
+
line: call.getStartLineNumber(),
|
|
3526
|
+
expression: call.getExpression().getText().replace(/\s+/g, ""),
|
|
3527
|
+
reason: "call that no strategy knew how to follow"
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3326
3530
|
}
|
|
3327
3531
|
/**
|
|
3328
3532
|
* Read after the loop: whether a key holds a nested transformer is known only
|
|
@@ -3342,6 +3546,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3342
3546
|
followUps,
|
|
3343
3547
|
unresolved,
|
|
3344
3548
|
validators: validator.fields,
|
|
3549
|
+
packageCalls,
|
|
3345
3550
|
commandFields,
|
|
3346
3551
|
opaqueValidators: validator.opaque,
|
|
3347
3552
|
requestFields: request.fields,
|
|
@@ -3451,6 +3656,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3451
3656
|
const writtenStores = /* @__PURE__ */ new Set();
|
|
3452
3657
|
const inputFields = /* @__PURE__ */ new Set();
|
|
3453
3658
|
const commandFields = /* @__PURE__ */ new Set();
|
|
3659
|
+
const packageCalls = [];
|
|
3454
3660
|
const opaqueInputFields = /* @__PURE__ */ new Set();
|
|
3455
3661
|
const requestFields = /* @__PURE__ */ new Set();
|
|
3456
3662
|
let opaqueRequest = false;
|
|
@@ -3614,12 +3820,36 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3614
3820
|
* Dropping it silently is the worst possible defect: the transaction
|
|
3615
3821
|
* loses a path and nobody knows.
|
|
3616
3822
|
*/
|
|
3617
|
-
if (!isNoiseMember(ref.file, ref.member))
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3823
|
+
if (!isNoiseMember(ref.file, ref.member)) {
|
|
3824
|
+
/**
|
|
3825
|
+
* `catalogue.load()` where `catalogue` is `defineCollection(…)` from a package,
|
|
3826
|
+
* or a class extending a package base without the member: the body is the
|
|
3827
|
+
* package's. Outside the boundary — noted, not a gap (plan 0.11 §A).
|
|
3828
|
+
*/
|
|
3829
|
+
const source = sourceFile(ref.file);
|
|
3830
|
+
const fromPackage = source ? packageMemberOriginOf(source, ref.member, app) : null;
|
|
3831
|
+
if (fromPackage) {
|
|
3832
|
+
packageCalls.push({
|
|
3833
|
+
file: ref.file,
|
|
3834
|
+
line: ref.line ?? 0,
|
|
3835
|
+
expression: `${pathOf(ref.file)}.${ref.member ?? "handle"}`,
|
|
3836
|
+
specifier: fromPackage
|
|
3837
|
+
});
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* `fonte.varrerPorDocumento()` where the file declares an INTERFACE with that
|
|
3842
|
+
* member and no class: there is no body because the implementation is injected
|
|
3843
|
+
* at runtime — a gap, and the right words for it.
|
|
3844
|
+
*/
|
|
3845
|
+
const declaredAsInterface = !!ref.member && !!source && source.getInterfaces().some((i) => i.getMethod(ref.member) || i.getProperty(ref.member));
|
|
3846
|
+
unresolved.push({
|
|
3847
|
+
file: ref.file,
|
|
3848
|
+
line: ref.line ?? 0,
|
|
3849
|
+
expression: `${pathOf(ref.file)}.${ref.member ?? "handle"}`,
|
|
3850
|
+
reason: declaredAsInterface ? "interface method: the implementation is injected at runtime and cannot be followed statically" : "body not found in the resolved file: probably inherited from a package class"
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3623
3853
|
return;
|
|
3624
3854
|
}
|
|
3625
3855
|
let bodyWrites = false;
|
|
@@ -3639,6 +3869,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3639
3869
|
unresolved.push(...facts.unresolved);
|
|
3640
3870
|
for (const field of facts.validators) inputFields.add(field);
|
|
3641
3871
|
for (const field of facts.commandFields) commandFields.add(field);
|
|
3872
|
+
for (const packaged of facts.packageCalls) packageCalls.push(packaged);
|
|
3642
3873
|
for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
|
|
3643
3874
|
for (const field of facts.requestFields) requestFields.add(field);
|
|
3644
3875
|
if (facts.opaqueRequest) opaqueRequest = true;
|
|
@@ -3705,6 +3936,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
3705
3936
|
writtenStores: [...writtenStores].sort(),
|
|
3706
3937
|
inputFields: [...inputFields].sort(),
|
|
3707
3938
|
commandFields: [...commandFields].sort(),
|
|
3939
|
+
packageCalls,
|
|
3708
3940
|
opaqueInputFields: [...opaqueInputFields].sort(),
|
|
3709
3941
|
requestFields: [...requestFields].sort(),
|
|
3710
3942
|
opaqueRequest,
|
|
@@ -3939,7 +4171,91 @@ function dependencyTypeOf(node) {
|
|
|
3939
4171
|
if (Node.isIdentifier(target)) return target.getText();
|
|
3940
4172
|
}
|
|
3941
4173
|
}
|
|
3942
|
-
/** type
|
|
4174
|
+
/** the specifier a type name is imported from in `file`, when the application does not resolve it */
|
|
4175
|
+
function packageSpecifierOfType(typeName, file, app) {
|
|
4176
|
+
const bare = typeName.replace(/<.*/, "").replace(/\|.*$/, "").replace(/\[\]$/, "").trim();
|
|
4177
|
+
for (const declaration of file.getImportDeclarations()) {
|
|
4178
|
+
const specifier = declaration.getModuleSpecifierValue();
|
|
4179
|
+
if (![declaration.getDefaultImport()?.getText(), ...declaration.getNamedImports().map((n) => n.getAliasNode()?.getText() ?? n.getName())].includes(bare)) continue;
|
|
4180
|
+
return app.resolveSpecifier(specifier) ? null : specifier;
|
|
4181
|
+
}
|
|
4182
|
+
return null;
|
|
4183
|
+
}
|
|
4184
|
+
/** the declared type of `this.<property>` on the owner class: a constructor parameter or a property */
|
|
4185
|
+
function declaredTypeOfProperty(owner, property) {
|
|
4186
|
+
if (!owner) return void 0;
|
|
4187
|
+
const parameter = owner.getConstructors()[0]?.getParameters().find((p) => p.getName() === property);
|
|
4188
|
+
if (parameter) return dependencyTypeOf(parameter);
|
|
4189
|
+
const declared = owner.getProperties().find((p) => p.getName() === property);
|
|
4190
|
+
return declared ? dependencyTypeOf(declared) : void 0;
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* Does this call go INTO a package? Structurally, never by name:
|
|
4194
|
+
*
|
|
4195
|
+
* this.authz.can(…) `authz` injected as a type imported from a package
|
|
4196
|
+
* this.resource.avatar.getVariant a column of the store whose declared type is a package's
|
|
4197
|
+
* attachmentManager.create(…) the receiver itself imported from a package
|
|
4198
|
+
*
|
|
4199
|
+
* The specifier comes back so the note can say which package.
|
|
4200
|
+
*/
|
|
4201
|
+
function packageTargetOf(call, owner, file, symbols, app, stores, sourceFile) {
|
|
4202
|
+
const callee = call.getExpression();
|
|
4203
|
+
if (!Node.isPropertyAccessExpression(callee)) return null;
|
|
4204
|
+
const receiver = unwrapAwait(callee.getExpression());
|
|
4205
|
+
if (Node.isPropertyAccessExpression(receiver) && Node.isThisExpression(receiver.getExpression())) {
|
|
4206
|
+
const declared = declaredTypeOfProperty(owner, receiver.getName());
|
|
4207
|
+
if (declared) return packageSpecifierOfType(declared, file, app);
|
|
4208
|
+
}
|
|
4209
|
+
if (Node.isPropertyAccessExpression(receiver)) {
|
|
4210
|
+
const holder = receiver.getExpression();
|
|
4211
|
+
const holderText = holder.getText();
|
|
4212
|
+
const store = symbols.get(rootSymbolOf(holder) ?? "") ?? (holderText === "this.resource" && owner ? transformerResourceOf(owner) : null);
|
|
4213
|
+
const declared = store ? stores.get(store) : void 0;
|
|
4214
|
+
const modelFile = declared ? sourceFile(declared.provenance.file) : null;
|
|
4215
|
+
if (declared && modelFile) {
|
|
4216
|
+
const type = modelFile.getClasses().flatMap((c) => c.getProperties()).find((p) => p.getName() === receiver.getName())?.getTypeNode()?.getText() ?? declared.attributes.find((a) => a.name === receiver.getName())?.type;
|
|
4217
|
+
if (type) return packageSpecifierOfType(type, modelFile, app);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
if (Node.isIdentifier(receiver)) return packageSpecifierOfType(receiver.getText(), file, app);
|
|
4221
|
+
return null;
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* A member the resolved file does not declare, because it is a package's: the
|
|
4225
|
+
* exported symbol is a const built by a package factory, or a class extending a
|
|
4226
|
+
* package base. Returns the package's specifier.
|
|
4227
|
+
*/
|
|
4228
|
+
function packageMemberOriginOf(file, member, app) {
|
|
4229
|
+
const rootOf = (node) => {
|
|
4230
|
+
let current = node;
|
|
4231
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
4232
|
+
if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
|
|
4233
|
+
current = current.getExpression();
|
|
4234
|
+
continue;
|
|
4235
|
+
}
|
|
4236
|
+
return Node.isIdentifier(current) ? current.getText() : null;
|
|
4237
|
+
}
|
|
4238
|
+
return null;
|
|
4239
|
+
};
|
|
4240
|
+
const defaultExported = file.getExportAssignments().map((e) => e.getExpression().getText());
|
|
4241
|
+
const declarations = file.getVariableDeclarations();
|
|
4242
|
+
const exported = declarations.filter((d) => d.getVariableStatement()?.isExported() || defaultExported.includes(d.getName()));
|
|
4243
|
+
for (const declaration of exported.length > 0 ? exported : declarations) {
|
|
4244
|
+
const initializer = declaration.getInitializer();
|
|
4245
|
+
if (!initializer || !Node.isCallExpression(initializer)) continue;
|
|
4246
|
+
const root = rootOf(initializer);
|
|
4247
|
+
const specifier = root ? packageSpecifierOfType(root, file, app) : null;
|
|
4248
|
+
if (specifier) return specifier;
|
|
4249
|
+
}
|
|
4250
|
+
for (const cls of file.getClasses()) {
|
|
4251
|
+
if (member && cls.getMethod(member)) return null;
|
|
4252
|
+
const parent = cls.getExtends()?.getExpression();
|
|
4253
|
+
const root = parent ? rootOf(parent) : null;
|
|
4254
|
+
const specifier = root ? packageSpecifierOfType(root, file, app) : null;
|
|
4255
|
+
if (specifier) return specifier;
|
|
4256
|
+
}
|
|
4257
|
+
return null;
|
|
4258
|
+
}
|
|
3943
4259
|
function resolveTypeToFile(typeName, file, app) {
|
|
3944
4260
|
const bare = typeName.replace(/<.*/, "").trim();
|
|
3945
4261
|
for (const declaration of file.getImportDeclarations()) {
|
|
@@ -4194,8 +4510,15 @@ function isUnreadableWrite(call, symbols, imports, packageImports, body) {
|
|
|
4194
4510
|
const nameNode = declaration.getNameNode();
|
|
4195
4511
|
if (!Node.isIdentifier(nameNode) || nameNode.getText() !== root) continue;
|
|
4196
4512
|
const initializer = declaration.getInitializer();
|
|
4197
|
-
const
|
|
4198
|
-
|
|
4513
|
+
const initialized = initializer ? unwrapAwait(initializer) : null;
|
|
4514
|
+
/**
|
|
4515
|
+
* `await db.transaction(async (trx) => …)` is rooted at a package too, but its value
|
|
4516
|
+
* is what the CALLBACK returns — the application's. Only a call with no function
|
|
4517
|
+
* argument is a package's own object.
|
|
4518
|
+
*/
|
|
4519
|
+
const handsBackACallback = initialized && Node.isCallExpression(initialized) && initialized.getArguments().some((a) => Node.isArrowFunction(a) || Node.isFunctionExpression(a));
|
|
4520
|
+
const origin = initialized ? rootSymbolOf(initialized) : null;
|
|
4521
|
+
if (origin && packageImports.has(origin) && !handsBackACallback) return false;
|
|
4199
4522
|
}
|
|
4200
4523
|
return true;
|
|
4201
4524
|
}
|
|
@@ -5142,13 +5465,14 @@ const RULESET = "afp";
|
|
|
5142
5465
|
* the guard's user, a service the container made — and a write nobody can type is
|
|
5143
5466
|
* an unresolved call instead of silence. Two in 1.8.0: a method the model
|
|
5144
5467
|
* declares names the store its caller writes, and a listener written inline on a
|
|
5145
|
-
* string event is followed like a listener class.
|
|
5468
|
+
* string event is followed like a listener class. One in 1.9.0: the value of a
|
|
5469
|
+
* transaction callback is what its returns name.
|
|
5146
5470
|
*
|
|
5147
5471
|
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
5148
5472
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
5149
5473
|
* exists for exactly that, and only this constant arms it.
|
|
5150
5474
|
*/
|
|
5151
|
-
const RULESET_VERSION = "1.
|
|
5475
|
+
const RULESET_VERSION = "1.9.0";
|
|
5152
5476
|
function count(input, options = {}) {
|
|
5153
5477
|
const warnings = [];
|
|
5154
5478
|
const usage = usageOf(input);
|
|
@@ -5296,7 +5620,7 @@ function unreadableDeliveryWarnings(input) {
|
|
|
5296
5620
|
if (blind.length === 0) return [];
|
|
5297
5621
|
return [
|
|
5298
5622
|
`${blind.length} transaction(s) deliver a value the analysis cannot read — a generated document, a call nobody followed — counted as 1 DET each, a FLOOR. This UNDERSTATES the output:`,
|
|
5299
|
-
...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.
|
|
5623
|
+
...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.identity}: ${behavior.delivered.opaqueFields.join(", ")}`),
|
|
5300
5624
|
...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
|
|
5301
5625
|
];
|
|
5302
5626
|
}
|
|
@@ -5312,7 +5636,7 @@ function unreadablePageWarnings(input) {
|
|
|
5312
5636
|
for (const entry of input.entryPoints) {
|
|
5313
5637
|
const behavior = input.behaviors.get(entry.id);
|
|
5314
5638
|
if (!behavior || behavior.writes) continue;
|
|
5315
|
-
for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.
|
|
5639
|
+
for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.identity}: ${store} leaves whole — ${reason}`);
|
|
5316
5640
|
}
|
|
5317
5641
|
if (lines.length === 0) return [];
|
|
5318
5642
|
return [
|
|
@@ -5446,7 +5770,7 @@ function unreadableInputWarnings(input) {
|
|
|
5446
5770
|
if (blind.length === 0) return [];
|
|
5447
5771
|
return [
|
|
5448
5772
|
`${blind.length} transaction(s) read the request without enumerating fields (\`all()\`, \`body()\`, \`except()\`), so their input DETs could not be counted and each sits at the floor of its band. This UNDERSTATES the total — the fix is a validator, not a configuration:`,
|
|
5449
|
-
...blind.slice(0, 10).map(({ entry }) => ` ${entry.
|
|
5773
|
+
...blind.slice(0, 10).map(({ entry }) => ` ${entry.identity}`),
|
|
5450
5774
|
...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
|
|
5451
5775
|
];
|
|
5452
5776
|
}
|
|
@@ -5467,7 +5791,7 @@ function unreadableOutputWarnings(input) {
|
|
|
5467
5791
|
if (blind.length === 0) return [];
|
|
5468
5792
|
return [
|
|
5469
5793
|
`${blind.length} transaction(s) pass through a transformer that spreads something the analysis cannot read, counted as 1 DET each — a FLOOR. This UNDERSTATES the output; the fix is in the transformer (\`this.pick(...)\` or named keys), not a configuration:`,
|
|
5470
|
-
...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.
|
|
5794
|
+
...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.identity}: ${behavior.opaqueOutputFields.join(", ")}`),
|
|
5471
5795
|
...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
|
|
5472
5796
|
];
|
|
5473
5797
|
}
|
|
@@ -5692,9 +6016,32 @@ var CoverageTooLowError = class extends Error {
|
|
|
5692
6016
|
this.name = "CoverageTooLowError";
|
|
5693
6017
|
}
|
|
5694
6018
|
};
|
|
6019
|
+
/**
|
|
6020
|
+
* One note per call site into a package, naming the transactions that reach it —
|
|
6021
|
+
* outside the boundary (§4): what a package does with its own tables is technical,
|
|
6022
|
+
* what it hands back is a value. Never in coverage (plan 0.11 §A).
|
|
6023
|
+
*/
|
|
6024
|
+
function packageCallNotes(entryPoints, behaviors, emit) {
|
|
6025
|
+
const sites = /* @__PURE__ */ new Map();
|
|
6026
|
+
for (const entry of entryPoints) {
|
|
6027
|
+
const behavior = behaviors.get(entry.id);
|
|
6028
|
+
for (const call of behavior?.packageCalls ?? []) {
|
|
6029
|
+
const key = `${call.file}:${call.line}:${call.expression}`;
|
|
6030
|
+
const site = sites.get(key) ?? {
|
|
6031
|
+
specifier: call.specifier,
|
|
6032
|
+
expression: call.expression,
|
|
6033
|
+
where: call.line > 0 ? `${emit(call.file)}:${call.line}` : emit(call.file),
|
|
6034
|
+
reached: []
|
|
6035
|
+
};
|
|
6036
|
+
if (!site.reached.includes(entry.identity)) site.reached.push(entry.identity);
|
|
6037
|
+
sites.set(key, site);
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
return [...sites.values()].sort((a, b) => a.where.localeCompare(b.where)).map((site) => `${site.reached.join(", ")}: call into ${site.specifier} (${site.expression}) — outside the boundary; what a package does with its own tables is technical (§4), what it hands back is a value [${site.where}]`);
|
|
6041
|
+
}
|
|
5695
6042
|
async function analyze(root, options = {}) {
|
|
5696
6043
|
const app = await discoverApp(root);
|
|
5697
|
-
const { stores, unresolved: storeProblems } = await collectDataStores(app);
|
|
6044
|
+
const { stores, unresolved: storeProblems, notes: storeNotes } = await collectDataStores(app);
|
|
5698
6045
|
const routes = await collectEntryPoints(app);
|
|
5699
6046
|
const routeProblems = routes.unresolved;
|
|
5700
6047
|
const entryPoints = [...routes.entryPoints, ...collectCommands(app)];
|
|
@@ -5824,6 +6171,10 @@ async function analyze(root, options = {}) {
|
|
|
5824
6171
|
transformedStores: behavior.transformedStores,
|
|
5825
6172
|
delivered: behavior.delivered,
|
|
5826
6173
|
pageReads: behavior.pageReads,
|
|
6174
|
+
packageCalls: behavior.packageCalls.map((c) => ({
|
|
6175
|
+
...c,
|
|
6176
|
+
file: emit(c.file)
|
|
6177
|
+
})),
|
|
5827
6178
|
unreadablePages: behavior.unreadablePages,
|
|
5828
6179
|
outputReads: behavior.outputReads,
|
|
5829
6180
|
trace: behavior.trace.map((step) => ({
|
|
@@ -5844,7 +6195,8 @@ async function analyze(root, options = {}) {
|
|
|
5844
6195
|
unresolved: unresolvedSites.map((site) => ({
|
|
5845
6196
|
...site,
|
|
5846
6197
|
file: emit(site.file)
|
|
5847
|
-
}))
|
|
6198
|
+
})),
|
|
6199
|
+
notes: [...storeNotes, ...packageCallNotes(entryPoints, behaviors, emit)]
|
|
5848
6200
|
};
|
|
5849
6201
|
const minimum = options.minCoverage ?? 0;
|
|
5850
6202
|
if (inventory.coverage.ratio < minimum) throw new CoverageTooLowError(inventory.coverage.ratio, minimum);
|
|
@@ -1368,4 +1368,4 @@ function ignoreCalls(options) {
|
|
|
1368
1368
|
};
|
|
1369
1369
|
}
|
|
1370
1370
|
//#endregion
|
|
1371
|
-
export {
|
|
1371
|
+
export { isSeeder as _, chainShapeOf as a, toPosix as b, transformerResourceOf as c, EXECUTION_METHODS as d, collectEventBindings as f, isApplicationCode as g, rootSymbolOf as h, resolveCall as i, unwrap as l, hooksFiredBy as m, ignoreCalls as n, mappedLiteralOf as o, detectAccess as p, isTechnicalWrite as r, outputFieldsIn as s, BUILTIN_CALL_RESOLVERS as t, DISPATCH_METHODS as u, relativeTo as v, samePath as y };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as IncomparableSourcesError, d as DEFAULTS, f as defineConfig, i as measureStructure, n as parseSamples, o as FACTOR_PRESETS, r as measureConformance, s as IncomparableRulesetsError, t as calibrate, u as diffCounts } from "./calibration-DVIf8hcE.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as analyze } from "./pipeline-
|
|
2
|
+
import { b as toPosix } from "./resolvers-Bz1F-Tgf.js";
|
|
3
|
+
import { n as analyze } from "./pipeline-CdDJRsLM.js";
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
@@ -312,7 +312,8 @@ async function runInventory(options) {
|
|
|
312
312
|
`data stores: ${inventory.dataStores.length}`,
|
|
313
313
|
`entry points: ${coverage.entryPointsTotal}`,
|
|
314
314
|
`coverage: ${(coverage.ratio * 100).toFixed(1)}% (${coverage.unresolvedCalls} unresolved calls)`,
|
|
315
|
-
...inventory.unresolved.map((site) => ` ${describeSite(site)}`)
|
|
315
|
+
...inventory.unresolved.map((site) => ` ${describeSite(site)}`),
|
|
316
|
+
...inventory.notes.length > 0 ? ["notes (declarations, not gaps):", ...inventory.notes.map((note) => ` ${note}`)] : []
|
|
316
317
|
].join("\n")
|
|
317
318
|
};
|
|
318
319
|
}
|
|
@@ -46,13 +46,14 @@ export declare const RULESET = "afp";
|
|
|
46
46
|
* the guard's user, a service the container made — and a write nobody can type is
|
|
47
47
|
* an unresolved call instead of silence. Two in 1.8.0: a method the model
|
|
48
48
|
* declares names the store its caller writes, and a listener written inline on a
|
|
49
|
-
* string event is followed like a listener class.
|
|
49
|
+
* string event is followed like a listener class. One in 1.9.0: the value of a
|
|
50
|
+
* transaction callback is what its returns name.
|
|
50
51
|
*
|
|
51
52
|
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
52
53
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
53
54
|
* exists for exactly that, and only this constant arms it.
|
|
54
55
|
*/
|
|
55
|
-
export declare const RULESET_VERSION = "1.
|
|
56
|
+
export declare const RULESET_VERSION = "1.9.0";
|
|
56
57
|
export type CountInput = {
|
|
57
58
|
app: AppContext;
|
|
58
59
|
stores: CollectedDataStore[];
|
package/build/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as CoverageTooLowError } from "../pipeline-
|
|
2
|
-
import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-
|
|
1
|
+
import { t as CoverageTooLowError } from "../pipeline-CdDJRsLM.js";
|
|
2
|
+
import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-CzzlX7h-.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Node } from 'ts-morph';
|
|
1
2
|
import type { ClassDeclaration, SourceFile } from 'ts-morph';
|
|
2
3
|
import type { AppContext } from '../app_context.js';
|
|
3
4
|
import type { CollectedDataStore } from '../sources/data_stores.js';
|
|
@@ -104,6 +105,12 @@ export type Behavior = {
|
|
|
104
105
|
*/
|
|
105
106
|
pageReads: Record<string, string[]>;
|
|
106
107
|
unreadablePages: Record<string, string>;
|
|
108
|
+
/**
|
|
109
|
+
* Calls into a PACKAGE the walk met on the way — an injected package type, a
|
|
110
|
+
* package-typed column, a package factory's product, a member inherited from a
|
|
111
|
+
* package base. Outside the boundary (§4): noted, never a gap (plan 0.11 §A).
|
|
112
|
+
*/
|
|
113
|
+
packageCalls: PackageCall[];
|
|
107
114
|
trace: TraceStep[];
|
|
108
115
|
/** bodies reached, for `fp:diff` */
|
|
109
116
|
scope: ScopeEntry[];
|
|
@@ -157,6 +164,15 @@ export declare function analyzeHandler(app: AppContext, stores: CollectedDataSto
|
|
|
157
164
|
* as any import. No type checker is needed.
|
|
158
165
|
*/
|
|
159
166
|
export declare function injectedFor(owner: ClassDeclaration | undefined, file: SourceFile, app: AppContext): Map<string, string>;
|
|
167
|
+
/** type identifier -> application file where it is declared */
|
|
168
|
+
/** a call whose target resolves to a package: where, what, which package */
|
|
169
|
+
export type PackageCall = {
|
|
170
|
+
file: string;
|
|
171
|
+
line: number;
|
|
172
|
+
expression: string;
|
|
173
|
+
specifier: string;
|
|
174
|
+
};
|
|
175
|
+
export declare function membersOfType(typeNode: Node, file: SourceFile, app: AppContext): Map<string, string>;
|
|
160
176
|
/**
|
|
161
177
|
* Both maps a file's imports produce: where a local name resolves, and what it
|
|
162
178
|
* was called where it was exported.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Node } from 'ts-morph';
|
|
1
2
|
import type { CallExpression, ClassDeclaration } from 'ts-morph';
|
|
2
3
|
/** Is this call one that cannot reach a data store? */
|
|
3
|
-
|
|
4
|
+
/** resolves a type annotation to its members (`name -> type text`), when the caller can */
|
|
5
|
+
export type MembersOf = (typeNode: Node) => Map<string, string>;
|
|
6
|
+
export declare function isNoise(call: CallExpression, owner?: ClassDeclaration, membersOf?: MembersOf): boolean;
|
|
4
7
|
/**
|
|
5
8
|
* Iteration over a list, checked BEFORE the resolvers run.
|
|
6
9
|
*
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as resolveCall, n as ignoreCalls, r as isTechnicalWrite, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-
|
|
1
|
+
import { i as resolveCall, n as ignoreCalls, r as isTechnicalWrite, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-Bz1F-Tgf.js";
|
|
2
2
|
export { BUILTIN_CALL_RESOLVERS, ignoreCalls, isTechnicalWrite, resolveCall };
|
|
@@ -27,5 +27,12 @@ export type DataStoreCollection = {
|
|
|
27
27
|
stores: CollectedDataStore[];
|
|
28
28
|
/** chains that left the application, required by AFP §6.5.3 */
|
|
29
29
|
unresolved: UnresolvedCall[];
|
|
30
|
+
/**
|
|
31
|
+
* Facts about the declarations that are not gaps: a base class or a mixin that
|
|
32
|
+
* comes from a package. What it adds is technical (a hashed password, audit
|
|
33
|
+
* stamps, a soft-delete mark) and never a user-recognisable attribute (§7.2); the
|
|
34
|
+
* model's own columns and the schema decide. Listed, never counted in coverage.
|
|
35
|
+
*/
|
|
36
|
+
notes: string[];
|
|
30
37
|
};
|
|
31
38
|
export declare function collectDataStores(app: AppContext): Promise<DataStoreCollection>;
|
package/build/src/pipeline.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as analyze, t as CoverageTooLowError } from "../pipeline-
|
|
1
|
+
import { n as analyze, t as CoverageTooLowError } from "../pipeline-CdDJRsLM.js";
|
|
2
2
|
export { CoverageTooLowError, analyze };
|
package/build/src/types.d.ts
CHANGED
|
@@ -148,6 +148,13 @@ export type HandlerBehavior = {
|
|
|
148
148
|
pageReads?: Record<string, string[]>;
|
|
149
149
|
/** stores a page could not be read for, and why: they leave whole */
|
|
150
150
|
unreadablePages?: Record<string, string>;
|
|
151
|
+
/** calls into a package met on the way: outside the boundary, noted, never a gap (plan 0.11 §A) */
|
|
152
|
+
packageCalls?: {
|
|
153
|
+
file: string;
|
|
154
|
+
line: number;
|
|
155
|
+
expression: string;
|
|
156
|
+
specifier: string;
|
|
157
|
+
}[];
|
|
151
158
|
/**
|
|
152
159
|
* How each store was read: rows whole, `.select()` columns, or one aggregate
|
|
153
160
|
* scalar; by its own chain (`direct`) or preloaded through another store (`via`).
|
|
@@ -222,6 +229,11 @@ export type Inventory = {
|
|
|
222
229
|
* on. One entry per site — a body five routes reach is one gap, not five.
|
|
223
230
|
*/
|
|
224
231
|
unresolved: UnresolvedSite[];
|
|
232
|
+
/**
|
|
233
|
+
* Facts about the declarations that are not gaps — a base class or a mixin from a
|
|
234
|
+
* package, whose additions are technical. Listed for the reader; never in coverage.
|
|
235
|
+
*/
|
|
236
|
+
notes: string[];
|
|
225
237
|
};
|
|
226
238
|
export type FunctionType = 'ILF' | 'EIF' | 'EI' | 'EO' | 'EQ';
|
|
227
239
|
export type Complexity = 'low' | 'average' | 'high';
|