@ecoma-io/archkeep 0.15.0 → 0.16.1
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/README.md +3 -3
- package/cli.mjs +126 -4
- package/commands.mjs +6 -0
- package/lsp.mjs +15 -2
- package/package.json +6 -2
- package/src/analysis/analyze.mjs +15 -0
- package/src/analysis/contract.md +36 -18
- package/src/analysis/csharp.mjs +514 -0
- package/src/analysis/dotnet/csproj.mjs +380 -0
- package/src/analysis/dotnet/mask.mjs +178 -0
- package/src/analysis/dotnet/namespaces.mjs +172 -0
- package/src/analysis/dotnet/resolve.mjs +89 -0
- package/src/analysis/go.mjs +303 -5
- package/src/analysis/java.mjs +329 -0
- package/src/analysis/jvm/gradle.mjs +545 -0
- package/src/analysis/jvm/mask.mjs +170 -0
- package/src/analysis/jvm/maven.mjs +612 -0
- package/src/analysis/jvm/packages.mjs +209 -0
- package/src/analysis/jvm/resolve.mjs +139 -0
- package/src/analysis/kotlin.mjs +210 -0
- package/src/analysis/manifest-util.mjs +30 -0
- package/src/analysis/python.mjs +3 -2
- package/src/analysis/registry.mjs +11 -0
- package/src/analysis/rust.mjs +171 -17
- package/src/analysis/source-util.mjs +155 -6
- package/src/analysis/typescript.mjs +9 -2
- package/src/commands/context.mjs +84 -14
- package/src/commands/provenance.mjs +7 -44
- package/src/commands/rules.mjs +775 -0
- package/src/governance/profile-registry.mjs +0 -1
- package/src/graph/create-dependencies.mjs +138 -15
- package/src/lsp/diagnose.mjs +1 -1
- package/src/lsp/server.mjs +97 -1
- package/src/lsp/workspace-index.mjs +106 -15
- package/src/options.mjs +30 -7
- package/src/process.mjs +10 -1
- package/src/providers/moon.mjs +287 -36
- package/src/providers/native/differential.fixtures.mjs +32 -6
- package/src/providers/native/discover.mjs +83 -4
- package/src/providers/native/graph.mjs +58 -0
- package/src/providers/native/model.mjs +59 -1
- package/src/rules/index.mjs +21 -6
- package/src/rules/reachability.mjs +2 -0
- package/src/rules/tags.mjs +7 -5
- package/src/rules/topology.mjs +5 -3
- package/src/workspace.mjs +115 -23
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Architecture governance for polyglot repositories** — a deterministic
|
|
4
4
|
authority that keeps the architecture your team declared aligned with the code
|
|
5
5
|
your team keeps changing. Dependency graphs and module boundaries for Go, Rust,
|
|
6
|
-
Python, TypeScript, JavaScript
|
|
6
|
+
Python, TypeScript, JavaScript, Vue, Java, Kotlin and C#, with Nx and Moon as first-class
|
|
7
7
|
integrations. Coding agents read the same verdicts, machine-readably, through
|
|
8
8
|
the `arch-*` skills. The system boundary — what Archkeep is and what it is not —
|
|
9
9
|
is owned by [architecture-authority.md](https://github.com/ecoma-io/archkeep/blob/main/docs/doctrine/architecture-authority.md).
|
|
@@ -182,7 +182,7 @@ a file it cannot edit ([overview.md](https://github.com/ecoma-io/archkeep/blob/m
|
|
|
182
182
|
|
|
183
183
|
## Support
|
|
184
184
|
|
|
185
|
-
- **Languages** — Go, Rust, Python, TypeScript and JavaScript, and
|
|
185
|
+
- **Languages** — Go, Rust, Python, TypeScript and JavaScript, Vue, Java, Kotlin and C#. Analysis
|
|
186
186
|
from source, never a build: nothing shells out to `go`, `cargo`, `uv` or `tsc`
|
|
187
187
|
([languages.md](https://github.com/ecoma-io/archkeep/blob/main/docs/reference/languages.md)).
|
|
188
188
|
- **Workspaces** — any repository: Nx registers the plugin in `nx.json` and
|
|
@@ -225,7 +225,7 @@ Ten minutes end to end, most of it spent deciding what your tags mean:
|
|
|
225
225
|
Ten minutes end to end, most of it spent deciding what your tags mean:
|
|
226
226
|
[**Getting started →**](https://github.com/ecoma-io/archkeep/blob/main/docs/getting-started/installation.md). `graph`, `diff`,
|
|
227
227
|
`history`, `trajectory`, `evolution`, `drift`, `impact`, `explain`,
|
|
228
|
-
`context` and the rest of the
|
|
228
|
+
`context` and the rest of the 22-command surface are in the
|
|
229
229
|
[CLI reference](https://github.com/ecoma-io/archkeep/blob/main/docs/reference/cli.md).
|
|
230
230
|
|
|
231
231
|
## Documentation map
|
package/cli.mjs
CHANGED
|
@@ -124,6 +124,12 @@ import { debtCommand } from "./src/commands/debt.mjs";
|
|
|
124
124
|
import { explainCommand } from "./src/commands/explain.mjs";
|
|
125
125
|
import { impactCommand } from "./src/commands/impact.mjs";
|
|
126
126
|
import { provenanceCommand } from "./src/commands/provenance-command.mjs";
|
|
127
|
+
import {
|
|
128
|
+
rulesAddCommand,
|
|
129
|
+
rulesInfoCommand,
|
|
130
|
+
rulesListCommand,
|
|
131
|
+
rulesVerifyCommand,
|
|
132
|
+
} from "./src/commands/rules.mjs";
|
|
127
133
|
import { waiversCommand } from "./src/commands/waivers.mjs";
|
|
128
134
|
import { INTENT_FILE, loadIntent } from "./src/architecture-intent/model.mjs";
|
|
129
135
|
import { isProgramEntry } from "./src/entry-point.mjs";
|
|
@@ -1674,8 +1680,8 @@ async function runAdr(options, { cwd, env }) {
|
|
|
1674
1680
|
const root = resolveWorkspaceRootForUsage(cwd);
|
|
1675
1681
|
if (root === null) {
|
|
1676
1682
|
env.err(
|
|
1677
|
-
`archkeep: adr needs a workspace root — no nx.json, archkeep.json, or
|
|
1678
|
-
|
|
1683
|
+
`archkeep: adr needs a workspace root — no nx.json, archkeep.json, or ` +
|
|
1684
|
+
`.moon/workspace.yml marker found walking up from ${cwd}`,
|
|
1679
1685
|
);
|
|
1680
1686
|
return EXIT.error;
|
|
1681
1687
|
}
|
|
@@ -1707,6 +1713,72 @@ async function runAdr(options, { cwd, env }) {
|
|
|
1707
1713
|
return result.status === "ok" ? EXIT.ok : EXIT.error;
|
|
1708
1714
|
}
|
|
1709
1715
|
|
|
1716
|
+
/**
|
|
1717
|
+
* `rules`'s `run`: dispatches to the appropriate subcommand (list/info/verify/add),
|
|
1718
|
+
* drives it, writes the report, and returns the exit code.
|
|
1719
|
+
*
|
|
1720
|
+
* @param {{format: string, output: string|null, catalog: string|null, to: string|null, paths: string[]}} options
|
|
1721
|
+
* @param {{cwd: string, env: {out: Function, err: Function}}} runContext
|
|
1722
|
+
* @returns {Promise<number>}
|
|
1723
|
+
*/
|
|
1724
|
+
async function runRules(options, { cwd, env }) {
|
|
1725
|
+
if (options.paths.length === 0) {
|
|
1726
|
+
env.err(`archkeep: rules requires a subcommand (list, info, verify, or add)`);
|
|
1727
|
+
return EXIT.usage;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const subcommand = options.paths[0];
|
|
1731
|
+
const ruleName = options.paths[1];
|
|
1732
|
+
|
|
1733
|
+
const dispatch = {
|
|
1734
|
+
list: rulesListCommand,
|
|
1735
|
+
info: rulesInfoCommand,
|
|
1736
|
+
verify: rulesVerifyCommand,
|
|
1737
|
+
add: rulesAddCommand,
|
|
1738
|
+
}[subcommand];
|
|
1739
|
+
|
|
1740
|
+
if (!dispatch) {
|
|
1741
|
+
env.err(
|
|
1742
|
+
`archkeep: unknown rules subcommand '${subcommand}' — must be list, info, verify, or add`,
|
|
1743
|
+
);
|
|
1744
|
+
return EXIT.usage;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if (subcommand === "info" && !ruleName) {
|
|
1748
|
+
env.err(`archkeep: rules info requires a rule name`);
|
|
1749
|
+
return EXIT.usage;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (subcommand === "add" && !ruleName) {
|
|
1753
|
+
env.err(`archkeep: rules add requires a rule name`);
|
|
1754
|
+
return EXIT.usage;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
let result;
|
|
1758
|
+
try {
|
|
1759
|
+
const commandOptions = { catalog: options.catalog, to: options.to };
|
|
1760
|
+
result = await dispatch(commandOptions, { cwd, ruleName });
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
env.err(String(error?.message ?? error));
|
|
1763
|
+
return EXIT.error;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const report = options.format === "json" ? result.report.json : result.report.text;
|
|
1767
|
+
|
|
1768
|
+
if (options.output) {
|
|
1769
|
+
const reportText = report.endsWith("\n") ? report : `${report}\n`;
|
|
1770
|
+
if (!writeOutputReport(options.output, reportText, env, cwd, null)) return EXIT.error;
|
|
1771
|
+
env.err(`archkeep: rules ${subcommand} complete → ${options.output}`);
|
|
1772
|
+
} else {
|
|
1773
|
+
env.out(report);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Exit codes: 0 for ok, 1 for findings (verify only), 3 for no-verdict
|
|
1777
|
+
if (result.status === "ok") return EXIT.ok;
|
|
1778
|
+
if (result.status === "findings") return EXIT.violations;
|
|
1779
|
+
return EXIT.error;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1710
1782
|
/**
|
|
1711
1783
|
* `history`'s `run`: resolves the command context, optionally captures a
|
|
1712
1784
|
* snapshot of the current workspace, drives `historyCommand`, writes the
|
|
@@ -1909,8 +1981,8 @@ async function runEvolution(options, { cwd, env }) {
|
|
|
1909
1981
|
const root = findWorkspaceRoot(cwd, WORKSPACE_MARKERS);
|
|
1910
1982
|
if (root === null) {
|
|
1911
1983
|
env.err(
|
|
1912
|
-
`archkeep: evolution needs a workspace root — no nx.json, archkeep.json, or
|
|
1913
|
-
|
|
1984
|
+
`archkeep: evolution needs a workspace root — no nx.json, archkeep.json, or ` +
|
|
1985
|
+
`.moon/workspace.yml marker found walking up from ${cwd}`,
|
|
1914
1986
|
);
|
|
1915
1987
|
return EXIT.error;
|
|
1916
1988
|
}
|
|
@@ -2991,6 +3063,46 @@ const ADR_FLAG_HELP = Object.freeze([
|
|
|
2991
3063
|
}),
|
|
2992
3064
|
]);
|
|
2993
3065
|
|
|
3066
|
+
/**
|
|
3067
|
+
* `rules`'s flags: catalog path, format, output, and target directory for add.
|
|
3068
|
+
* The first positional is the subcommand (list/info/verify/add), the second is
|
|
3069
|
+
* the rule name for info and add.
|
|
3070
|
+
*
|
|
3071
|
+
* @type {readonly FlagHelp[]}
|
|
3072
|
+
*/
|
|
3073
|
+
const RULES_FLAG_HELP = Object.freeze([
|
|
3074
|
+
Object.freeze({
|
|
3075
|
+
flag: "--catalog",
|
|
3076
|
+
key: "catalog",
|
|
3077
|
+
arg: "<path>",
|
|
3078
|
+
describe: Object.freeze([
|
|
3079
|
+
"Path to the catalog file",
|
|
3080
|
+
"(default: node_modules/@ecoma-io/archkeep-rules/catalog.json)",
|
|
3081
|
+
]),
|
|
3082
|
+
}),
|
|
3083
|
+
Object.freeze({
|
|
3084
|
+
flag: "--format",
|
|
3085
|
+
key: "format",
|
|
3086
|
+
arg: "text|json",
|
|
3087
|
+
describe: Object.freeze([
|
|
3088
|
+
"Terminal report (default) or the versioned JSON envelope",
|
|
3089
|
+
"docs/reference/json-output.md documents",
|
|
3090
|
+
]),
|
|
3091
|
+
}),
|
|
3092
|
+
Object.freeze({
|
|
3093
|
+
flag: "--output",
|
|
3094
|
+
key: "output",
|
|
3095
|
+
arg: "<file>",
|
|
3096
|
+
describe: Object.freeze(["Write the report to a file instead of stdout"]),
|
|
3097
|
+
}),
|
|
3098
|
+
Object.freeze({
|
|
3099
|
+
flag: "--to",
|
|
3100
|
+
key: "to",
|
|
3101
|
+
arg: "<dir>",
|
|
3102
|
+
describe: Object.freeze(["Target directory for the .wasm file", "(default: tools/rules/)"]),
|
|
3103
|
+
}),
|
|
3104
|
+
]);
|
|
3105
|
+
|
|
2994
3106
|
/**
|
|
2995
3107
|
* The command table `usage()` and `runCli` both read from — a command added
|
|
2996
3108
|
* later is a new entry here, not a new branch in either. `args` is the
|
|
@@ -3215,6 +3327,16 @@ const COMMANDS = Object.freeze({
|
|
|
3215
3327
|
formats: DESCRIBABLE_FORMATS,
|
|
3216
3328
|
run: runAdr,
|
|
3217
3329
|
}),
|
|
3330
|
+
rules: Object.freeze({
|
|
3331
|
+
name: "rules",
|
|
3332
|
+
args: "<list|info|verify|add> [<rule-name>]",
|
|
3333
|
+
summary: "List official rules, show details, verify catalog integrity, or add a rule",
|
|
3334
|
+
flagHelp: RULES_FLAG_HELP,
|
|
3335
|
+
flags: Object.freeze(Object.fromEntries(RULES_FLAG_HELP.map((f) => [f.flag, f.key]))),
|
|
3336
|
+
defaults: Object.freeze({ format: "text", output: null, catalog: null, to: null }),
|
|
3337
|
+
formats: DESCRIBABLE_FORMATS,
|
|
3338
|
+
run: runRules,
|
|
3339
|
+
}),
|
|
3218
3340
|
});
|
|
3219
3341
|
|
|
3220
3342
|
/**
|
package/commands.mjs
CHANGED
|
@@ -47,5 +47,11 @@ export { historyCommand } from "./src/commands/history.mjs";
|
|
|
47
47
|
export { adrCommand } from "./src/commands/adr.mjs";
|
|
48
48
|
export { discoverCommand } from "./src/commands/discover.mjs";
|
|
49
49
|
export { reconcileCommand } from "./src/commands/reconcile.mjs";
|
|
50
|
+
export {
|
|
51
|
+
rulesAddCommand,
|
|
52
|
+
rulesInfoCommand,
|
|
53
|
+
rulesListCommand,
|
|
54
|
+
rulesVerifyCommand,
|
|
55
|
+
} from "./src/commands/rules.mjs";
|
|
50
56
|
|
|
51
57
|
export { UsageError } from "./src/errors.mjs";
|
package/lsp.mjs
CHANGED
|
@@ -68,11 +68,24 @@ export function serve(input = process.stdin, output = process.stdout, onExit = n
|
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
70
|
pending = framed.rest;
|
|
71
|
-
for (const message of framed.messages)
|
|
71
|
+
for (const message of framed.messages) {
|
|
72
|
+
// `handle` is async and owns its own error envelope — a rejection here
|
|
73
|
+
// is a wiring defect, not a diagnostic. The catch reports it on stderr
|
|
74
|
+
// (the channel a client surfaces as its server log) instead of letting
|
|
75
|
+
// it become an unhandled rejection that kills the connection, or
|
|
76
|
+
// nothing at all, which would read as a clean frame.
|
|
77
|
+
server
|
|
78
|
+
.handle(message)
|
|
79
|
+
.catch((error) => process.stderr.write(`archkeep-lsp: ${error?.message ?? error}\n`));
|
|
80
|
+
}
|
|
72
81
|
});
|
|
73
82
|
// A client that closed the pipe without saying `exit` did not shut the server
|
|
74
83
|
// down; reporting that as a clean stop would hide a crashed editor.
|
|
75
|
-
input.on("end", () =>
|
|
84
|
+
input.on("end", () => {
|
|
85
|
+
server
|
|
86
|
+
.handle({ jsonrpc: "2.0", method: "exit" })
|
|
87
|
+
.catch((error) => process.stderr.write(`archkeep-lsp: ${error?.message ?? error}\n`));
|
|
88
|
+
});
|
|
76
89
|
return server;
|
|
77
90
|
}
|
|
78
91
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecoma-io/archkeep",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Architecture enforcement for polyglot repositories — dependency graphs and module boundaries for Go, Rust, Python, TypeScript, JavaScript and
|
|
3
|
+
"version": "0.16.1",
|
|
4
|
+
"description": "Architecture enforcement for polyglot repositories — dependency graphs and module boundaries for Go, Rust, Python, TypeScript, JavaScript, Vue, Java and Kotlin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"architecture",
|
|
7
7
|
"enforcement",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@nx/eslint-plugin": ">=21",
|
|
56
|
+
"fast-xml-parser": "5.11.0",
|
|
56
57
|
"nx": ">=21",
|
|
57
58
|
"typescript": ">=5 <7",
|
|
58
59
|
"vue": ">=3"
|
|
@@ -61,6 +62,9 @@
|
|
|
61
62
|
"@nx/eslint-plugin": {
|
|
62
63
|
"optional": true
|
|
63
64
|
},
|
|
65
|
+
"fast-xml-parser": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
64
68
|
"nx": {
|
|
65
69
|
"optional": true
|
|
66
70
|
},
|
package/src/analysis/analyze.mjs
CHANGED
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* exists. Every other import site keeps naming them from this module.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { analyzeCSharp } from "./csharp.mjs";
|
|
25
26
|
import { analyzeGo } from "./go.mjs";
|
|
27
|
+
import { analyzeJava } from "./java.mjs";
|
|
28
|
+
import { analyzeKotlin } from "./kotlin.mjs";
|
|
26
29
|
import { analyzePython } from "./python.mjs";
|
|
27
30
|
import { LANGUAGE_BY_EXTENSION, languageOf } from "./registry.mjs";
|
|
28
31
|
import { analyzeRust } from "./rust.mjs";
|
|
@@ -78,6 +81,15 @@ export { LANGUAGE_BY_EXTENSION, languageOf };
|
|
|
78
81
|
* `./x` and `../x` in JavaScript, `crate::`/`self::`/`super::` and a sibling
|
|
79
82
|
* crate target of the same Cargo package in Rust, a leading-dot import in
|
|
80
83
|
* Python. It is the counter-evidence `noSelfCircularDependencies` looks for.
|
|
84
|
+
* @property {boolean} namesOnly The language's ONLY import spelling is a
|
|
85
|
+
* name — a Go module path, a Rust crate, a Python dotted module, a C#
|
|
86
|
+
* namespace, a Java or Kotlin package — so no specifier it produces is ever
|
|
87
|
+
* a filesystem path, and text beginning `libs/…` or `apps/…` is a name whose
|
|
88
|
+
* first segments are those words, not a path into a project. Constant on
|
|
89
|
+
* every record one analyzer produces, unlike `path` and `relative`, which
|
|
90
|
+
* answer per specifier. `isAbsoluteImportIntoAnotherProject` is gated on it
|
|
91
|
+
* (#376): false in the JavaScript family, where a bare `libs/x` is a deep
|
|
92
|
+
* import and a `/libs/x` an absolute path.
|
|
81
93
|
*/
|
|
82
94
|
|
|
83
95
|
/**
|
|
@@ -170,6 +182,9 @@ const ANALYZER_BY_LANGUAGE = Object.freeze({
|
|
|
170
182
|
go: analyzeGo,
|
|
171
183
|
rust: analyzeRust,
|
|
172
184
|
python: analyzePython,
|
|
185
|
+
java: analyzeJava,
|
|
186
|
+
kotlin: analyzeKotlin,
|
|
187
|
+
csharp: analyzeCSharp,
|
|
173
188
|
});
|
|
174
189
|
|
|
175
190
|
/**
|
package/src/analysis/contract.md
CHANGED
|
@@ -71,23 +71,40 @@ beats every consumer remembering which convention this tool chose.
|
|
|
71
71
|
### How the specifier is spelled is a per-language fact, so the analyzer states it
|
|
72
72
|
|
|
73
73
|
`specifier` is the raw text; `spelling` is what that text IS in the language it
|
|
74
|
-
was written in.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| `
|
|
82
|
-
|
|
|
83
|
-
| `
|
|
74
|
+
was written in. Three bits, because the rules ask three independent questions —
|
|
75
|
+
the first two per specifier, the third per language and therefore constant on
|
|
76
|
+
every record one analyzer produces:
|
|
77
|
+
|
|
78
|
+
| | `path` | `relative` | `namesOnly` |
|
|
79
|
+
| --------------------------------- | :----: | :--------: | :---------: |
|
|
80
|
+
| `./x`, `../x`, `.`, `..` (JS) | yes | yes | no |
|
|
81
|
+
| `/x` (JS) | yes | no | no |
|
|
82
|
+
| `react` (JS, bare) | no | no | no |
|
|
83
|
+
| `crate::x`, `self::x`, `super::x` | no | yes | yes |
|
|
84
|
+
| `rba_desktop_lib::run` from a bin | no | yes | yes |
|
|
85
|
+
| `.mod`, `..pkg.sub` (Python) | no | yes | yes |
|
|
86
|
+
| `react`, `serde`, `example.com/m` | no | no | yes* |
|
|
87
|
+
|
|
88
|
+
\* the last row holds for `serde` and `example.com/m`; a JavaScript `react` is
|
|
89
|
+
`namesOnly: no` — the same text is a package name in one language and a name
|
|
90
|
+
that IS the only spelling in another, which is exactly why the bit rides on the
|
|
91
|
+
record instead of being derived from the text.
|
|
84
92
|
|
|
85
93
|
`path` says the specifier is a **filesystem path**: resolvable by path
|
|
86
94
|
arithmetic against the importing file, and naming no package. It decides
|
|
87
95
|
whether a specifier may receive a synthesized external node, and which message
|
|
88
96
|
an unresolvable one gets. `relative` says the specifier **reaches inside its
|
|
89
97
|
own project without going out through the project's public name** — the
|
|
90
|
-
counter-evidence `noSelfCircularDependencies` looks for.
|
|
98
|
+
counter-evidence `noSelfCircularDependencies` looks for. `namesOnly` says the
|
|
99
|
+
**language has no path spelling at all** — every specifier it produces is a
|
|
100
|
+
name, so no path-text rule applies to any of them. It gates
|
|
101
|
+
`isAbsoluteImportIntoAnotherProject`, which judges the JavaScript-family
|
|
102
|
+
spellings a bare `libs/x` deep import and a `/libs/x` absolute path: applied to
|
|
103
|
+
a language whose only spelling is the name, it read a Go `module libs/foo`'s
|
|
104
|
+
legal `import "libs/foo/bar"` as an absolute path into a project — an
|
|
105
|
+
unfixable verdict, because the name is the only spelling the language has
|
|
106
|
+
(#376). The edge such an import resolves to is still judged by every rule
|
|
107
|
+
below the spelling check; only the spelling check itself stands down.
|
|
91
108
|
|
|
92
109
|
**Why the analyzer answers and not the rules.** The rules layer used to derive
|
|
93
110
|
both from the text with one predicate — `.`, `..`, `./`, `../` — which is
|
|
@@ -101,10 +118,10 @@ import; the rules layer knows neither, and a language table there would be a
|
|
|
101
118
|
second registry drifting from `LANGUAGE_BY_EXTENSION`. So the record carries the
|
|
102
119
|
fact and the rule reads it.
|
|
103
120
|
|
|
104
|
-
The field is **mandatory**, and `evaluate()` throws on a record that omits
|
|
105
|
-
rather than falling back to the JavaScript shape. A default is how
|
|
106
|
-
analyzer inherits this bug silently; a throw is how it is told, once,
|
|
107
|
-
first record it produces.
|
|
121
|
+
The field is **mandatory**, and `evaluate()` throws on a record that omits any
|
|
122
|
+
bit of it rather than falling back to the JavaScript shape. A default is how
|
|
123
|
+
the next analyzer inherits this bug silently; a throw is how it is told, once,
|
|
124
|
+
at the first record it produces.
|
|
108
125
|
|
|
109
126
|
### Intra-project imports are emitted too
|
|
110
127
|
|
|
@@ -208,9 +225,10 @@ rule cares about, and the pressure to fix it would be an ignore list that
|
|
|
208
225
|
someone has to keep in sync with reality. An extension with no analyzer simply
|
|
209
226
|
has no imports this tool can see, which is the truth.
|
|
210
227
|
|
|
211
|
-
The registry is `LANGUAGE_BY_EXTENSION` in `
|
|
212
|
-
|
|
213
|
-
|
|
228
|
+
The registry is `LANGUAGE_BY_EXTENSION` in `registry.mjs` (re-exported by
|
|
229
|
+
`analyze.mjs`, so nothing that imports it from there has to change), and it is
|
|
230
|
+
the one place an extension is mapped. A language whose analyzer arrives adds
|
|
231
|
+
itself there and nowhere else.
|
|
214
232
|
|
|
215
233
|
## What an analyzer is handed
|
|
216
234
|
|