@supersigil/eslint-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +7 -0
- package/dist/rules/valid-criterion-ref.d.ts +10 -0
- package/dist/rules/valid-criterion-ref.d.ts.map +1 -0
- package/package.json +51 -0
- package/src/index.ts +20 -0
- package/src/rules/valid-criterion-ref.ts +193 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @supersigil/eslint-plugin
|
|
2
|
+
|
|
3
|
+
ESLint plugin for validating [Supersigil](https://supersigil.org) criterion refs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -D @supersigil/eslint-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires the `supersigil` CLI to be available on `PATH`.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Add the plugin to your ESLint flat config:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import supersigil from '@supersigil/eslint-plugin'
|
|
19
|
+
|
|
20
|
+
export default [
|
|
21
|
+
supersigil.configs.recommended,
|
|
22
|
+
// ...your other configs
|
|
23
|
+
]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or configure manually:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import supersigil from '@supersigil/eslint-plugin'
|
|
30
|
+
|
|
31
|
+
export default [
|
|
32
|
+
{
|
|
33
|
+
plugins: { '@supersigil': supersigil },
|
|
34
|
+
rules: {
|
|
35
|
+
'@supersigil/valid-criterion-ref': 'error',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Rules
|
|
42
|
+
|
|
43
|
+
### `valid-criterion-ref`
|
|
44
|
+
|
|
45
|
+
Validates that criterion refs passed to `verifies()` or listed in `meta.verifies` arrays point to criteria that actually exist in your specifications.
|
|
46
|
+
|
|
47
|
+
Reports errors for:
|
|
48
|
+
|
|
49
|
+
- **Malformed refs** — missing the `#` separator (e.g. `'auth/req'` instead of `'auth/req#req-1-1'`)
|
|
50
|
+
- **Unknown documents** — the document part of the ref doesn't match any specification
|
|
51
|
+
- **Unknown criteria** — the criterion doesn't exist in the referenced document
|
|
52
|
+
|
|
53
|
+
The rule loads valid refs from the `supersigil` CLI on first run and caches them for the ESLint session. If the CLI is unavailable, the rule silently disables itself.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
[MIT](../../LICENSE-MIT) OR [Apache-2.0](../../LICENSE-APACHE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,MAAM;;;;aAIK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CACvC,CAAA;AAYD,eAAe,MAAM,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/rules/valid-criterion-ref.ts
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
var cachedRefMap = null;
|
|
4
|
+
var cachedLoadError = null;
|
|
5
|
+
var loaded = false;
|
|
6
|
+
var warnedBinaryUnavailable = false;
|
|
7
|
+
var _testMode = false;
|
|
8
|
+
var _testRefMap = null;
|
|
9
|
+
var _testLoadError = null;
|
|
10
|
+
function loadRefMap() {
|
|
11
|
+
if (_testMode) {
|
|
12
|
+
return { refMap: _testRefMap, error: _testLoadError };
|
|
13
|
+
}
|
|
14
|
+
if (loaded) {
|
|
15
|
+
return { refMap: cachedRefMap, error: cachedLoadError };
|
|
16
|
+
}
|
|
17
|
+
loaded = true;
|
|
18
|
+
try {
|
|
19
|
+
const output = execFileSync("supersigil", ["refs", "--all", "--format", "json"], {
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
timeout: 3e4
|
|
22
|
+
});
|
|
23
|
+
const parsed = JSON.parse(output);
|
|
24
|
+
const entries = parsed.refs ?? parsed;
|
|
25
|
+
const refMap = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
let criteria = refMap.get(entry.doc_id);
|
|
28
|
+
if (!criteria) {
|
|
29
|
+
criteria = /* @__PURE__ */ new Set();
|
|
30
|
+
refMap.set(entry.doc_id, criteria);
|
|
31
|
+
}
|
|
32
|
+
criteria.add(entry.criterion_id);
|
|
33
|
+
}
|
|
34
|
+
cachedRefMap = refMap;
|
|
35
|
+
cachedLoadError = null;
|
|
36
|
+
return { refMap, error: null };
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : "Unknown error loading supersigil refs";
|
|
39
|
+
cachedRefMap = null;
|
|
40
|
+
cachedLoadError = message;
|
|
41
|
+
return { refMap: null, error: message };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function validateRef(context, node, value, refMap) {
|
|
45
|
+
const hashIndex = value.indexOf("#");
|
|
46
|
+
if (hashIndex === -1) {
|
|
47
|
+
context.report({
|
|
48
|
+
node,
|
|
49
|
+
messageId: "malformed",
|
|
50
|
+
data: { ref: value }
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const docId = value.slice(0, hashIndex);
|
|
55
|
+
const criterionId = value.slice(hashIndex + 1);
|
|
56
|
+
const criteria = refMap.get(docId);
|
|
57
|
+
if (!criteria) {
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: "unknownDocument",
|
|
61
|
+
data: { docId, ref: value }
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!criteria.has(criterionId)) {
|
|
66
|
+
context.report({
|
|
67
|
+
node,
|
|
68
|
+
messageId: "unknownCriterion",
|
|
69
|
+
data: { criterionId, docId }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
var rule = {
|
|
74
|
+
meta: {
|
|
75
|
+
type: "problem",
|
|
76
|
+
docs: {
|
|
77
|
+
description: "Validate Supersigil criterion refs in verifies() calls"
|
|
78
|
+
},
|
|
79
|
+
messages: {
|
|
80
|
+
malformed: "Malformed criterion ref '{{ref}}'. Expected format: document-id#criterion-id",
|
|
81
|
+
unknownDocument: "Unknown document '{{docId}}' in criterion ref '{{ref}}'",
|
|
82
|
+
unknownCriterion: "Unknown criterion '{{criterionId}}' in document '{{docId}}'"
|
|
83
|
+
},
|
|
84
|
+
schema: []
|
|
85
|
+
},
|
|
86
|
+
create(context) {
|
|
87
|
+
const { refMap, error } = loadRefMap();
|
|
88
|
+
if (refMap === null) {
|
|
89
|
+
if (error !== null && !warnedBinaryUnavailable) {
|
|
90
|
+
warnedBinaryUnavailable = true;
|
|
91
|
+
console.warn(
|
|
92
|
+
"[@supersigil/eslint-plugin] supersigil binary not available; criterion ref validation disabled"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
const refs = refMap;
|
|
98
|
+
function checkStringLiteral(node) {
|
|
99
|
+
validateRef(context, node, node.value, refs);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
// Match: verifies('ref', 'ref', ...)
|
|
103
|
+
CallExpression(node) {
|
|
104
|
+
if (node.callee.type === "Identifier" && node.callee.name === "verifies") {
|
|
105
|
+
for (const arg of node.arguments) {
|
|
106
|
+
if (arg.type === "Literal" && typeof arg.value === "string") {
|
|
107
|
+
checkStringLiteral(arg);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
// Match: { verifies: ['ref', ...] } in object expressions
|
|
113
|
+
'Property[key.name="verifies"] > ArrayExpression > Literal'(node) {
|
|
114
|
+
if (typeof node.value === "string") {
|
|
115
|
+
checkStringLiteral(node);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var valid_criterion_ref_default = rule;
|
|
122
|
+
|
|
123
|
+
// src/index.ts
|
|
124
|
+
var plugin = {
|
|
125
|
+
rules: {
|
|
126
|
+
"valid-criterion-ref": valid_criterion_ref_default
|
|
127
|
+
},
|
|
128
|
+
configs: {}
|
|
129
|
+
};
|
|
130
|
+
plugin.configs.recommended = {
|
|
131
|
+
plugins: {
|
|
132
|
+
"@supersigil": plugin
|
|
133
|
+
},
|
|
134
|
+
rules: {
|
|
135
|
+
"@supersigil/valid-criterion-ref": "error"
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var index_default = plugin;
|
|
139
|
+
export {
|
|
140
|
+
index_default as default
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/rules/valid-criterion-ref.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Rule } from 'eslint'\nimport { execFileSync } from 'node:child_process'\n\n// Session-level cache for the ref map (docId -> Set<criterionId>).\n// This persists for the lifetime of the ESLint process. In long-lived\n// servers (eslint_d, editor integrations), the cache is never invalidated \u2014\n// restart the ESLint server if specs change.\nlet cachedRefMap: Map<string, Set<string>> | null = null\nlet cachedLoadError: string | null = null\nlet loaded = false\nlet warnedBinaryUnavailable = false\n\n// Test seam: allow overriding the ref loading result in tests.\nlet _testMode = false\nlet _testRefMap: Map<string, Set<string>> | null = null\nlet _testLoadError: string | null = null\n\n/** @internal \u2014 test-only: inject a ref map to avoid shelling out */\nexport function _setRefMapForTesting(\n map: Map<string, Set<string>> | null,\n): void {\n _testMode = true\n _testRefMap = map\n}\n\n/** @internal \u2014 test-only: simulate a load error */\nexport function _setLoadErrorForTesting(error: string | null): void {\n _testLoadError = error\n}\n\n/** @internal \u2014 test-only: reset all overrides */\nexport function _resetTestOverrides(): void {\n _testMode = false\n _testRefMap = null\n _testLoadError = null\n warnedBinaryUnavailable = false\n}\n\ninterface RefEntry {\n ref: string\n doc_id: string\n criterion_id: string\n body_text: string\n}\n\nfunction loadRefMap(): { refMap: Map<string, Set<string>> | null; error: string | null } {\n if (_testMode) {\n return { refMap: _testRefMap, error: _testLoadError }\n }\n\n if (loaded) {\n return { refMap: cachedRefMap, error: cachedLoadError }\n }\n\n loaded = true\n\n try {\n const output = execFileSync('supersigil', ['refs', '--all', '--format', 'json'], {\n encoding: 'utf-8',\n timeout: 30_000,\n })\n\n const parsed = JSON.parse(output)\n const entries: RefEntry[] = parsed.refs ?? parsed\n const refMap = new Map<string, Set<string>>()\n\n for (const entry of entries) {\n let criteria = refMap.get(entry.doc_id)\n if (!criteria) {\n criteria = new Set<string>()\n refMap.set(entry.doc_id, criteria)\n }\n criteria.add(entry.criterion_id)\n }\n\n cachedRefMap = refMap\n cachedLoadError = null\n return { refMap, error: null }\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : 'Unknown error loading supersigil refs'\n cachedRefMap = null\n cachedLoadError = message\n return { refMap: null, error: message }\n }\n}\n\nfunction validateRef(\n context: Rule.RuleContext,\n node: Rule.Node,\n value: string,\n refMap: Map<string, Set<string>>,\n): void {\n const hashIndex = value.indexOf('#')\n\n if (hashIndex === -1) {\n context.report({\n node,\n messageId: 'malformed',\n data: { ref: value },\n })\n return\n }\n\n const docId = value.slice(0, hashIndex)\n const criterionId = value.slice(hashIndex + 1)\n\n const criteria = refMap.get(docId)\n if (!criteria) {\n context.report({\n node,\n messageId: 'unknownDocument',\n data: { docId, ref: value },\n })\n return\n }\n\n if (!criteria.has(criterionId)) {\n context.report({\n node,\n messageId: 'unknownCriterion',\n data: { criterionId, docId },\n })\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate Supersigil criterion refs in verifies() calls',\n },\n messages: {\n malformed:\n \"Malformed criterion ref '{{ref}}'. Expected format: document-id#criterion-id\",\n unknownDocument:\n \"Unknown document '{{docId}}' in criterion ref '{{ref}}'\",\n unknownCriterion:\n \"Unknown criterion '{{criterionId}}' in document '{{docId}}'\",\n },\n schema: [],\n },\n\n create(context) {\n const { refMap, error } = loadRefMap()\n\n // When supersigil binary is unavailable, log a warning to stderr and\n // return an empty visitor so no refs are checked. This avoids failing\n // lint (which would happen if we used context.report at error severity).\n if (refMap === null) {\n if (error !== null && !warnedBinaryUnavailable) {\n warnedBinaryUnavailable = true\n console.warn(\n '[@supersigil/eslint-plugin] supersigil binary not available; criterion ref validation disabled',\n )\n }\n return {}\n }\n\n const refs = refMap\n\n function checkStringLiteral(node: Rule.Node & { value: string }): void {\n validateRef(context, node, node.value, refs)\n }\n\n return {\n // Match: verifies('ref', 'ref', ...)\n CallExpression(node) {\n if (\n node.callee.type === 'Identifier' &&\n node.callee.name === 'verifies'\n ) {\n for (const arg of node.arguments) {\n if (arg.type === 'Literal' && typeof arg.value === 'string') {\n checkStringLiteral(arg as Rule.Node & { value: string })\n }\n }\n }\n },\n\n // Match: { verifies: ['ref', ...] } in object expressions\n 'Property[key.name=\"verifies\"] > ArrayExpression > Literal'(\n node: Rule.Node & { value: unknown },\n ) {\n if (typeof node.value === 'string') {\n checkStringLiteral(node as Rule.Node & { value: string })\n }\n },\n }\n },\n}\n\nexport default rule\n", "import validCriterionRef from './rules/valid-criterion-ref.ts'\n\nconst plugin = {\n rules: {\n 'valid-criterion-ref': validCriterionRef,\n },\n configs: {} as Record<string, unknown>,\n}\n\n// Self-referencing plugin for flat config\nplugin.configs.recommended = {\n plugins: {\n '@supersigil': plugin,\n },\n rules: {\n '@supersigil/valid-criterion-ref': 'error',\n },\n}\n\nexport default plugin\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
/** @internal — test-only: inject a ref map to avoid shelling out */
|
|
3
|
+
export declare function _setRefMapForTesting(map: Map<string, Set<string>> | null): void;
|
|
4
|
+
/** @internal — test-only: simulate a load error */
|
|
5
|
+
export declare function _setLoadErrorForTesting(error: string | null): void;
|
|
6
|
+
/** @internal — test-only: reset all overrides */
|
|
7
|
+
export declare function _resetTestOverrides(): void;
|
|
8
|
+
declare const rule: Rule.RuleModule;
|
|
9
|
+
export default rule;
|
|
10
|
+
//# sourceMappingURL=valid-criterion-ref.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-criterion-ref.d.ts","sourceRoot":"","sources":["../../src/rules/valid-criterion-ref.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAiBlC,oEAAoE;AACpE,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,GACnC,IAAI,CAGN;AAED,mDAAmD;AACnD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAElE;AAED,iDAAiD;AACjD,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AA0FD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAgEhB,CAAA;AAED,eAAe,IAAI,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@supersigil/eslint-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin for validating Supersigil criterion refs",
|
|
5
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/jonisavo/supersigil.git",
|
|
9
|
+
"directory": "packages/eslint-plugin"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://supersigil.org",
|
|
12
|
+
"bugs": "https://github.com/jonisavo/supersigil/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"eslint",
|
|
15
|
+
"eslintplugin",
|
|
16
|
+
"eslint-plugin",
|
|
17
|
+
"supersigil",
|
|
18
|
+
"specification",
|
|
19
|
+
"traceability"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist/",
|
|
33
|
+
"src/",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"eslint": ">=9.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.19.17",
|
|
41
|
+
"esbuild": "^0.28.0",
|
|
42
|
+
"eslint": "^9.39.4",
|
|
43
|
+
"typescript": "^6.0.2",
|
|
44
|
+
"vitest": "^4.1.3",
|
|
45
|
+
"@supersigil/vitest": "0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "node esbuild.mjs && tsc --emitDeclarationOnly --noEmit false --outDir dist",
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import validCriterionRef from './rules/valid-criterion-ref.ts'
|
|
2
|
+
|
|
3
|
+
const plugin = {
|
|
4
|
+
rules: {
|
|
5
|
+
'valid-criterion-ref': validCriterionRef,
|
|
6
|
+
},
|
|
7
|
+
configs: {} as Record<string, unknown>,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Self-referencing plugin for flat config
|
|
11
|
+
plugin.configs.recommended = {
|
|
12
|
+
plugins: {
|
|
13
|
+
'@supersigil': plugin,
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
'@supersigil/valid-criterion-ref': 'error',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default plugin
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { Rule } from 'eslint'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
// Session-level cache for the ref map (docId -> Set<criterionId>).
|
|
5
|
+
// This persists for the lifetime of the ESLint process. In long-lived
|
|
6
|
+
// servers (eslint_d, editor integrations), the cache is never invalidated —
|
|
7
|
+
// restart the ESLint server if specs change.
|
|
8
|
+
let cachedRefMap: Map<string, Set<string>> | null = null
|
|
9
|
+
let cachedLoadError: string | null = null
|
|
10
|
+
let loaded = false
|
|
11
|
+
let warnedBinaryUnavailable = false
|
|
12
|
+
|
|
13
|
+
// Test seam: allow overriding the ref loading result in tests.
|
|
14
|
+
let _testMode = false
|
|
15
|
+
let _testRefMap: Map<string, Set<string>> | null = null
|
|
16
|
+
let _testLoadError: string | null = null
|
|
17
|
+
|
|
18
|
+
/** @internal — test-only: inject a ref map to avoid shelling out */
|
|
19
|
+
export function _setRefMapForTesting(
|
|
20
|
+
map: Map<string, Set<string>> | null,
|
|
21
|
+
): void {
|
|
22
|
+
_testMode = true
|
|
23
|
+
_testRefMap = map
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @internal — test-only: simulate a load error */
|
|
27
|
+
export function _setLoadErrorForTesting(error: string | null): void {
|
|
28
|
+
_testLoadError = error
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @internal — test-only: reset all overrides */
|
|
32
|
+
export function _resetTestOverrides(): void {
|
|
33
|
+
_testMode = false
|
|
34
|
+
_testRefMap = null
|
|
35
|
+
_testLoadError = null
|
|
36
|
+
warnedBinaryUnavailable = false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface RefEntry {
|
|
40
|
+
ref: string
|
|
41
|
+
doc_id: string
|
|
42
|
+
criterion_id: string
|
|
43
|
+
body_text: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadRefMap(): { refMap: Map<string, Set<string>> | null; error: string | null } {
|
|
47
|
+
if (_testMode) {
|
|
48
|
+
return { refMap: _testRefMap, error: _testLoadError }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (loaded) {
|
|
52
|
+
return { refMap: cachedRefMap, error: cachedLoadError }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
loaded = true
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const output = execFileSync('supersigil', ['refs', '--all', '--format', 'json'], {
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
timeout: 30_000,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const parsed = JSON.parse(output)
|
|
64
|
+
const entries: RefEntry[] = parsed.refs ?? parsed
|
|
65
|
+
const refMap = new Map<string, Set<string>>()
|
|
66
|
+
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
let criteria = refMap.get(entry.doc_id)
|
|
69
|
+
if (!criteria) {
|
|
70
|
+
criteria = new Set<string>()
|
|
71
|
+
refMap.set(entry.doc_id, criteria)
|
|
72
|
+
}
|
|
73
|
+
criteria.add(entry.criterion_id)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cachedRefMap = refMap
|
|
77
|
+
cachedLoadError = null
|
|
78
|
+
return { refMap, error: null }
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
const message =
|
|
81
|
+
err instanceof Error ? err.message : 'Unknown error loading supersigil refs'
|
|
82
|
+
cachedRefMap = null
|
|
83
|
+
cachedLoadError = message
|
|
84
|
+
return { refMap: null, error: message }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateRef(
|
|
89
|
+
context: Rule.RuleContext,
|
|
90
|
+
node: Rule.Node,
|
|
91
|
+
value: string,
|
|
92
|
+
refMap: Map<string, Set<string>>,
|
|
93
|
+
): void {
|
|
94
|
+
const hashIndex = value.indexOf('#')
|
|
95
|
+
|
|
96
|
+
if (hashIndex === -1) {
|
|
97
|
+
context.report({
|
|
98
|
+
node,
|
|
99
|
+
messageId: 'malformed',
|
|
100
|
+
data: { ref: value },
|
|
101
|
+
})
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const docId = value.slice(0, hashIndex)
|
|
106
|
+
const criterionId = value.slice(hashIndex + 1)
|
|
107
|
+
|
|
108
|
+
const criteria = refMap.get(docId)
|
|
109
|
+
if (!criteria) {
|
|
110
|
+
context.report({
|
|
111
|
+
node,
|
|
112
|
+
messageId: 'unknownDocument',
|
|
113
|
+
data: { docId, ref: value },
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!criteria.has(criterionId)) {
|
|
119
|
+
context.report({
|
|
120
|
+
node,
|
|
121
|
+
messageId: 'unknownCriterion',
|
|
122
|
+
data: { criterionId, docId },
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rule: Rule.RuleModule = {
|
|
128
|
+
meta: {
|
|
129
|
+
type: 'problem',
|
|
130
|
+
docs: {
|
|
131
|
+
description: 'Validate Supersigil criterion refs in verifies() calls',
|
|
132
|
+
},
|
|
133
|
+
messages: {
|
|
134
|
+
malformed:
|
|
135
|
+
"Malformed criterion ref '{{ref}}'. Expected format: document-id#criterion-id",
|
|
136
|
+
unknownDocument:
|
|
137
|
+
"Unknown document '{{docId}}' in criterion ref '{{ref}}'",
|
|
138
|
+
unknownCriterion:
|
|
139
|
+
"Unknown criterion '{{criterionId}}' in document '{{docId}}'",
|
|
140
|
+
},
|
|
141
|
+
schema: [],
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
create(context) {
|
|
145
|
+
const { refMap, error } = loadRefMap()
|
|
146
|
+
|
|
147
|
+
// When supersigil binary is unavailable, log a warning to stderr and
|
|
148
|
+
// return an empty visitor so no refs are checked. This avoids failing
|
|
149
|
+
// lint (which would happen if we used context.report at error severity).
|
|
150
|
+
if (refMap === null) {
|
|
151
|
+
if (error !== null && !warnedBinaryUnavailable) {
|
|
152
|
+
warnedBinaryUnavailable = true
|
|
153
|
+
console.warn(
|
|
154
|
+
'[@supersigil/eslint-plugin] supersigil binary not available; criterion ref validation disabled',
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
return {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const refs = refMap
|
|
161
|
+
|
|
162
|
+
function checkStringLiteral(node: Rule.Node & { value: string }): void {
|
|
163
|
+
validateRef(context, node, node.value, refs)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
// Match: verifies('ref', 'ref', ...)
|
|
168
|
+
CallExpression(node) {
|
|
169
|
+
if (
|
|
170
|
+
node.callee.type === 'Identifier' &&
|
|
171
|
+
node.callee.name === 'verifies'
|
|
172
|
+
) {
|
|
173
|
+
for (const arg of node.arguments) {
|
|
174
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
175
|
+
checkStringLiteral(arg as Rule.Node & { value: string })
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Match: { verifies: ['ref', ...] } in object expressions
|
|
182
|
+
'Property[key.name="verifies"] > ArrayExpression > Literal'(
|
|
183
|
+
node: Rule.Node & { value: unknown },
|
|
184
|
+
) {
|
|
185
|
+
if (typeof node.value === 'string') {
|
|
186
|
+
checkStringLiteral(node as Rule.Node & { value: string })
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export default rule
|