@shrkcrft/boundaries 0.1.0-alpha.30 → 0.1.0-alpha.31
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/dist/baseline/compute-baseline.d.ts +8 -0
- package/dist/baseline/compute-baseline.d.ts.map +1 -1
- package/dist/baseline/compute-baseline.js +8 -7
- package/dist/baseline/diff-baseline.d.ts +28 -0
- package/dist/baseline/diff-baseline.d.ts.map +1 -1
- package/dist/baseline/diff-baseline.js +29 -0
- package/dist/evaluate/boundary-unit-finding.d.ts +10 -0
- package/dist/evaluate/boundary-unit-finding.d.ts.map +1 -0
- package/dist/evaluate/boundary-unit-finding.js +21 -0
- package/dist/evaluate/boundary-unit-kind.d.ts +7 -0
- package/dist/evaluate/boundary-unit-kind.d.ts.map +1 -0
- package/dist/evaluate/boundary-unit-kind.js +14 -0
- package/dist/evaluate/evaluate-boundaries.d.ts +230 -3
- package/dist/evaluate/evaluate-boundaries.d.ts.map +1 -1
- package/dist/evaluate/evaluate-boundaries.js +481 -53
- package/dist/evaluate/i-boundary-rule-settle-input.d.ts +27 -0
- package/dist/evaluate/i-boundary-rule-settle-input.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-rule-settle-input.js +1 -0
- package/dist/evaluate/i-boundary-rule-settlement.d.ts +23 -0
- package/dist/evaluate/i-boundary-rule-settlement.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-rule-settlement.js +1 -0
- package/dist/evaluate/i-boundary-unit-finding.d.ts +22 -0
- package/dist/evaluate/i-boundary-unit-finding.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-unit-finding.js +1 -0
- package/dist/evaluate/settle-boundary-rule.d.ts +23 -0
- package/dist/evaluate/settle-boundary-rule.d.ts.map +1 -0
- package/dist/evaluate/settle-boundary-rule.js +73 -0
- package/dist/evaluate/with-boundary-rule-settlement.d.ts +11 -0
- package/dist/evaluate/with-boundary-rule-settlement.d.ts.map +1 -0
- package/dist/evaluate/with-boundary-rule-settlement.js +44 -0
- package/dist/extract/code-zones.d.ts +95 -3
- package/dist/extract/code-zones.d.ts.map +1 -1
- package/dist/extract/code-zones.js +313 -4
- package/dist/extract/extract-tokens.d.ts +30 -0
- package/dist/extract/extract-tokens.d.ts.map +1 -1
- package/dist/extract/extract-tokens.js +233 -27
- package/dist/extract/i-labeled-source.d.ts +12 -0
- package/dist/extract/i-labeled-source.d.ts.map +1 -0
- package/dist/extract/i-labeled-source.js +1 -0
- package/dist/extract/import-edges.d.ts +1 -0
- package/dist/extract/import-edges.d.ts.map +1 -1
- package/dist/extract/import-edges.js +20 -7
- package/dist/extract/inspect-source.d.ts +32 -3
- package/dist/extract/inspect-source.d.ts.map +1 -1
- package/dist/extract/inspect-source.js +50 -7
- package/dist/extract/parse-imports.d.ts +34 -12
- package/dist/extract/parse-imports.d.ts.map +1 -1
- package/dist/extract/parse-imports.js +119 -20
- package/dist/extract/scan-literals.d.ts +12 -1
- package/dist/extract/scan-literals.d.ts.map +1 -1
- package/dist/extract/scan-literals.js +15 -1
- package/dist/extract/source-liveness-request.d.ts +24 -0
- package/dist/extract/source-liveness-request.d.ts.map +1 -0
- package/dist/extract/source-liveness-request.js +45 -0
- package/dist/generated/check-provenance.d.ts.map +1 -1
- package/dist/generated/check-provenance.js +3 -1
- package/dist/generated/read-regen-tree.d.ts +16 -0
- package/dist/generated/read-regen-tree.d.ts.map +1 -0
- package/dist/generated/read-regen-tree.js +61 -0
- package/dist/generated/scan-generated.d.ts +8 -0
- package/dist/generated/scan-generated.d.ts.map +1 -1
- package/dist/generated/scan-generated.js +25 -10
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -0
- package/dist/model/boundary-intended-empty-line.d.ts +10 -0
- package/dist/model/boundary-intended-empty-line.d.ts.map +1 -0
- package/dist/model/boundary-intended-empty-line.js +11 -0
- package/dist/model/boundary-markable-list.d.ts +15 -0
- package/dist/model/boundary-markable-list.d.ts.map +1 -0
- package/dist/model/boundary-markable-list.js +15 -0
- package/dist/model/boundary-rule-input-keys.d.ts +11 -0
- package/dist/model/boundary-rule-input-keys.d.ts.map +1 -0
- package/dist/model/boundary-rule-input-keys.js +29 -0
- package/dist/model/boundary-rule-input.d.ts +23 -0
- package/dist/model/boundary-rule-input.d.ts.map +1 -0
- package/dist/model/boundary-rule-input.js +1 -0
- package/dist/model/boundary-rule-key-problems.d.ts +11 -0
- package/dist/model/boundary-rule-key-problems.d.ts.map +1 -0
- package/dist/model/boundary-rule-key-problems.js +36 -0
- package/dist/model/boundary-rule-marker-problems.d.ts +26 -0
- package/dist/model/boundary-rule-marker-problems.d.ts.map +1 -0
- package/dist/model/boundary-rule-marker-problems.js +94 -0
- package/dist/model/boundary-rule-scope.d.ts +88 -0
- package/dist/model/boundary-rule-scope.d.ts.map +1 -0
- package/dist/model/boundary-rule-scope.js +175 -0
- package/dist/model/boundary-rule.d.ts +120 -5
- package/dist/model/boundary-rule.d.ts.map +1 -1
- package/dist/model/boundary-rule.js +148 -1
- package/dist/model/boundary-unit-problem-issue.d.ts +9 -0
- package/dist/model/boundary-unit-problem-issue.d.ts.map +1 -0
- package/dist/model/boundary-unit-problem-issue.js +16 -0
- package/dist/model/normalize-boundary-rule.d.ts +21 -0
- package/dist/model/normalize-boundary-rule.d.ts.map +1 -0
- package/dist/model/normalize-boundary-rule.js +62 -0
- package/dist/policy/evaluate-policy.d.ts +80 -3
- package/dist/policy/evaluate-policy.d.ts.map +1 -1
- package/dist/policy/evaluate-policy.js +152 -19
- package/dist/policy/i-policy-rule-liveness.d.ts +15 -0
- package/dist/policy/i-policy-rule-liveness.d.ts.map +1 -0
- package/dist/policy/i-policy-rule-liveness.js +1 -0
- package/dist/policy/run-policy.d.ts +1 -1
- package/dist/policy/run-policy.d.ts.map +1 -1
- package/dist/policy/run-policy.js +171 -21
- package/dist/registry/load-boundary-rules.d.ts +29 -1
- package/dist/registry/load-boundary-rules.d.ts.map +1 -1
- package/dist/registry/load-boundary-rules.js +38 -8
- package/dist/scan/glob.d.ts +90 -0
- package/dist/scan/glob.d.ts.map +1 -1
- package/dist/scan/glob.js +205 -0
- package/dist/scan/i-glob-unit-measure.d.ts +22 -0
- package/dist/scan/i-glob-unit-measure.d.ts.map +1 -0
- package/dist/scan/i-glob-unit-measure.js +1 -0
- package/dist/scan/import-pattern.d.ts +109 -0
- package/dist/scan/import-pattern.d.ts.map +1 -0
- package/dist/scan/import-pattern.js +191 -0
- package/dist/scan/node-builtin-package-names.d.ts +13 -0
- package/dist/scan/node-builtin-package-names.d.ts.map +1 -0
- package/dist/scan/node-builtin-package-names.js +25 -0
- package/dist/scan/scan-imports.d.ts +36 -3
- package/dist/scan/scan-imports.d.ts.map +1 -1
- package/dist/scan/scan-imports.js +114 -45
- package/dist/util/blank-run-hazard-finding.d.ts +34 -0
- package/dist/util/blank-run-hazard-finding.d.ts.map +1 -0
- package/dist/util/blank-run-hazard-finding.js +1 -0
- package/dist/util/blank-run-hazard.d.ts +29 -0
- package/dist/util/blank-run-hazard.d.ts.map +1 -0
- package/dist/util/blank-run-hazard.js +450 -0
- package/dist/util/dead-glob-units.d.ts +46 -0
- package/dist/util/dead-glob-units.d.ts.map +1 -0
- package/dist/util/dead-glob-units.js +116 -0
- package/dist/util/glob-list-liveness-input.d.ts +31 -0
- package/dist/util/glob-list-liveness-input.d.ts.map +1 -0
- package/dist/util/glob-list-liveness-input.js +65 -0
- package/dist/util/i-dead-glob-unit.d.ts +24 -0
- package/dist/util/i-dead-glob-unit.d.ts.map +1 -0
- package/dist/util/i-dead-glob-unit.js +1 -0
- package/dist/util/i-glob-list-units.d.ts +25 -0
- package/dist/util/i-glob-list-units.d.ts.map +1 -0
- package/dist/util/i-glob-list-units.js +1 -0
- package/dist/util/i-glob-liveness-list.d.ts +23 -0
- package/dist/util/i-glob-liveness-list.d.ts.map +1 -0
- package/dist/util/i-glob-liveness-list.js +1 -0
- package/dist/util/i-glob-liveness-request.d.ts +11 -0
- package/dist/util/i-glob-liveness-request.d.ts.map +1 -0
- package/dist/util/i-glob-liveness-request.js +1 -0
- package/dist/util/i-glob-negation.d.ts +13 -0
- package/dist/util/i-glob-negation.d.ts.map +1 -0
- package/dist/util/i-glob-negation.js +1 -0
- package/dist/util/matched-files.d.ts +22 -0
- package/dist/util/matched-files.d.ts.map +1 -0
- package/dist/util/matched-files.js +1 -0
- package/dist/util/negation-cause.d.ts +15 -0
- package/dist/util/negation-cause.d.ts.map +1 -0
- package/dist/util/negation-cause.js +21 -0
- package/dist/util/plane-scan-exclude-dirs.d.ts +17 -0
- package/dist/util/plane-scan-exclude-dirs.d.ts.map +1 -0
- package/dist/util/plane-scan-exclude-dirs.js +22 -0
- package/dist/util/read-glob-list-liveness.d.ts +20 -0
- package/dist/util/read-glob-list-liveness.d.ts.map +1 -0
- package/dist/util/read-glob-list-liveness.js +20 -0
- package/dist/util/read-scope-coverage.d.ts +90 -0
- package/dist/util/read-scope-coverage.d.ts.map +1 -0
- package/dist/util/read-scope-coverage.js +173 -0
- package/dist/util/read-scope.d.ts +14 -0
- package/dist/util/read-scope.d.ts.map +1 -0
- package/dist/util/read-scope.js +1 -0
- package/dist/util/read-selected-files.d.ts +16 -0
- package/dist/util/read-selected-files.d.ts.map +1 -0
- package/dist/util/read-selected-files.js +25 -0
- package/dist/util/settle-glob-lists.d.ts +12 -0
- package/dist/util/settle-glob-lists.d.ts.map +1 -0
- package/dist/util/settle-glob-lists.js +13 -0
- package/dist/util/unread-file-reason.d.ts +26 -0
- package/dist/util/unread-file-reason.d.ts.map +1 -0
- package/dist/util/unread-file-reason.js +26 -0
- package/dist/util/unread-file.d.ts +10 -0
- package/dist/util/unread-file.d.ts.map +1 -0
- package/dist/util/unread-file.js +1 -0
- package/dist/util/walk-files.d.ts +74 -10
- package/dist/util/walk-files.d.ts.map +1 -1
- package/dist/util/walk-files.js +137 -31
- package/dist/wiring/evaluate-wiring.d.ts +79 -5
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +267 -12
- package/dist/wiring/explain-wiring.d.ts +49 -5
- package/dist/wiring/explain-wiring.d.ts.map +1 -1
- package/dist/wiring/explain-wiring.js +118 -10
- package/dist/wiring/i-idiom-role-coverage.d.ts +24 -0
- package/dist/wiring/i-idiom-role-coverage.d.ts.map +1 -0
- package/dist/wiring/i-idiom-role-coverage.js +1 -0
- package/dist/wiring/i-registration-query-verdict.d.ts +38 -0
- package/dist/wiring/i-registration-query-verdict.d.ts.map +1 -0
- package/dist/wiring/i-registration-query-verdict.js +1 -0
- package/dist/wiring/i-registration-roles.d.ts +71 -0
- package/dist/wiring/i-registration-roles.d.ts.map +1 -0
- package/dist/wiring/i-registration-roles.js +1 -0
- package/dist/wiring/measure-idiom-role-coverage.d.ts +12 -0
- package/dist/wiring/measure-idiom-role-coverage.d.ts.map +1 -0
- package/dist/wiring/measure-idiom-role-coverage.js +20 -0
- package/dist/wiring/measure-registration-roles.d.ts +25 -0
- package/dist/wiring/measure-registration-roles.d.ts.map +1 -0
- package/dist/wiring/measure-registration-roles.js +124 -0
- package/dist/wiring/plan-wiring-fix.js +2 -2
- package/dist/wiring/registration-graph.d.ts +12 -0
- package/dist/wiring/registration-graph.d.ts.map +1 -1
- package/dist/wiring/registration-graph.js +16 -5
- package/dist/wiring/registration-query-verdict.d.ts +23 -0
- package/dist/wiring/registration-query-verdict.d.ts.map +1 -0
- package/dist/wiring/registration-query-verdict.js +118 -0
- package/dist/wiring/registry-query.d.ts +8 -0
- package/dist/wiring/registry-query.d.ts.map +1 -1
- package/dist/wiring/registry-query.js +12 -5
- package/dist/wiring/scan-wiring-files.d.ts +4 -2
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +52 -15
- package/dist/wiring/sink-imports.d.ts +11 -0
- package/dist/wiring/sink-imports.d.ts.map +1 -1
- package/dist/wiring/sink-imports.js +11 -2
- package/dist/wiring/trace-literal.d.ts +6 -0
- package/dist/wiring/trace-literal.d.ts.map +1 -1
- package/dist/wiring/trace-literal.js +5 -2
- package/dist/wiring/wiring-labeled-sources.d.ts +12 -0
- package/dist/wiring/wiring-labeled-sources.d.ts.map +1 -0
- package/dist/wiring/wiring-labeled-sources.js +20 -0
- package/package.json +2 -2
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UnitLivenessState } from '@shrkcrft/core';
|
|
2
|
+
import type { IBoundaryDeadUnit } from './evaluate-boundaries.js';
|
|
3
|
+
/**
|
|
4
|
+
* One settled selector unit of a boundary rule, as every boundary surface
|
|
5
|
+
* reports it (round 13): `check boundaries` text and `--json` (`intendedEmpty`,
|
|
6
|
+
* `wentLive`, `failingUnits`), MCP `check_boundaries`, the quality and finish
|
|
7
|
+
* notes. Built by `boundaryUnitFinding` from the one settle.
|
|
8
|
+
*/
|
|
9
|
+
export interface IBoundaryUnitFinding {
|
|
10
|
+
readonly ruleId: string;
|
|
11
|
+
/** The boundary JSON's unit word: `from`, `forbidden`, `allowed` or `exemptFiles`. */
|
|
12
|
+
readonly unit: IBoundaryDeadUnit['unit'];
|
|
13
|
+
readonly selector: string;
|
|
14
|
+
readonly state: UnitLivenessState;
|
|
15
|
+
/** THE sentence for the unit (`formatUnitLiveness`), without the selector the row already names. */
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
/** The marker's own reason, when the unit is marked `expectEmpty`. */
|
|
18
|
+
readonly markReason?: string;
|
|
19
|
+
/** The pack that stamped the marker: a pack marker that went live is INFO and never fails the run. */
|
|
20
|
+
readonly packageName?: string;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=i-boundary-unit-finding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i-boundary-unit-finding.d.ts","sourceRoot":"","sources":["../../src/evaluate/i-boundary-unit-finding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,oGAAoG;IACpG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,sGAAsG;IACtG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IBoundaryRuleSettleInput } from './i-boundary-rule-settle-input.js';
|
|
2
|
+
import type { IBoundaryRuleSettlement } from './i-boundary-rule-settlement.js';
|
|
3
|
+
/**
|
|
4
|
+
* THE settle of one boundary rule (round 13). The evaluator calls it with what
|
|
5
|
+
* it observed; the orchestrator's unread re-settle (`boundaryUnitLiveness` —
|
|
6
|
+
* `check boundaries` AND `--diff-against`) calls it with the claims an unread
|
|
7
|
+
* file could refute withdrawn. One function, so the two paths can never settle
|
|
8
|
+
* one rule differently:
|
|
9
|
+
*
|
|
10
|
+
* - units through core's `settleUnitLiveness`: a `from` inclusion is
|
|
11
|
+
* Coverage weight (dead → a shortfall, exit 2), every other list is
|
|
12
|
+
* Advisory (listed, ✓ withheld); a marked unit whose target does not
|
|
13
|
+
* exist is intended-empty (record B, printed `accepted by expectEmpty`),
|
|
14
|
+
* one whose target exists went live;
|
|
15
|
+
* - the rule's emptiness through `settleRuleEmptiness`: a rule whose every
|
|
16
|
+
* `from` inclusion is intended-empty and that matched no file is
|
|
17
|
+
* IntendedEmpty — accepted, never failOnEmpty; an unread governed file
|
|
18
|
+
* makes it partial, never "matched nothing";
|
|
19
|
+
* - the dead list is the settled `.dead` (unmarked units only), each worded
|
|
20
|
+
* by the one per-unit line.
|
|
21
|
+
*/
|
|
22
|
+
export declare function settleBoundaryRule(input: IBoundaryRuleSettleInput): IBoundaryRuleSettlement;
|
|
23
|
+
//# sourceMappingURL=settle-boundary-rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settle-boundary-rule.d.ts","sourceRoot":"","sources":["../../src/evaluate/settle-boundary-rule.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE/E;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,wBAAwB,GAAG,uBAAuB,CAiD3F"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { settleRuleEmptiness, settleUnitLiveness, UnitDeadWeight, unitStateLists, } from '@shrkcrft/core';
|
|
2
|
+
import { BoundaryMarkableList } from "../model/boundary-markable-list.js";
|
|
3
|
+
import { boundaryRuleFailsOnEmpty } from "../model/boundary-rule-scope.js";
|
|
4
|
+
import { boundaryUnitFinding } from "./boundary-unit-finding.js";
|
|
5
|
+
/**
|
|
6
|
+
* THE settle of one boundary rule (round 13). The evaluator calls it with what
|
|
7
|
+
* it observed; the orchestrator's unread re-settle (`boundaryUnitLiveness` —
|
|
8
|
+
* `check boundaries` AND `--diff-against`) calls it with the claims an unread
|
|
9
|
+
* file could refute withdrawn. One function, so the two paths can never settle
|
|
10
|
+
* one rule differently:
|
|
11
|
+
*
|
|
12
|
+
* - units through core's `settleUnitLiveness`: a `from` inclusion is
|
|
13
|
+
* Coverage weight (dead → a shortfall, exit 2), every other list is
|
|
14
|
+
* Advisory (listed, ✓ withheld); a marked unit whose target does not
|
|
15
|
+
* exist is intended-empty (record B, printed `accepted by expectEmpty`),
|
|
16
|
+
* one whose target exists went live;
|
|
17
|
+
* - the rule's emptiness through `settleRuleEmptiness`: a rule whose every
|
|
18
|
+
* `from` inclusion is intended-empty and that matched no file is
|
|
19
|
+
* IntendedEmpty — accepted, never failOnEmpty; an unread governed file
|
|
20
|
+
* makes it partial, never "matched nothing";
|
|
21
|
+
* - the dead list is the settled `.dead` (unmarked units only), each worded
|
|
22
|
+
* by the one per-unit line.
|
|
23
|
+
*/
|
|
24
|
+
export function settleBoundaryRule(input) {
|
|
25
|
+
const universeNote = input.fileUniverse === 'edges' ? ' (file list derived from import edges)' : '';
|
|
26
|
+
const settled = settleUnitLiveness({
|
|
27
|
+
unitLabel: 'scope globs',
|
|
28
|
+
acceptanceUnitLabel: 'selector units',
|
|
29
|
+
weight: UnitDeadWeight.Advisory,
|
|
30
|
+
observations: input.observations,
|
|
31
|
+
marks: input.rule.expectEmptyUnits ?? [],
|
|
32
|
+
deadSummary: `reached no governed file among ${input.filesScanned} scanned${universeNote}`,
|
|
33
|
+
});
|
|
34
|
+
const noFiles = `from globs matched 0 of ${input.filesScanned} scanned files: ${input.includeGlobs.join(', ')}`;
|
|
35
|
+
const emptiness = settleRuleEmptiness({
|
|
36
|
+
unitLabel: 'governed files',
|
|
37
|
+
filesMatched: input.filesInScope + input.exemptFilesInScope,
|
|
38
|
+
unitsMatched: input.filesInScope,
|
|
39
|
+
unread: input.unread,
|
|
40
|
+
emptiedByNegations: input.filesInScope === 0 && input.exemptFilesInScope > 0,
|
|
41
|
+
emptiedReason: `every file its from globs match is exempt (${input.exemptFilesInScope} file(s))`,
|
|
42
|
+
liveness: settled,
|
|
43
|
+
primaryLists: [BoundaryMarkableList.From],
|
|
44
|
+
failOnEmpty: boundaryRuleFailsOnEmpty(input.rule),
|
|
45
|
+
noFilesReason: noFiles,
|
|
46
|
+
noUnitsReason: noFiles,
|
|
47
|
+
});
|
|
48
|
+
const status = emptiness.skipped ? 'skipped' : input.violations > 0 ? 'failed' : 'passed';
|
|
49
|
+
const coverage = emptiness.coverage ??
|
|
50
|
+
settled.shortfall ?? { unit: 'scope globs', expected: 0, examined: 0 };
|
|
51
|
+
const deadUnits = settled.dead.map((u) => {
|
|
52
|
+
const f = boundaryUnitFinding(input.rule.id, u);
|
|
53
|
+
return {
|
|
54
|
+
ruleId: f.ruleId,
|
|
55
|
+
unit: f.unit,
|
|
56
|
+
selector: f.selector,
|
|
57
|
+
reason: f.reason,
|
|
58
|
+
...(u.cause !== undefined ? { cause: u.cause } : {}),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
const hasUnits = settled.dead.length + settled.intendedEmpty.length + settled.wentLive.length > 0;
|
|
62
|
+
return {
|
|
63
|
+
settled,
|
|
64
|
+
emptiness,
|
|
65
|
+
status,
|
|
66
|
+
...(emptiness.skipReason !== undefined ? { skipReason: emptiness.skipReason } : {}),
|
|
67
|
+
failedOnEmpty: emptiness.fails,
|
|
68
|
+
coverage,
|
|
69
|
+
...(settled.acceptance !== undefined ? { unitAcceptance: settled.acceptance } : {}),
|
|
70
|
+
...(hasUnits ? { units: unitStateLists(settled) } : {}),
|
|
71
|
+
deadUnits,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IBoundaryRuleCoverage } from './evaluate-boundaries.js';
|
|
2
|
+
import type { IBoundaryRuleSettlement } from './i-boundary-rule-settlement.js';
|
|
3
|
+
/**
|
|
4
|
+
* A rule's coverage record with a settlement applied (round 13): its status,
|
|
5
|
+
* skip, verdict coverage, acceptance, dead list, settled units and every row's
|
|
6
|
+
* `state`. The evaluator and the orchestrator's unread re-settle both build the
|
|
7
|
+
* record here, so neither can carry a field the other drops: a skip reason or
|
|
8
|
+
* an acceptance the new settlement does not carry is gone, never inherited.
|
|
9
|
+
*/
|
|
10
|
+
export declare function withBoundaryRuleSettlement(counts: Pick<IBoundaryRuleCoverage, 'ruleId' | 'severity' | 'filesInScope' | 'exemptFilesInScope' | 'edgesInScope' | 'violations' | 'suppressed' | 'fromGlobs' | 'forbidden' | 'allowed' | 'exemptions' | 'exceptions'>, s: IBoundaryRuleSettlement): IBoundaryRuleCoverage;
|
|
11
|
+
//# sourceMappingURL=with-boundary-rule-settlement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-boundary-rule-settlement.d.ts","sourceRoot":"","sources":["../../src/evaluate/with-boundary-rule-settlement.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE/E;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,IAAI,CACV,qBAAqB,EACnB,QAAQ,GACR,UAAU,GACV,cAAc,GACd,oBAAoB,GACpB,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,WAAW,GACX,WAAW,GACX,SAAS,GACT,YAAY,GACZ,YAAY,CACf,EACD,CAAC,EAAE,uBAAuB,GACzB,qBAAqB,CAoCvB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { RuleEmptiness } from '@shrkcrft/core';
|
|
2
|
+
import { BoundaryMarkableList } from "../model/boundary-markable-list.js";
|
|
3
|
+
/**
|
|
4
|
+
* A rule's coverage record with a settlement applied (round 13): its status,
|
|
5
|
+
* skip, verdict coverage, acceptance, dead list, settled units and every row's
|
|
6
|
+
* `state`. The evaluator and the orchestrator's unread re-settle both build the
|
|
7
|
+
* record here, so neither can carry a field the other drops: a skip reason or
|
|
8
|
+
* an acceptance the new settlement does not carry is gone, never inherited.
|
|
9
|
+
*/
|
|
10
|
+
export function withBoundaryRuleSettlement(counts, s) {
|
|
11
|
+
const stateOf = new Map(s.settled.units.map((u) => [`${u.list} ${u.unit}`, u.state]));
|
|
12
|
+
const withState = (row, list, unit) => {
|
|
13
|
+
const { state: _previous, ...bare } = row;
|
|
14
|
+
const state = stateOf.get(`${list} ${unit}`);
|
|
15
|
+
return (state !== undefined ? { ...bare, state } : bare);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
ruleId: counts.ruleId,
|
|
19
|
+
severity: counts.severity,
|
|
20
|
+
status: s.status,
|
|
21
|
+
...(s.skipReason !== undefined ? { skipReason: s.skipReason } : {}),
|
|
22
|
+
...(s.failedOnEmpty ? { failedOnEmpty: true } : {}),
|
|
23
|
+
...(s.emptiness.state === RuleEmptiness.IntendedEmpty ? { acceptedAsIntendedEmpty: true } : {}),
|
|
24
|
+
filesInScope: counts.filesInScope,
|
|
25
|
+
exemptFilesInScope: counts.exemptFilesInScope,
|
|
26
|
+
edgesInScope: counts.edgesInScope,
|
|
27
|
+
violations: counts.violations,
|
|
28
|
+
suppressed: counts.suppressed,
|
|
29
|
+
fromGlobs: counts.fromGlobs.map((g) => withState(g, BoundaryMarkableList.From, g.glob)),
|
|
30
|
+
forbidden: counts.forbidden.map((f) => withState(f, BoundaryMarkableList.ForbiddenImports, f.pattern)),
|
|
31
|
+
allowed: counts.allowed.map((a) => withState(a, BoundaryMarkableList.AllowedImports, a.pattern)),
|
|
32
|
+
exemptions: counts.exemptions.map((e) => e.origin === 'from-negation'
|
|
33
|
+
? withState(e, BoundaryMarkableList.From, `!${e.glob}`)
|
|
34
|
+
: e.origin === 'exemptFiles'
|
|
35
|
+
? withState(e, BoundaryMarkableList.ExemptFiles, e.glob)
|
|
36
|
+
: e),
|
|
37
|
+
exceptions: counts.exceptions,
|
|
38
|
+
deadUnits: s.deadUnits,
|
|
39
|
+
unitLiveness: s.settled.units,
|
|
40
|
+
coverage: s.coverage,
|
|
41
|
+
...(s.unitAcceptance !== undefined ? { unitAcceptance: s.unitAcceptance } : {}),
|
|
42
|
+
...(s.units !== undefined ? { units: s.units } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ScanZone } from '@shrkcrft/core';
|
|
1
2
|
/**
|
|
2
3
|
* Which lexical zone of a file a character belongs to.
|
|
3
4
|
*
|
|
@@ -14,14 +15,49 @@ export interface ICodeZone {
|
|
|
14
15
|
readonly kind: CodeZoneKind;
|
|
15
16
|
readonly start: number;
|
|
16
17
|
readonly end: number;
|
|
18
|
+
/**
|
|
19
|
+
* Set on a `string` zone delimited by BACKTICKS.
|
|
20
|
+
*
|
|
21
|
+
* A template literal is the one string form that routinely carries real
|
|
22
|
+
* content a rule must see — an inline `template:`, a SQL/GraphQL tagged
|
|
23
|
+
* query. Tagging it lets `code-and-templates` keep those bodies while still
|
|
24
|
+
* blanking comments and plain quoted strings, without a second lexer.
|
|
25
|
+
*/
|
|
26
|
+
readonly template?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Set on a `code` zone that is one REGEX LITERAL (`/…/flags`).
|
|
29
|
+
*
|
|
30
|
+
* A regex literal is code, but its body is opaque: the backtick in
|
|
31
|
+
* `` /`/g `` must not open a template literal and the `/*` in `/\/*$/` must
|
|
32
|
+
* not open a block comment. Either mis-lex used to swallow every real
|
|
33
|
+
* construct up to the next backtick / `*\/` in the file — a real `import()`
|
|
34
|
+
* after it vanished from the code zone, and every boundary surface passed
|
|
35
|
+
* over a forbidden import (round 11 review).
|
|
36
|
+
*/
|
|
37
|
+
readonly regex?: boolean;
|
|
17
38
|
}
|
|
39
|
+
/** Run `fn` with the `lexCodeZones` memo enabled (nestable), then clear it. */
|
|
40
|
+
export declare function withLexCache<T>(fn: () => T): T;
|
|
41
|
+
/** How many contents were actually lexed, and how many lexes the memo served (tests / perf locks). */
|
|
42
|
+
export declare function lexCodeZonesStats(): {
|
|
43
|
+
readonly lexed: number;
|
|
44
|
+
readonly memoHits: number;
|
|
45
|
+
};
|
|
46
|
+
/** Reset {@link lexCodeZonesStats}. */
|
|
47
|
+
export declare function resetLexCodeZonesStats(): void;
|
|
18
48
|
/**
|
|
19
49
|
* Split `content` into contiguous code / string / comment zones.
|
|
20
50
|
*
|
|
21
51
|
* A lexer, not a parser: it recognises the C/JS-family string quotes
|
|
22
|
-
* (`'`, `"`, `` ` ``)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
52
|
+
* (`'`, `"`, `` ` ``), comment forms (`//`, `/* … *\/`) and regex literals.
|
|
53
|
+
* A `/` in expression position opens a regex literal (a `code` zone tagged
|
|
54
|
+
* `regex`, see {@link ICodeZone.regex}) whose quotes, backticks and `/*` open
|
|
55
|
+
* nothing. Documented limits — `#`-comment languages are reported as code, and
|
|
56
|
+
* a regex right after `)` (`if (x) /re/`) is read as a division, i.e. with the
|
|
57
|
+
* pre-round-11 lexing of its body.
|
|
58
|
+
*
|
|
59
|
+
* Inside {@link withLexCache} the result is memoised by content and shared
|
|
60
|
+
* (frozen) between callers.
|
|
25
61
|
*/
|
|
26
62
|
export declare function lexCodeZones(content: string): readonly ICodeZone[];
|
|
27
63
|
/**
|
|
@@ -29,4 +65,60 @@ export declare function lexCodeZones(content: string): readonly ICodeZone[];
|
|
|
29
65
|
* recorded zone (e.g. past end-of-content).
|
|
30
66
|
*/
|
|
31
67
|
export declare function zoneAt(zones: readonly ICodeZone[], index: number): CodeZoneKind;
|
|
68
|
+
/**
|
|
69
|
+
* The lexed zone containing `index` (`undefined` past the end) — for a caller
|
|
70
|
+
* that needs a zone's BOUNDS, not just its kind: the import parser requires a
|
|
71
|
+
* specifier's quote to OPEN a string, which "is inside a string" cannot tell
|
|
72
|
+
* apart from a quote sitting inside a template literal.
|
|
73
|
+
*/
|
|
74
|
+
export declare function zoneContaining(zones: readonly ICodeZone[], index: number): ICodeZone | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Does `zone` keep a match starting at `index`?
|
|
77
|
+
*
|
|
78
|
+
* The ONE place that maps a {@link ScanZone} onto a lexed offset. Both raw-text
|
|
79
|
+
* engines call it — the policy plane per finding, the extraction DSL through
|
|
80
|
+
* {@link blankOutsideZone} — so "what does `code` mean" cannot drift between
|
|
81
|
+
* the two planes an author moves a rule between.
|
|
82
|
+
*
|
|
83
|
+
* Narrower zones require the match to START in the named zone: a pattern
|
|
84
|
+
* spanning out of a comment is judged by where it began, which is the only
|
|
85
|
+
* offset a regex engine reports.
|
|
86
|
+
*/
|
|
87
|
+
export declare function zoneKeepsAt(zone: ScanZone, zones: readonly ICodeZone[], index: number): boolean;
|
|
88
|
+
/** A run of characters {@link blankOutsideZone} replaced with whitespace. */
|
|
89
|
+
export interface IBlankedSpan {
|
|
90
|
+
readonly kind: CodeZoneKind;
|
|
91
|
+
readonly start: number;
|
|
92
|
+
readonly end: number;
|
|
93
|
+
}
|
|
94
|
+
/** What {@link blankOutsideZone} produced, plus what it had to remove to get there. */
|
|
95
|
+
export interface IBlankedContent {
|
|
96
|
+
/** Same LENGTH as the input — every dropped character became a space or newline. */
|
|
97
|
+
readonly content: string;
|
|
98
|
+
readonly blankedSpans: readonly IBlankedSpan[];
|
|
99
|
+
readonly blankedChars: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Return `content` with every span OUTSIDE `zone` replaced by equal-length
|
|
103
|
+
* whitespace.
|
|
104
|
+
*
|
|
105
|
+
* Blanking rather than deleting is the load-bearing detail: the result has the
|
|
106
|
+
* same length and the same newlines as the input, so every offset and every
|
|
107
|
+
* line number a downstream extractor reports is still the real one in the real
|
|
108
|
+
* file. That is what lets a zone apply to EVERY extractor kind — `array-members`,
|
|
109
|
+
* `call-args`, `object-keys` — instead of only the regex form, for free and with
|
|
110
|
+
* no per-extractor zone logic to keep in sync.
|
|
111
|
+
*/
|
|
112
|
+
export declare function blankOutsideZone(content: string, zone: ScanZone): IBlankedContent;
|
|
113
|
+
/**
|
|
114
|
+
* Blank every lexed zone whose KIND is in `kinds`, keeping everything else.
|
|
115
|
+
*
|
|
116
|
+
* The import extractor needs the one shape no {@link ScanZone} expresses:
|
|
117
|
+
* comments gone (so a commented-out import is not an edge, and an apostrophe
|
|
118
|
+
* in a comment cannot cut a real import clause short) while string literals
|
|
119
|
+
* stay (the specifier IS a string). `zones` is passed in so a caller that also
|
|
120
|
+
* judges offsets against the lex does not lex twice. Same blanking loop as
|
|
121
|
+
* {@link blankOutsideZone} — one implementation of "blank a zone".
|
|
122
|
+
*/
|
|
123
|
+
export declare function blankZoneKinds(content: string, zones: readonly ICodeZone[], kinds: ReadonlySet<CodeZoneKind>): IBlankedContent;
|
|
32
124
|
//# sourceMappingURL=code-zones.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code-zones.d.ts","sourceRoot":"","sources":["../../src/extract/code-zones.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"code-zones.d.ts","sourceRoot":"","sources":["../../src/extract/code-zones.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AA6HD,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQ9C;AAED,sGAAsG;AACtG,wBAAgB,iBAAiB,IAAI;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAEzF;AAED,uCAAuC;AACvC,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CAiBlE;AAgDD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAY/E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAWhG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,KAAK,EAAE,MAAM,GACZ,OAAO,CAsBT;AAED,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,CAAC;IAC/C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,eAAe,CAIjF;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,KAAK,EAAE,WAAW,CAAC,YAAY,CAAC,GAC/B,eAAe,CAEjB"}
|
|
@@ -1,13 +1,187 @@
|
|
|
1
1
|
import { skipComment, skipString } from "./scan-literals.js";
|
|
2
|
+
/** Characters after which a `/` starts an EXPRESSION (a regex literal), never a division. */
|
|
3
|
+
const REGEX_AFTER_PUNCTUATION = new Set([
|
|
4
|
+
'(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';',
|
|
5
|
+
'+', '-', '*', '%', '<', '>', '~', '^', '/',
|
|
6
|
+
]);
|
|
7
|
+
/** Keywords after which a `/` starts a regex literal (`return /x/.test(s)`). */
|
|
8
|
+
const REGEX_AFTER_KEYWORD = new Set([
|
|
9
|
+
'return', 'typeof', 'case', 'in', 'of', 'new', 'delete', 'void', 'throw',
|
|
10
|
+
'yield', 'await', 'instanceof', 'else', 'do',
|
|
11
|
+
]);
|
|
12
|
+
const isIdentifierChar = (c) => c !== undefined && /[A-Za-z0-9_$]/.test(c);
|
|
13
|
+
const isBlank = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === '\v';
|
|
14
|
+
/**
|
|
15
|
+
* Is a `/` whose previous significant code character sits at `prev` in
|
|
16
|
+
* EXPRESSION position — i.e. does it open a regex literal rather than divide?
|
|
17
|
+
* `prev < 0` is the start of the file. After an operand (an identifier, a
|
|
18
|
+
* number, `)` or `]`) it is a division; after an operator, an opening bracket
|
|
19
|
+
* or one of {@link REGEX_AFTER_KEYWORD} it is a regex. `)` is judged a division
|
|
20
|
+
* on purpose: `(a + b) / 2` is far more common than `if (x) /re/.test(y)`, and
|
|
21
|
+
* a missed regex only falls back to the pre-round-11 reading.
|
|
22
|
+
*/
|
|
23
|
+
function regexMayStartAfter(content, prev) {
|
|
24
|
+
if (prev < 0)
|
|
25
|
+
return true;
|
|
26
|
+
const ch = content[prev];
|
|
27
|
+
if (REGEX_AFTER_PUNCTUATION.has(ch))
|
|
28
|
+
return true;
|
|
29
|
+
if (!isIdentifierChar(ch))
|
|
30
|
+
return false;
|
|
31
|
+
let start = prev;
|
|
32
|
+
while (start > 0 && isIdentifierChar(content[start - 1]))
|
|
33
|
+
start -= 1;
|
|
34
|
+
// `obj.return / 2` — a property named like a keyword is an operand.
|
|
35
|
+
if (start > 0 && content[start - 1] === '.')
|
|
36
|
+
return false;
|
|
37
|
+
return REGEX_AFTER_KEYWORD.has(content.slice(start, prev + 1));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* If a regex literal starts at `start` (a `/` already judged to be in
|
|
41
|
+
* expression position, not followed by `/` or `*`), the index of its LAST
|
|
42
|
+
* character (the closing `/`, or its last flag); otherwise `-1`.
|
|
43
|
+
*
|
|
44
|
+
* Bounded to the line — a regex literal cannot contain a line terminator — so
|
|
45
|
+
* a `/` misjudged as a regex start costs at most the rest of one line, and a
|
|
46
|
+
* lone division with no closing `/` on its line is not a regex at all. A `/`
|
|
47
|
+
* inside a `[...]` class does not close the literal; `\` escapes one character.
|
|
48
|
+
*/
|
|
49
|
+
function skipRegexLiteral(content, start) {
|
|
50
|
+
let inClass = false;
|
|
51
|
+
for (let i = start + 1; i < content.length; i += 1) {
|
|
52
|
+
const c = content[i];
|
|
53
|
+
if (c === '\n' || c === '\r')
|
|
54
|
+
return -1;
|
|
55
|
+
if (c === '\\') {
|
|
56
|
+
const next = content[i + 1];
|
|
57
|
+
if (next === undefined || next === '\n' || next === '\r')
|
|
58
|
+
return -1;
|
|
59
|
+
i += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (inClass) {
|
|
63
|
+
if (c === ']')
|
|
64
|
+
inClass = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (c === '[') {
|
|
68
|
+
inClass = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === '/') {
|
|
72
|
+
let end = i;
|
|
73
|
+
while (end + 1 < content.length && /[A-Za-z]/.test(content[end + 1]))
|
|
74
|
+
end += 1;
|
|
75
|
+
return end;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return -1;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Does the `/` at `i` open a regex literal? Judged LAZILY, only for the rare
|
|
82
|
+
* `/` that is not a comment: scan back over blanks to the previous significant
|
|
83
|
+
* code character. The pending code region is `[codeStart, i)`; before it, the
|
|
84
|
+
* recorded zones partition `[0, codeStart)` — a comment is trivia (keep
|
|
85
|
+
* scanning before it), a string or regex literal is an operand (so the `/`
|
|
86
|
+
* divides), a code zone is scanned like the pending region, and running out of
|
|
87
|
+
* zones is the start of the file.
|
|
88
|
+
*
|
|
89
|
+
* This is exactly the previous-significant-character judgement the lexer used
|
|
90
|
+
* to track EAGERLY on every character (round 11 review: that bookkeeping made
|
|
91
|
+
* `lexCodeZones` ~1.8x slower under every zoned plane and the import parser).
|
|
92
|
+
*/
|
|
93
|
+
function slashOpensRegex(content, zones, codeStart, i) {
|
|
94
|
+
let lo = codeStart;
|
|
95
|
+
let j = i - 1;
|
|
96
|
+
let z = zones.length - 1;
|
|
97
|
+
for (;;) {
|
|
98
|
+
while (j >= lo && isBlank(content[j]))
|
|
99
|
+
j -= 1;
|
|
100
|
+
if (j >= lo)
|
|
101
|
+
return regexMayStartAfter(content, j);
|
|
102
|
+
if (z < 0)
|
|
103
|
+
return true;
|
|
104
|
+
const zone = zones[z];
|
|
105
|
+
z -= 1;
|
|
106
|
+
if (zone.kind === 'comment') {
|
|
107
|
+
lo = zone.start;
|
|
108
|
+
j = zone.start - 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (zone.kind === 'string' || zone.regex === true)
|
|
112
|
+
return false;
|
|
113
|
+
lo = zone.start;
|
|
114
|
+
j = zone.end - 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Memo of `lexCodeZones`, keyed by the CONTENT itself — so an entry can never
|
|
119
|
+
* be stale (the key IS the bytes), even across a spawn or a write. Enabled only
|
|
120
|
+
* inside {@link withLexCache}: one `shrk quality` run lexes the same file under
|
|
121
|
+
* the plane check, the coverage pass and every zoned rule of both; within a
|
|
122
|
+
* window each distinct content is lexed once. Bounded, and cleared when the
|
|
123
|
+
* outermost window closes.
|
|
124
|
+
*/
|
|
125
|
+
const LEX_MEMO = new Map();
|
|
126
|
+
const LEX_MEMO_LIMIT = 50_000;
|
|
127
|
+
let lexWindowDepth = 0;
|
|
128
|
+
let lexCount = 0;
|
|
129
|
+
let lexMemoHits = 0;
|
|
130
|
+
/** Run `fn` with the `lexCodeZones` memo enabled (nestable), then clear it. */
|
|
131
|
+
export function withLexCache(fn) {
|
|
132
|
+
lexWindowDepth += 1;
|
|
133
|
+
try {
|
|
134
|
+
return fn();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
lexWindowDepth -= 1;
|
|
138
|
+
if (lexWindowDepth === 0)
|
|
139
|
+
LEX_MEMO.clear();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** How many contents were actually lexed, and how many lexes the memo served (tests / perf locks). */
|
|
143
|
+
export function lexCodeZonesStats() {
|
|
144
|
+
return { lexed: lexCount, memoHits: lexMemoHits };
|
|
145
|
+
}
|
|
146
|
+
/** Reset {@link lexCodeZonesStats}. */
|
|
147
|
+
export function resetLexCodeZonesStats() {
|
|
148
|
+
lexCount = 0;
|
|
149
|
+
lexMemoHits = 0;
|
|
150
|
+
}
|
|
2
151
|
/**
|
|
3
152
|
* Split `content` into contiguous code / string / comment zones.
|
|
4
153
|
*
|
|
5
154
|
* A lexer, not a parser: it recognises the C/JS-family string quotes
|
|
6
|
-
* (`'`, `"`, `` ` ``)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
155
|
+
* (`'`, `"`, `` ` ``), comment forms (`//`, `/* … *\/`) and regex literals.
|
|
156
|
+
* A `/` in expression position opens a regex literal (a `code` zone tagged
|
|
157
|
+
* `regex`, see {@link ICodeZone.regex}) whose quotes, backticks and `/*` open
|
|
158
|
+
* nothing. Documented limits — `#`-comment languages are reported as code, and
|
|
159
|
+
* a regex right after `)` (`if (x) /re/`) is read as a division, i.e. with the
|
|
160
|
+
* pre-round-11 lexing of its body.
|
|
161
|
+
*
|
|
162
|
+
* Inside {@link withLexCache} the result is memoised by content and shared
|
|
163
|
+
* (frozen) between callers.
|
|
9
164
|
*/
|
|
10
165
|
export function lexCodeZones(content) {
|
|
166
|
+
if (lexWindowDepth > 0) {
|
|
167
|
+
const hit = LEX_MEMO.get(content);
|
|
168
|
+
if (hit !== undefined) {
|
|
169
|
+
lexMemoHits += 1;
|
|
170
|
+
return hit;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const zones = lexUncached(content);
|
|
174
|
+
lexCount += 1;
|
|
175
|
+
if (lexWindowDepth > 0) {
|
|
176
|
+
if (LEX_MEMO.size >= LEX_MEMO_LIMIT)
|
|
177
|
+
LEX_MEMO.clear();
|
|
178
|
+
const frozen = Object.freeze(zones);
|
|
179
|
+
LEX_MEMO.set(content, frozen);
|
|
180
|
+
return frozen;
|
|
181
|
+
}
|
|
182
|
+
return zones;
|
|
183
|
+
}
|
|
184
|
+
function lexUncached(content) {
|
|
11
185
|
const zones = [];
|
|
12
186
|
let codeStart = 0;
|
|
13
187
|
const flushCode = (upTo) => {
|
|
@@ -19,7 +193,12 @@ export function lexCodeZones(content) {
|
|
|
19
193
|
if (c === '"' || c === "'" || c === '`') {
|
|
20
194
|
flushCode(i);
|
|
21
195
|
const end = skipString(content, i);
|
|
22
|
-
zones.push({
|
|
196
|
+
zones.push({
|
|
197
|
+
kind: 'string',
|
|
198
|
+
start: i,
|
|
199
|
+
end: end + 1,
|
|
200
|
+
...(c === '`' ? { template: true } : {}),
|
|
201
|
+
});
|
|
23
202
|
i = end;
|
|
24
203
|
codeStart = i + 1;
|
|
25
204
|
continue;
|
|
@@ -31,6 +210,17 @@ export function lexCodeZones(content) {
|
|
|
31
210
|
zones.push({ kind: 'comment', start: i, end: end + 1 });
|
|
32
211
|
i = end;
|
|
33
212
|
codeStart = i + 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (slashOpensRegex(content, zones, codeStart, i)) {
|
|
216
|
+
const reEnd = skipRegexLiteral(content, i);
|
|
217
|
+
if (reEnd >= 0) {
|
|
218
|
+
flushCode(i);
|
|
219
|
+
zones.push({ kind: 'code', start: i, end: reEnd + 1, regex: true });
|
|
220
|
+
i = reEnd;
|
|
221
|
+
codeStart = i + 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
34
224
|
}
|
|
35
225
|
}
|
|
36
226
|
}
|
|
@@ -57,3 +247,122 @@ export function zoneAt(zones, index) {
|
|
|
57
247
|
}
|
|
58
248
|
return 'code';
|
|
59
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* The lexed zone containing `index` (`undefined` past the end) — for a caller
|
|
252
|
+
* that needs a zone's BOUNDS, not just its kind: the import parser requires a
|
|
253
|
+
* specifier's quote to OPEN a string, which "is inside a string" cannot tell
|
|
254
|
+
* apart from a quote sitting inside a template literal.
|
|
255
|
+
*/
|
|
256
|
+
export function zoneContaining(zones, index) {
|
|
257
|
+
let lo = 0;
|
|
258
|
+
let hi = zones.length - 1;
|
|
259
|
+
while (lo <= hi) {
|
|
260
|
+
const mid = (lo + hi) >> 1;
|
|
261
|
+
const z = zones[mid];
|
|
262
|
+
if (index < z.start)
|
|
263
|
+
hi = mid - 1;
|
|
264
|
+
else if (index >= z.end)
|
|
265
|
+
lo = mid + 1;
|
|
266
|
+
else
|
|
267
|
+
return z;
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Does `zone` keep a match starting at `index`?
|
|
273
|
+
*
|
|
274
|
+
* The ONE place that maps a {@link ScanZone} onto a lexed offset. Both raw-text
|
|
275
|
+
* engines call it — the policy plane per finding, the extraction DSL through
|
|
276
|
+
* {@link blankOutsideZone} — so "what does `code` mean" cannot drift between
|
|
277
|
+
* the two planes an author moves a rule between.
|
|
278
|
+
*
|
|
279
|
+
* Narrower zones require the match to START in the named zone: a pattern
|
|
280
|
+
* spanning out of a comment is judged by where it began, which is the only
|
|
281
|
+
* offset a regex engine reports.
|
|
282
|
+
*/
|
|
283
|
+
export function zoneKeepsAt(zone, zones, index) {
|
|
284
|
+
if (zone === 'all')
|
|
285
|
+
return true;
|
|
286
|
+
let lo = 0;
|
|
287
|
+
let hi = zones.length - 1;
|
|
288
|
+
let hit;
|
|
289
|
+
while (lo <= hi) {
|
|
290
|
+
const mid = (lo + hi) >> 1;
|
|
291
|
+
const z = zones[mid];
|
|
292
|
+
if (index < z.start)
|
|
293
|
+
hi = mid - 1;
|
|
294
|
+
else if (index >= z.end)
|
|
295
|
+
lo = mid + 1;
|
|
296
|
+
else {
|
|
297
|
+
hit = z;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Outside every recorded zone (e.g. past end-of-content) reads as code.
|
|
302
|
+
const kind = hit?.kind ?? 'code';
|
|
303
|
+
if (zone === 'code')
|
|
304
|
+
return kind === 'code';
|
|
305
|
+
if (zone === 'strings')
|
|
306
|
+
return kind === 'string';
|
|
307
|
+
if (zone === 'comments')
|
|
308
|
+
return kind === 'comment';
|
|
309
|
+
// code-and-templates
|
|
310
|
+
return kind === 'code' || (kind === 'string' && hit?.template === true);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Return `content` with every span OUTSIDE `zone` replaced by equal-length
|
|
314
|
+
* whitespace.
|
|
315
|
+
*
|
|
316
|
+
* Blanking rather than deleting is the load-bearing detail: the result has the
|
|
317
|
+
* same length and the same newlines as the input, so every offset and every
|
|
318
|
+
* line number a downstream extractor reports is still the real one in the real
|
|
319
|
+
* file. That is what lets a zone apply to EVERY extractor kind — `array-members`,
|
|
320
|
+
* `call-args`, `object-keys` — instead of only the regex form, for free and with
|
|
321
|
+
* no per-extractor zone logic to keep in sync.
|
|
322
|
+
*/
|
|
323
|
+
export function blankOutsideZone(content, zone) {
|
|
324
|
+
if (zone === 'all')
|
|
325
|
+
return { content, blankedSpans: [], blankedChars: 0 };
|
|
326
|
+
const zones = lexCodeZones(content);
|
|
327
|
+
return blankZonesWhere(content, zones, (z) => !zoneKeepsAt(zone, zones, z.start));
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Blank every lexed zone whose KIND is in `kinds`, keeping everything else.
|
|
331
|
+
*
|
|
332
|
+
* The import extractor needs the one shape no {@link ScanZone} expresses:
|
|
333
|
+
* comments gone (so a commented-out import is not an edge, and an apostrophe
|
|
334
|
+
* in a comment cannot cut a real import clause short) while string literals
|
|
335
|
+
* stay (the specifier IS a string). `zones` is passed in so a caller that also
|
|
336
|
+
* judges offsets against the lex does not lex twice. Same blanking loop as
|
|
337
|
+
* {@link blankOutsideZone} — one implementation of "blank a zone".
|
|
338
|
+
*/
|
|
339
|
+
export function blankZoneKinds(content, zones, kinds) {
|
|
340
|
+
return blankZonesWhere(content, zones, (z) => kinds.has(z.kind));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The ONE blanking loop: replace each selected zone with equal-length
|
|
344
|
+
* whitespace. Newlines survive so line numbers — and any `m`-flag anchor —
|
|
345
|
+
* hold. Built from slices (not a per-character array) so a large file costs
|
|
346
|
+
* one pass.
|
|
347
|
+
*/
|
|
348
|
+
function blankZonesWhere(content, zones, blank) {
|
|
349
|
+
const parts = [];
|
|
350
|
+
const blankedSpans = [];
|
|
351
|
+
let blankedChars = 0;
|
|
352
|
+
let cursor = 0;
|
|
353
|
+
for (const z of zones) {
|
|
354
|
+
if (!blank(z))
|
|
355
|
+
continue;
|
|
356
|
+
const start = Math.min(z.start, content.length);
|
|
357
|
+
const end = Math.min(z.end, content.length);
|
|
358
|
+
if (start > cursor)
|
|
359
|
+
parts.push(content.slice(cursor, start));
|
|
360
|
+
parts.push(content.slice(start, end).replace(/[^\n]/g, ' '));
|
|
361
|
+
cursor = end;
|
|
362
|
+
blankedSpans.push({ kind: z.kind, start: z.start, end: z.end });
|
|
363
|
+
blankedChars += z.end - z.start;
|
|
364
|
+
}
|
|
365
|
+
if (cursor < content.length)
|
|
366
|
+
parts.push(content.slice(cursor));
|
|
367
|
+
return { content: parts.join(''), blankedSpans, blankedChars };
|
|
368
|
+
}
|