@metaobjectsdev/cli 1.0.10 → 1.0.12
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/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +5 -1
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/fmt.d.ts +2 -0
- package/dist/src/commands/fmt.d.ts.map +1 -0
- package/dist/src/commands/fmt.js +80 -0
- package/dist/src/commands/fmt.js.map +1 -0
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +7 -0
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +1 -0
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +74 -1
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +76 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +36 -0
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +20 -0
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/deprecation-lint.d.ts +11 -0
- package/dist/src/lib/deprecation-lint.d.ts.map +1 -0
- package/dist/src/lib/deprecation-lint.js +273 -0
- package/dist/src/lib/deprecation-lint.js.map +1 -0
- package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
- package/dist/src/lib/fk-index-advisory.js +4 -1
- package/dist/src/lib/fk-index-advisory.js.map +1 -1
- package/dist/src/lib/fmt-engine.d.ts +22 -0
- package/dist/src/lib/fmt-engine.d.ts.map +1 -0
- package/dist/src/lib/fmt-engine.js +128 -0
- package/dist/src/lib/fmt-engine.js.map +1 -0
- package/dist/src/lib/name-lint.d.ts +12 -0
- package/dist/src/lib/name-lint.d.ts.map +1 -0
- package/dist/src/lib/name-lint.js +95 -0
- package/dist/src/lib/name-lint.js.map +1 -0
- package/dist/src/lib/runtime-boundary-advisory.d.ts +24 -0
- package/dist/src/lib/runtime-boundary-advisory.d.ts.map +1 -0
- package/dist/src/lib/runtime-boundary-advisory.js +47 -0
- package/dist/src/lib/runtime-boundary-advisory.js.map +1 -0
- package/package.json +10 -10
- package/src/commands/eject.ts +6 -1
- package/src/commands/fmt.ts +90 -0
- package/src/commands/gen.ts +9 -0
- package/src/commands/migrate.ts +1 -0
- package/src/commands/verify.ts +90 -1
- package/src/index.ts +76 -0
- package/src/lib/args.ts +47 -0
- package/src/lib/deprecation-lint.ts +327 -0
- package/src/lib/fk-index-advisory.ts +4 -1
- package/src/lib/fmt-engine.ts +179 -0
- package/src/lib/name-lint.ts +106 -0
- package/src/lib/runtime-boundary-advisory.ts +74 -0
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ COMMANDS:
|
|
|
41
41
|
deps list One line per locked dependency: name, version, hash, node/package summary
|
|
42
42
|
types [query] Search the metadata vocabulary (types, subtypes, @attrs) by name or description
|
|
43
43
|
export Flatten loaded metadata to one canonical JSON artifact
|
|
44
|
+
fmt [<file>...] Rewrite metadata files into canonical form; --check lists drift, exits non-zero
|
|
44
45
|
docs [<project-root>] --out <dir> Generate neutral metadata documentation (entity + template pages; --site for HTML site)
|
|
45
46
|
verify Drift gate — subverbs: --templates / --db / --codegen / --docs / --deps (bare = --templates)
|
|
46
47
|
upgrade Rewrite retired metadata vocabulary (previews; --apply writes)
|
|
@@ -86,6 +87,21 @@ GEN FLAGS:
|
|
|
86
87
|
EXPORT FLAGS:
|
|
87
88
|
--out <file> Write output to a file (default: stdout)
|
|
88
89
|
|
|
90
|
+
FMT FLAGS:
|
|
91
|
+
[<file>...] Optional explicit file(s) to format — must be members of this
|
|
92
|
+
project's resolved metadata sources (resolveCollection()); omit
|
|
93
|
+
to format every one of them.
|
|
94
|
+
--check List files that are not canonical and exit non-zero; changes nothing.
|
|
95
|
+
A file whose formatting would change the LOADED MODEL's meaning
|
|
96
|
+
is never rewritten — reported as an error instead (safety check).
|
|
97
|
+
A YAML file is always reported as skipped: no canonical YAML
|
|
98
|
+
emitter exists yet (ADR-0006 — JSON is the canonical interchange
|
|
99
|
+
form). A file declaring 'overlay: true' anywhere — whether or not
|
|
100
|
+
a same-file base exists to merge into — is skipped too: a
|
|
101
|
+
formatter never changes structure, and buildTree's ordinary
|
|
102
|
+
find-or-reuse behavior would otherwise silently fold a same-file
|
|
103
|
+
base + overlay into one node, dropping the overlay marker.
|
|
104
|
+
|
|
89
105
|
DOCS FLAGS:
|
|
90
106
|
[<project-root>] PROJECT ROOT to resolve metadata from — the directory that CONTAINS
|
|
91
107
|
your metadata, NOT the metadata directory. Passing it SCOPES the run
|
|
@@ -127,6 +143,10 @@ VERIFY FLAGS (ADR-0021 D2 — explicit subverbs; combine any; exit 1 on ANY drif
|
|
|
127
143
|
--no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
|
|
128
144
|
--no-overlay-lint Suppress the advisory overlay-redeclaration AUTHORING lint
|
|
129
145
|
(never a gate — this lint can't fail the build)
|
|
146
|
+
--no-name-lint Suppress the advisory node-name AUTHORING lint (whitespace
|
|
147
|
+
in a name; never a gate — this lint can't fail the build)
|
|
148
|
+
--no-deprecation-lint Suppress the advisory deprecated-reference AUTHORING lint
|
|
149
|
+
(never a gate — this lint can't fail the build)
|
|
130
150
|
--limit <n|all> How many advisory lines TEXT output prints per section before
|
|
131
151
|
it truncates (default 20). Never applies to --format
|
|
132
152
|
toon/json, which carry every finding and every diagnostic.
|
|
@@ -320,6 +340,10 @@ FLAGS:
|
|
|
320
340
|
--no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
|
|
321
341
|
--no-overlay-lint Suppress the advisory overlay-redeclaration AUTHORING lint —
|
|
322
342
|
never a gate; this lint can't fail the build
|
|
343
|
+
--no-name-lint Suppress the advisory node-name AUTHORING lint (whitespace in
|
|
344
|
+
a name) — never a gate; this lint can't fail the build
|
|
345
|
+
--no-deprecation-lint Suppress the advisory deprecated-reference AUTHORING lint —
|
|
346
|
+
never a gate; this lint can't fail the build
|
|
323
347
|
--limit <n|all> Advisory lines TEXT output prints PER SECTION before truncating
|
|
324
348
|
(default 20; per-section so the authoring lint can never push
|
|
325
349
|
the gate's own warnings off the end)
|
|
@@ -350,6 +374,21 @@ redeclaration lacks 'overlay: true' — today's default merge rule reuses it
|
|
|
350
374
|
silently, but the same unflagged redeclaration silently becomes a NEW object
|
|
351
375
|
the day the target is renamed or removed upstream. Warnings only — it can
|
|
352
376
|
never fail the build. Opt out with --no-overlay-lint or META_NO_OVERLAY_LINT=1.
|
|
377
|
+
|
|
378
|
+
verify also prints an ADVISORY node-name authoring lint, in its own section: any
|
|
379
|
+
node (object, field, identity, relationship, ...) whose name contains whitespace.
|
|
380
|
+
Leading or trailing whitespace is almost certainly a copy-paste accident — rename
|
|
381
|
+
the node, and pin @column on a field whose physical column must keep its name.
|
|
382
|
+
Requirement names are left to the requirement lint above. Warnings only — it can
|
|
383
|
+
never fail the build. Opt out with --no-name-lint or META_NO_NAME_LINT=1.
|
|
384
|
+
|
|
385
|
+
verify also prints an ADVISORY deprecated-reference authoring lint, in its own
|
|
386
|
+
section: any node that depends on a node carrying the documentation @deprecated
|
|
387
|
+
attr, via extends, @objectRef, @references, or an origin's @from/@of/@via. Where
|
|
388
|
+
the deprecated node also carries @replacedBy, the finding names the replacement.
|
|
389
|
+
A node referencing itself (a recursive FK, a same-entity passthrough) is not a
|
|
390
|
+
finding. Warnings only — it can never fail the build. Opt out with
|
|
391
|
+
--no-deprecation-lint or META_NO_DEPRECATION_LINT=1.
|
|
353
392
|
`,
|
|
354
393
|
export: `meta export — flatten loaded metadata to one canonical JSON artifact
|
|
355
394
|
|
|
@@ -359,6 +398,39 @@ USAGE:
|
|
|
359
398
|
FLAGS:
|
|
360
399
|
--out <file> Write output to a file (default: stdout)
|
|
361
400
|
--help, -h Print this help
|
|
401
|
+
`,
|
|
402
|
+
fmt: `meta fmt — rewrite metadata files into canonical form
|
|
403
|
+
|
|
404
|
+
USAGE:
|
|
405
|
+
meta fmt [<file>...] [flags]
|
|
406
|
+
|
|
407
|
+
Formats every JSON metadata file this project's resolved sources contain
|
|
408
|
+
(resolveCollection() — never a hardcoded default directory), or just the
|
|
409
|
+
files named. Each file is formatted as its OWN content (own-mode,
|
|
410
|
+
declared-here layer only) — never inlining an inherited member or another
|
|
411
|
+
file's overlay.
|
|
412
|
+
|
|
413
|
+
FLAGS:
|
|
414
|
+
[<file>...] Optional explicit file(s) — must be members of this project's
|
|
415
|
+
resolved metadata sources; omit to format every one of them.
|
|
416
|
+
--check List files that are not canonical and exit non-zero; changes
|
|
417
|
+
nothing on disk.
|
|
418
|
+
--help, -h Print this help
|
|
419
|
+
|
|
420
|
+
SAFETY: before writing any file, fmt reloads the whole project with that file's
|
|
421
|
+
candidate content swapped in and requires the loaded model's canonical form to
|
|
422
|
+
be byte-identical to the untouched baseline. A file that fails this check is
|
|
423
|
+
left exactly as it was and reported as an error — fmt never changes meaning.
|
|
424
|
+
|
|
425
|
+
YAML: left untouched and reported as skipped — no canonical YAML emitter
|
|
426
|
+
exists yet (ADR-0006: JSON is the canonical interchange form; YAML is an
|
|
427
|
+
authoring format).
|
|
428
|
+
|
|
429
|
+
OVERLAY: a file declaring 'overlay: true' ANYWHERE in its tree is reported as
|
|
430
|
+
skipped — not only when no base exists in the same file. A formatter never
|
|
431
|
+
changes structure: even when a same-file base IS present, merging it with
|
|
432
|
+
the overlay (what the ordinary loader would do) would drop the overlay
|
|
433
|
+
marker from the output, so fmt refuses that too and leaves the file as-is.
|
|
362
434
|
`,
|
|
363
435
|
docs: `meta docs — generate neutral metadata documentation (entity + template pages)
|
|
364
436
|
|
|
@@ -634,6 +706,10 @@ export async function run(argv: string[]): Promise<number> {
|
|
|
634
706
|
const { exportCommand } = await import("./commands/export.js");
|
|
635
707
|
return exportCommand(rest, cwd);
|
|
636
708
|
}
|
|
709
|
+
case "fmt": {
|
|
710
|
+
const { fmtCommand } = await import("./commands/fmt.js");
|
|
711
|
+
return fmtCommand(rest, cwd);
|
|
712
|
+
}
|
|
637
713
|
case "types": {
|
|
638
714
|
const { typesCommand } = await import("./commands/types.js");
|
|
639
715
|
return typesCommand(rest, explicitFormat);
|
package/src/lib/args.ts
CHANGED
|
@@ -190,6 +190,35 @@ export function parseExportArgs(argv: string[]): ExportFlags {
|
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// fmt flags (#304)
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
export interface FmtFlags {
|
|
198
|
+
check: boolean;
|
|
199
|
+
/** Optional explicit file list — narrows the run to these files (must be
|
|
200
|
+
* members of the project's resolved metadata sources). Empty = every file. */
|
|
201
|
+
files: string[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The flag table `parseFmtArgs` parses. Exported so the help text can be gated against it. */
|
|
205
|
+
export const FMT_OPTIONS = {
|
|
206
|
+
check: { type: "boolean", default: false },
|
|
207
|
+
} as const;
|
|
208
|
+
|
|
209
|
+
export function parseFmtArgs(argv: string[]): FmtFlags {
|
|
210
|
+
const { values, positionals } = parseArgs({
|
|
211
|
+
args: argv,
|
|
212
|
+
options: FMT_OPTIONS,
|
|
213
|
+
strict: true,
|
|
214
|
+
allowPositionals: true,
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
check: !!values.check,
|
|
218
|
+
files: positionals,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
193
222
|
// ---------------------------------------------------------------------------
|
|
194
223
|
// shared DB-connection vocab (used by both verify --db and migrate)
|
|
195
224
|
// ---------------------------------------------------------------------------
|
|
@@ -362,6 +391,20 @@ export interface VerifyFlags {
|
|
|
362
391
|
* lint has no gate half at all — it can never fail the build).
|
|
363
392
|
*/
|
|
364
393
|
noOverlayLint: boolean;
|
|
394
|
+
/**
|
|
395
|
+
* Suppress the advisory node-NAME authoring lint — whitespace in a node's
|
|
396
|
+
* `name` (a trailing space copied from legacy XML loads clean on every port).
|
|
397
|
+
* Advisory only, like its siblings: it has no gate half and can never fail
|
|
398
|
+
* the build.
|
|
399
|
+
*/
|
|
400
|
+
noNameLint: boolean;
|
|
401
|
+
/**
|
|
402
|
+
* Suppress the advisory deprecated-REFERENCE authoring lint (#305) — a node
|
|
403
|
+
* depending (extends/@objectRef/@references/origin @from/@of/@via) on a
|
|
404
|
+
* `deprecated` node. Advisory only, like its siblings: it has no gate half
|
|
405
|
+
* and can never fail the build.
|
|
406
|
+
*/
|
|
407
|
+
noDeprecationLint: boolean;
|
|
365
408
|
/**
|
|
366
409
|
* ADR-0023 strict-attr load opt-OUT (#96). `verify` is strict-by-default — an
|
|
367
410
|
* undeclared/typo'd own `@attr` fails verify (ERR_UNKNOWN_ATTR). `--lax`
|
|
@@ -403,6 +446,8 @@ export const VERIFY_OPTIONS = {
|
|
|
403
446
|
"no-antipatterns": { type: "boolean", default: false },
|
|
404
447
|
"no-requirement-lint": { type: "boolean", default: false },
|
|
405
448
|
"no-overlay-lint": { type: "boolean", default: false },
|
|
449
|
+
"no-name-lint": { type: "boolean", default: false },
|
|
450
|
+
"no-deprecation-lint": { type: "boolean", default: false },
|
|
406
451
|
lax: { type: "boolean", default: false },
|
|
407
452
|
"d1": { type: "string" },
|
|
408
453
|
"remote": { type: "boolean", default: false },
|
|
@@ -487,6 +532,8 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
487
532
|
noAntipatterns: !!values["no-antipatterns"],
|
|
488
533
|
noRequirementLint: !!values["no-requirement-lint"],
|
|
489
534
|
noOverlayLint: !!values["no-overlay-lint"],
|
|
535
|
+
noNameLint: !!values["no-name-lint"],
|
|
536
|
+
noDeprecationLint: !!values["no-deprecation-lint"],
|
|
490
537
|
lax: !!values.lax,
|
|
491
538
|
d1: values.d1 as string | undefined,
|
|
492
539
|
remote: !!values.remote,
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// `meta verify` — the deprecated-REFERENCE authoring lint (#305).
|
|
2
|
+
//
|
|
3
|
+
// `deprecated` / `replacedBy` are registered documentation common-attrs in every
|
|
4
|
+
// port (doc-provider.ts) and nothing reads them: no validation pass, no `verify`
|
|
5
|
+
// check, no codegen behavior. A deprecation you cannot enforce is a comment.
|
|
6
|
+
//
|
|
7
|
+
// Smallest useful version (approved scope): warn when a node DEPENDS on a node
|
|
8
|
+
// carrying `deprecated`, through the reference kinds the metamodel already has —
|
|
9
|
+
// `extends`, `@objectRef`, `@references`, and an origin's `@from` / `@of` / `@via`
|
|
10
|
+
// path. Where the deprecated node also carries `replacedBy`, the diagnostic names
|
|
11
|
+
// the replacement. Never fails the build — ADVISORY ONLY, the same discipline as
|
|
12
|
+
// every sibling authoring lint in this directory (name-lint.ts, overlay-lint.ts,
|
|
13
|
+
// requirement-lint.ts).
|
|
14
|
+
//
|
|
15
|
+
// Explicitly NOT in scope here (tracked separately): a deprecation DATE that
|
|
16
|
+
// escalates warn -> error once passed, and per-finding suppressions with a
|
|
17
|
+
// recorded reason (#302) — both need their own design.
|
|
18
|
+
//
|
|
19
|
+
// SELF-REFERENCE IS NOT A FINDING, but ONLY the literal case: the same node, or
|
|
20
|
+
// one of the node's own ANCESTORS (the recursive-FK shape: `Category.parentId`
|
|
21
|
+
// is a child of `Category` and points back at `Category` itself — not a
|
|
22
|
+
// dependency on a different thing, it IS the deprecated thing). A DIFFERENT
|
|
23
|
+
// node that merely happens to live under the same entity — e.g. a passthrough
|
|
24
|
+
// field reading a DEPRECATED SIBLING field on its own entity — is a real
|
|
25
|
+
// cross-node dependency and DOES warn; sharing an ancestor is not sharing an
|
|
26
|
+
// identity. Guarded by `isSelfReference` below.
|
|
27
|
+
//
|
|
28
|
+
// Resolution reads the LOADED, FROZEN model only, via the RESOLVING accessors
|
|
29
|
+
// (ADR-0039: `attr()`, `children()`, `superData`) — so a target that inherits
|
|
30
|
+
// `deprecated` from its own abstract base still counts, and a reference
|
|
31
|
+
// inherited through `extends` is reported once, at its declaration
|
|
32
|
+
// (`ownChildren()`, the same sanctioned case name-lint.ts uses).
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
DOC_ATTR_DEPRECATED,
|
|
36
|
+
DOC_ATTR_REPLACED_BY,
|
|
37
|
+
TYPE_FIELD,
|
|
38
|
+
TYPE_IDENTITY,
|
|
39
|
+
TYPE_RELATIONSHIP,
|
|
40
|
+
TYPE_ORIGIN,
|
|
41
|
+
IDENTITY_SUBTYPE_REFERENCE,
|
|
42
|
+
IDENTITY_REFERENCE_ATTR_REFERENCES,
|
|
43
|
+
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
44
|
+
FIELD_ATTR_OBJECT_REF,
|
|
45
|
+
// @via is spelled identically on every origin subtype (passthrough / aggregate /
|
|
46
|
+
// first); @of is identical on aggregate and first. One constant each, reused —
|
|
47
|
+
// see origin-constants.ts.
|
|
48
|
+
ORIGIN_PASSTHROUGH_ATTR_FROM,
|
|
49
|
+
ORIGIN_PASSTHROUGH_ATTR_VIA,
|
|
50
|
+
ORIGIN_AGGREGATE_ATTR_OF,
|
|
51
|
+
isMetaObject,
|
|
52
|
+
resolveObjectRef,
|
|
53
|
+
type MetaData,
|
|
54
|
+
type MetaObject,
|
|
55
|
+
} from "@metaobjectsdev/metadata";
|
|
56
|
+
import type { Diagnostic } from "./requirement-check.js";
|
|
57
|
+
|
|
58
|
+
export const WARN_DEPRECATED_REFERENCE = "WARN_DEPRECATED_REFERENCE";
|
|
59
|
+
|
|
60
|
+
function warn(path: string, message: string): Diagnostic {
|
|
61
|
+
return { severity: "warn", code: WARN_DEPRECATED_REFERENCE, path, message };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// addressing — mirrors name-lint.ts's topLevelAddress/segment, applied to every
|
|
66
|
+
// node (not only ones whose own name is the finding).
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function packageOf(node: MetaData): string {
|
|
70
|
+
return node.package ?? node.fileDefaultPackage ?? "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Walk up to the nearest enclosing `object.*` — every field/identity/relationship/
|
|
74
|
+
* origin is nested under exactly one (objects are never nested in this metamodel). */
|
|
75
|
+
function owningObject(node: MetaData): MetaObject | undefined {
|
|
76
|
+
let cur: MetaData | undefined = node.parent;
|
|
77
|
+
while (cur !== undefined && !isMetaObject(cur)) cur = cur.parent;
|
|
78
|
+
return cur;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A human-readable address for a node: the object's FQN, or `<object FQN>.<name>` for
|
|
82
|
+
* something nested inside one. Falls back to the bare name (an abstract field
|
|
83
|
+
* declared at root, outside any object) when there is no enclosing object. */
|
|
84
|
+
function addressOf(node: MetaData): string {
|
|
85
|
+
if (isMetaObject(node)) return node.resolutionKey();
|
|
86
|
+
const owner = owningObject(node);
|
|
87
|
+
return owner !== undefined ? `${owner.resolutionKey()}.${node.name}` : node.name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// self-reference guard
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
function isAncestorOf(maybeAncestor: MetaData, node: MetaData): boolean {
|
|
95
|
+
let cur: MetaData | undefined = node.parent;
|
|
96
|
+
while (cur !== undefined) {
|
|
97
|
+
if (cur === maybeAncestor) return true;
|
|
98
|
+
cur = cur.parent;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* True when `target` is not really an OTHER node from `referrer`'s point of
|
|
105
|
+
* view: the same node, or an ANCESTOR of it (the referrer's own enclosing
|
|
106
|
+
* object/field — the recursive-FK shape). Deliberately NOT symmetric: a
|
|
107
|
+
* DESCENDANT of `referrer` is a real, different thing to depend on (and no
|
|
108
|
+
* call site here ever resolves a target nested inside the specific referrer
|
|
109
|
+
* node anyway — a sibling field, the common case, is nested in the shared
|
|
110
|
+
* parent ENTITY, not in the referrer).
|
|
111
|
+
*/
|
|
112
|
+
function isSelfReference(referrer: MetaData, target: MetaData): boolean {
|
|
113
|
+
return referrer === target || isAncestorOf(target, referrer);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// the check: does `target` carry `deprecated`, resolved?
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/** Returns true when a finding was pushed — callers checking a MORE SPECIFIC
|
|
121
|
+
* target first (e.g. a field) use this to decide whether to also check the
|
|
122
|
+
* less-specific one (its entity): only when the specific one was silent. */
|
|
123
|
+
function considerTarget(referrer: MetaData, referrerPath: string, verb: string, target: MetaData, out: Diagnostic[]): boolean {
|
|
124
|
+
if (isSelfReference(referrer, target)) return false;
|
|
125
|
+
// Registry contract (documentation-definition.embedded.ts): "Presence ⇒
|
|
126
|
+
// deprecated" — an empty-string reason still means deprecated (codegen-ts's
|
|
127
|
+
// jsdoc.ts reads it the same way, via `!== undefined`, not a truthiness
|
|
128
|
+
// check). Only an ABSENT attr (undefined) means "not deprecated".
|
|
129
|
+
const reason = target.attr(DOC_ATTR_DEPRECATED);
|
|
130
|
+
if (typeof reason !== "string") return false;
|
|
131
|
+
const replacedByRaw = target.attr(DOC_ATTR_REPLACED_BY);
|
|
132
|
+
const replacedBy = typeof replacedByRaw === "string" && replacedByRaw !== "" ? replacedByRaw : undefined;
|
|
133
|
+
const targetKind = `${target.type}.${target.subType}`;
|
|
134
|
+
const reasonClause = reason !== "" ? `: ${reason}` : "";
|
|
135
|
+
out.push(
|
|
136
|
+
warn(
|
|
137
|
+
referrerPath,
|
|
138
|
+
`${verb} deprecated ${targetKind} ${addressOf(target)}${reasonClause}` +
|
|
139
|
+
(replacedBy !== undefined ? ` Replaced by ${replacedBy}.` : ""),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Split a dotted reference into its entity HEAD and member tail ("Entity.field" ->
|
|
146
|
+
* "Entity" / "field"). `@objectRef` carries no tail (a pure object ref) and is
|
|
147
|
+
* resolved whole by the caller; `@references`/`@from`/`@of`/`@via` do. */
|
|
148
|
+
function headOf(raw: string): string {
|
|
149
|
+
const dot = raw.indexOf(".");
|
|
150
|
+
return dot === -1 ? raw : raw.slice(0, dot);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveEntityRef(root: MetaData, ref: string, referrerPkg: string): MetaObject | undefined {
|
|
154
|
+
const node = resolveObjectRef(root, ref, referrerPkg).node;
|
|
155
|
+
return node !== undefined && isMetaObject(node) ? node : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The relationship / identity.reference CHILD on `entity` named `hopName` — a
|
|
159
|
+
* `@via` path segment. Resolving `children()` (ADR-0039): an inherited hop still
|
|
160
|
+
* counts. */
|
|
161
|
+
function findHop(entity: MetaObject, hopName: string): MetaData | undefined {
|
|
162
|
+
return entity.children().find(
|
|
163
|
+
(c) =>
|
|
164
|
+
(c.type === TYPE_RELATIONSHIP || (c.type === TYPE_IDENTITY && c.subType === IDENTITY_SUBTYPE_REFERENCE)) &&
|
|
165
|
+
c.name === hopName,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The entity a resolved hop (relationship or identity.reference) points at, raw. */
|
|
170
|
+
function hopTargetRef(hop: MetaData): string | undefined {
|
|
171
|
+
const v = hop.type === TYPE_RELATIONSHIP
|
|
172
|
+
? hop.attr(RELATIONSHIP_ATTR_OBJECT_REF)
|
|
173
|
+
: hop.attr(IDENTITY_REFERENCE_ATTR_REFERENCES);
|
|
174
|
+
return typeof v === "string" && v !== "" ? headOf(v) : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check a dotted "Entity" or "Entity.field" reference — shared by origin
|
|
179
|
+
* `@from`/`@of` AND identity.reference's `@references` (ADR-0042 §5: the
|
|
180
|
+
* dotted form is not origin-specific; `Team.code` names an explicit FK target
|
|
181
|
+
* field exactly the way `Country.name` names a passthrough source field — see
|
|
182
|
+
* `fixtures/conformance/relationship-one-two-refs-dotted-references/`). The
|
|
183
|
+
* field is the more specific target (checked first); the entity is checked
|
|
184
|
+
* independently so a field on an otherwise-fine entity, OR a whole deprecated
|
|
185
|
+
* entity, each warn. Resolution failure (either half) is silent — bias to
|
|
186
|
+
* under-flagging, the same stance every sibling advisory pass in this
|
|
187
|
+
* directory takes.
|
|
188
|
+
*/
|
|
189
|
+
function checkEntityFieldPath(
|
|
190
|
+
root: MetaData,
|
|
191
|
+
referrer: MetaData,
|
|
192
|
+
referrerPath: string,
|
|
193
|
+
verb: string,
|
|
194
|
+
raw: string,
|
|
195
|
+
referrerPkg: string,
|
|
196
|
+
out: Diagnostic[],
|
|
197
|
+
): void {
|
|
198
|
+
const head = headOf(raw);
|
|
199
|
+
const entity = resolveEntityRef(root, head, referrerPkg);
|
|
200
|
+
if (entity === undefined) return;
|
|
201
|
+
const fieldName = raw.slice(head.length + 1);
|
|
202
|
+
const field = fieldName !== "" ? entity.fields().find((f) => f.name === fieldName) : undefined;
|
|
203
|
+
if (field !== undefined && considerTarget(referrer, referrerPath, verb, field, out)) {
|
|
204
|
+
return; // the field itself is the deprecated thing — don't ALSO warn about its (fine) entity.
|
|
205
|
+
}
|
|
206
|
+
considerTarget(referrer, referrerPath, verb, entity, out);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Walk an origin `@via` relationship path ("Entity.relOrRef[.relOrRef...]"),
|
|
211
|
+
* checking the head entity and every hop it names. Stops silently the moment a
|
|
212
|
+
* segment fails to resolve — a half-walked path still warned on everything it
|
|
213
|
+
* DID resolve, which is the right bias for an advisory that must never crash
|
|
214
|
+
* `meta verify` on a path shape it does not fully understand.
|
|
215
|
+
*/
|
|
216
|
+
function checkViaPath(
|
|
217
|
+
root: MetaData,
|
|
218
|
+
origin: MetaData,
|
|
219
|
+
referrerPath: string,
|
|
220
|
+
verb: string,
|
|
221
|
+
via: string,
|
|
222
|
+
referrerPkg: string,
|
|
223
|
+
out: Diagnostic[],
|
|
224
|
+
): void {
|
|
225
|
+
const segments = via.split(".");
|
|
226
|
+
const headSegment = segments[0];
|
|
227
|
+
if (headSegment === undefined || headSegment === "") return;
|
|
228
|
+
let current = resolveEntityRef(root, headSegment, referrerPkg);
|
|
229
|
+
if (current === undefined) return;
|
|
230
|
+
considerTarget(origin, referrerPath, verb, current, out);
|
|
231
|
+
for (const hopName of segments.slice(1)) {
|
|
232
|
+
const hop = findHop(current, hopName);
|
|
233
|
+
if (hop === undefined) return;
|
|
234
|
+
considerTarget(origin, referrerPath, verb, hop, out);
|
|
235
|
+
const targetRef = hopTargetRef(hop);
|
|
236
|
+
if (targetRef === undefined) return;
|
|
237
|
+
const next = resolveEntityRef(root, targetRef, packageOf(current));
|
|
238
|
+
if (next === undefined) return;
|
|
239
|
+
considerTarget(origin, referrerPath, verb, next, out);
|
|
240
|
+
current = next;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function checkOrigin(root: MetaData, origin: MetaData, referrerPath: string, out: Diagnostic[]): void {
|
|
245
|
+
const referrerPkg = (() => {
|
|
246
|
+
const owner = owningObject(origin);
|
|
247
|
+
return owner !== undefined ? packageOf(owner) : "";
|
|
248
|
+
})();
|
|
249
|
+
const subtypeLabel = `origin.${origin.subType}`;
|
|
250
|
+
|
|
251
|
+
// ADR-0039: own — origin.* never inherits (ADR-0029), so own is correct (the
|
|
252
|
+
// same stance every origin accessor in meta-origin.ts takes).
|
|
253
|
+
const from = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_FROM);
|
|
254
|
+
if (typeof from === "string" && from !== "") {
|
|
255
|
+
checkEntityFieldPath(root, origin, referrerPath, `${subtypeLabel} @from references`, from, referrerPkg, out);
|
|
256
|
+
}
|
|
257
|
+
const of_ = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_OF);
|
|
258
|
+
if (typeof of_ === "string" && of_ !== "") {
|
|
259
|
+
checkEntityFieldPath(root, origin, referrerPath, `${subtypeLabel} @of references`, of_, referrerPkg, out);
|
|
260
|
+
}
|
|
261
|
+
const via = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_VIA);
|
|
262
|
+
if (typeof via === "string" && via !== "") {
|
|
263
|
+
checkViaPath(root, origin, referrerPath, `${subtypeLabel} @via references`, via, referrerPkg, out);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function checkNode(root: MetaData, node: MetaData, path: string, out: Diagnostic[]): void {
|
|
268
|
+
// extends — every node type may carry a resolved super (generic, type-agnostic).
|
|
269
|
+
if (node.superData !== undefined) {
|
|
270
|
+
considerTarget(node, path, "extends", node.superData, out);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// @objectRef — relationship.* (its target object) and field.object/value (a
|
|
274
|
+
// value-typed field's own objectRef). Same attr name on both (naming-refs.ts).
|
|
275
|
+
if (node.type === TYPE_RELATIONSHIP || node.type === TYPE_FIELD) {
|
|
276
|
+
const ref = node.attr(node.type === TYPE_RELATIONSHIP ? RELATIONSHIP_ATTR_OBJECT_REF : FIELD_ATTR_OBJECT_REF);
|
|
277
|
+
if (typeof ref === "string" && ref !== "") {
|
|
278
|
+
const owner = owningObject(node);
|
|
279
|
+
const target = resolveEntityRef(root, ref, owner !== undefined ? packageOf(owner) : "");
|
|
280
|
+
if (target !== undefined) considerTarget(node, path, "references (@objectRef)", target, out);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// @references — identity.reference's FK target. May be bare ("Entity",
|
|
285
|
+
// defaulting to the primary identity) or dotted ("Entity.field" /
|
|
286
|
+
// "Entity.fieldA,fieldB", an explicit field/compound target) — the dotted
|
|
287
|
+
// field half is a deprecation-relevant target in its own right (#305 review:
|
|
288
|
+
// `@references: "Team.code"` where only `code` is `@deprecated` was missed
|
|
289
|
+
// when this checked the entity head alone).
|
|
290
|
+
if (node.type === TYPE_IDENTITY && node.subType === IDENTITY_SUBTYPE_REFERENCE) {
|
|
291
|
+
const raw = node.attr(IDENTITY_REFERENCE_ATTR_REFERENCES);
|
|
292
|
+
if (typeof raw === "string" && raw !== "") {
|
|
293
|
+
const owner = owningObject(node);
|
|
294
|
+
checkEntityFieldPath(root, node, path, "references (@references)", raw, owner !== undefined ? packageOf(owner) : "", out);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// origin.* — @from / @of / @via.
|
|
299
|
+
if (node.type === TYPE_ORIGIN) {
|
|
300
|
+
checkOrigin(root, node, path, out);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Lint the loaded model for references INTO a `deprecated` node, across every
|
|
306
|
+
* reference kind the metamodel resolves: `extends`, `@objectRef`, `@references`,
|
|
307
|
+
* and an origin's `@from` / `@of` / `@via`. Returns `[]` for a model with no
|
|
308
|
+
* `deprecated` attr anywhere reachable from a reference (additive, opt-in).
|
|
309
|
+
*/
|
|
310
|
+
export function lintDeprecatedReferences(root: MetaData): Diagnostic[] {
|
|
311
|
+
const out: Diagnostic[] = [];
|
|
312
|
+
const walk = (n: MetaData, prefix: string): void => {
|
|
313
|
+
for (const c of n.ownChildren()) {
|
|
314
|
+
// origin carries no name of its own (always a singleton child of a field) —
|
|
315
|
+
// report against the FIELD it belongs to, not a path ending in a blank segment.
|
|
316
|
+
const path = c.type === TYPE_ORIGIN
|
|
317
|
+
? prefix
|
|
318
|
+
: prefix === ""
|
|
319
|
+
? c.resolutionKey()
|
|
320
|
+
: `${prefix}.${c.name}`;
|
|
321
|
+
checkNode(root, c, path, out);
|
|
322
|
+
walk(c, path);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
walk(root, "");
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
@@ -96,7 +96,10 @@ export function scanForUnindexedForeignKeys(root: MetaData, opts: UnindexedFkOpt
|
|
|
96
96
|
});
|
|
97
97
|
const fields = ref !== undefined ? ref.fields : fk.columns;
|
|
98
98
|
const label = ref !== undefined ? `${entity.name}.${ref.name}` : `${entity.name} (${fk.name})`;
|
|
99
|
-
|
|
99
|
+
// Entity-prefixed: an index name is schema-wide in Postgres, so a bare `by<Field>`
|
|
100
|
+
// suggested the SAME name to every entity sharing the FK field, and applying the advice
|
|
101
|
+
// verbatim failed `meta migrate` with ERR_DUPLICATE_SQL_NAME.
|
|
102
|
+
const indexName = `${entity.name.charAt(0).toLowerCase()}${entity.name.slice(1)}By${fields.map(pascal).join("")}`;
|
|
100
103
|
const fieldList = fields.length === 1 ? fields[0]! : `[${fields.join(", ")}]`;
|
|
101
104
|
out.push({
|
|
102
105
|
file: files[0] ?? "",
|