@alex-dbs/coderuler 0.0.0-stage → 1.3.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/LICENSE +175 -0
- package/README.md +218 -2
- package/dist/adapter/cli/commands/check.d.ts +3 -0
- package/dist/adapter/cli/commands/check.js +93 -0
- package/dist/adapter/cli/commands/deps.d.ts +3 -0
- package/dist/adapter/cli/commands/deps.js +4 -0
- package/dist/adapter/cli/commands/inspect.d.ts +3 -0
- package/dist/adapter/cli/commands/inspect.js +59 -0
- package/dist/adapter/cli/commands/rules.d.ts +10 -0
- package/dist/adapter/cli/commands/rules.js +17 -0
- package/dist/adapter/cli/commands/skill.d.ts +4 -0
- package/dist/adapter/cli/commands/skill.js +7 -0
- package/dist/adapter/cli/commands/test.d.ts +3 -0
- package/dist/adapter/cli/commands/test.js +78 -0
- package/dist/adapter/cli/commands/update.d.ts +3 -0
- package/dist/adapter/cli/commands/update.js +7 -0
- package/dist/adapter/cli/commands/validate.d.ts +3 -0
- package/dist/adapter/cli/commands/validate.js +12 -0
- package/dist/adapter/cli/exit.d.ts +7 -0
- package/dist/adapter/cli/exit.js +10 -0
- package/dist/adapter/cli/inspect-json.d.ts +9 -0
- package/dist/adapter/cli/inspect-json.js +160 -0
- package/dist/adapter/cli/inspect-usage.d.ts +9 -0
- package/dist/adapter/cli/inspect-usage.js +14 -0
- package/dist/adapter/cli/program.d.ts +4 -0
- package/dist/adapter/cli/program.js +216 -0
- package/dist/adapter/cli/progress.d.ts +22 -0
- package/dist/adapter/cli/progress.js +79 -0
- package/dist/adapter/cli/reports.d.ts +47 -0
- package/dist/adapter/cli/reports.js +735 -0
- package/dist/adapter/cli/self-update.d.ts +1 -0
- package/dist/adapter/cli/self-update.js +89 -0
- package/dist/adapter/cli/services.d.ts +101 -0
- package/dist/adapter/cli/services.js +1 -0
- package/dist/adapter/cli/text.d.ts +5 -0
- package/dist/adapter/cli/text.js +36 -0
- package/dist/adapter/cli/version.d.ts +1 -0
- package/dist/adapter/cli/version.js +3 -0
- package/dist/adapter/decision/jev.d.ts +11 -0
- package/dist/adapter/decision/jev.js +138 -0
- package/dist/adapter/filesystem/dependencies.d.ts +26 -0
- package/dist/adapter/filesystem/dependencies.js +577 -0
- package/dist/adapter/filesystem/dependency-installation.d.ts +11 -0
- package/dist/adapter/filesystem/dependency-installation.js +373 -0
- package/dist/adapter/filesystem/project.d.ts +14 -0
- package/dist/adapter/filesystem/project.js +387 -0
- package/dist/adapter/filesystem/rule-resolution.d.ts +4 -0
- package/dist/adapter/filesystem/rule-resolution.js +516 -0
- package/dist/adapter/filesystem/rule-test-worker.d.ts +17 -0
- package/dist/adapter/filesystem/rule-test-worker.js +47 -0
- package/dist/adapter/filesystem/rule-tests.d.ts +3 -0
- package/dist/adapter/filesystem/rule-tests.js +264 -0
- package/dist/adapter/filesystem/rules.d.ts +13 -0
- package/dist/adapter/filesystem/rules.js +242 -0
- package/dist/adapter/git/changes.d.ts +1 -0
- package/dist/adapter/git/changes.js +54 -0
- package/dist/adapter/git/environment.d.ts +1 -0
- package/dist/adapter/git/environment.js +10 -0
- package/dist/adapter/github/api.d.ts +5 -0
- package/dist/adapter/github/api.js +40 -0
- package/dist/adapter/github/comments.d.ts +3 -0
- package/dist/adapter/github/comments.js +235 -0
- package/dist/adapter/github/pull-request.d.ts +16 -0
- package/dist/adapter/github/pull-request.js +139 -0
- package/dist/adapter/scope/astgrep-languages.d.ts +5 -0
- package/dist/adapter/scope/astgrep-languages.js +119 -0
- package/dist/adapter/scope/astgrep.d.ts +14 -0
- package/dist/adapter/scope/astgrep.js +183 -0
- package/dist/adapter/scope/csv.d.ts +4 -0
- package/dist/adapter/scope/csv.js +158 -0
- package/dist/adapter/scope/file.d.ts +2 -0
- package/dist/adapter/scope/file.js +4 -0
- package/dist/adapter/scope/json.d.ts +7 -0
- package/dist/adapter/scope/json.js +132 -0
- package/dist/adapter/scope/lines.d.ts +10 -0
- package/dist/adapter/scope/lines.js +42 -0
- package/dist/adapter/scope/protobuf-options.d.ts +29 -0
- package/dist/adapter/scope/protobuf-options.js +64 -0
- package/dist/adapter/scope/protobuf-parser.d.ts +30 -0
- package/dist/adapter/scope/protobuf-parser.js +772 -0
- package/dist/adapter/scope/protobuf.d.ts +5 -0
- package/dist/adapter/scope/protobuf.js +53 -0
- package/dist/adapter/scope/structured.d.ts +10 -0
- package/dist/adapter/scope/structured.js +84 -0
- package/dist/adapter/scope/yaml.d.ts +7 -0
- package/dist/adapter/scope/yaml.js +224 -0
- package/dist/adapter/skills/cli.d.ts +10 -0
- package/dist/adapter/skills/cli.js +77 -0
- package/dist/app/concurrency.d.ts +1 -0
- package/dist/app/concurrency.js +44 -0
- package/dist/app/evaluators.d.ts +14 -0
- package/dist/app/evaluators.js +175 -0
- package/dist/app/execute.d.ts +2 -0
- package/dist/app/execute.js +427 -0
- package/dist/app/jsonpath.d.ts +1 -0
- package/dist/app/jsonpath.js +8 -0
- package/dist/app/ports.d.ts +165 -0
- package/dist/app/ports.js +1 -0
- package/dist/app/rule-tests.d.ts +20 -0
- package/dist/app/rule-tests.js +11 -0
- package/dist/app/rule.d.ts +121 -0
- package/dist/app/rule.js +24 -0
- package/dist/app/selection.d.ts +2 -0
- package/dist/app/selection.js +23 -0
- package/dist/app/usage.d.ts +2 -0
- package/dist/app/usage.js +13 -0
- package/dist/app/validate.d.ts +11 -0
- package/dist/app/validate.js +415 -0
- package/dist/app/violation-comments.d.ts +10 -0
- package/dist/app/violation-comments.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +11 -0
- package/dist/infra/runtime.d.ts +3 -0
- package/dist/infra/runtime.js +168 -0
- package/dist/infra/settings.d.ts +5 -0
- package/dist/infra/settings.js +91 -0
- package/package.json +86 -4
- package/skills/coderuler-new-rule/SKILL.md +141 -0
- package/skills/coderuler-new-rule/reference.md +447 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { parse } from '@ast-grep/napi';
|
|
2
|
+
import { resolveAstGrepLanguage } from './astgrep-languages.js';
|
|
3
|
+
import { textLineSpan } from './lines.js';
|
|
4
|
+
const METAVARIABLE = /\$\$\$([A-Za-z_][A-Za-z0-9_]*)|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
5
|
+
const ANONYMOUS = /^_+$/;
|
|
6
|
+
function isMapping(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function optionalMapping(value, name) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (!isMapping(value))
|
|
13
|
+
throw new Error(`ast-grep scope option ${name} must be a mapping`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function addPatternCaptures(pattern, specs) {
|
|
17
|
+
for (const match of pattern.matchAll(METAVARIABLE)) {
|
|
18
|
+
const multiple = match[1] !== undefined;
|
|
19
|
+
const name = multiple ? match[1] : match[2];
|
|
20
|
+
if (ANONYMOUS.test(name))
|
|
21
|
+
continue;
|
|
22
|
+
if (multiple || !specs.has(name))
|
|
23
|
+
specs.set(name, multiple);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function collectCaptureSpecs(value, specs) {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
for (const item of value)
|
|
29
|
+
collectCaptureSpecs(item, specs);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!isMapping(value))
|
|
33
|
+
return;
|
|
34
|
+
for (const [key, child] of Object.entries(value)) {
|
|
35
|
+
if (key === 'pattern') {
|
|
36
|
+
if (typeof child === 'string')
|
|
37
|
+
addPatternCaptures(child, specs);
|
|
38
|
+
else if (isMapping(child) && typeof child.context === 'string')
|
|
39
|
+
addPatternCaptures(child.context, specs);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
collectCaptureSpecs(child, specs);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function readCaptures(node, specs) {
|
|
46
|
+
const captures = {};
|
|
47
|
+
for (const spec of specs) {
|
|
48
|
+
if (spec.multiple) {
|
|
49
|
+
const nodes = node.getMultipleMatches(spec.name);
|
|
50
|
+
if (nodes.length > 0)
|
|
51
|
+
captures[spec.name] = nodes.map((child) => child.text());
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const match = node.getMatch(spec.name);
|
|
55
|
+
if (match !== null)
|
|
56
|
+
captures[spec.name] = match.text();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return captures;
|
|
60
|
+
}
|
|
61
|
+
function firstDescendantText(node, kind) {
|
|
62
|
+
for (const child of node.namedChildren()) {
|
|
63
|
+
if (child.kind() === kind)
|
|
64
|
+
return child.text();
|
|
65
|
+
const nested = firstDescendantText(child, kind);
|
|
66
|
+
if (nested !== null)
|
|
67
|
+
return nested;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function receiverTypeName(method) {
|
|
72
|
+
const receiver = method.field('receiver');
|
|
73
|
+
return receiver === null ? null : firstDescendantText(receiver, 'type_identifier');
|
|
74
|
+
}
|
|
75
|
+
function declarationSegment(node) {
|
|
76
|
+
const nameNode = node.field('name');
|
|
77
|
+
if (nameNode === null)
|
|
78
|
+
return null;
|
|
79
|
+
const name = nameNode.text();
|
|
80
|
+
if (name === '')
|
|
81
|
+
return null;
|
|
82
|
+
const kind = node.kind();
|
|
83
|
+
if (kind !== 'method_declaration')
|
|
84
|
+
return `${kind}:${name}`;
|
|
85
|
+
const receiver = receiverTypeName(node);
|
|
86
|
+
return receiver === null ? `${kind}:${name}` : `${kind}:${receiver}.${name}`;
|
|
87
|
+
}
|
|
88
|
+
function identityOf(node, occurrence) {
|
|
89
|
+
const own = declarationSegment(node);
|
|
90
|
+
if (own === null)
|
|
91
|
+
return `${node.kind()}#${occurrence}`;
|
|
92
|
+
const enclosing = [];
|
|
93
|
+
let ancestor = node.parent();
|
|
94
|
+
while (ancestor !== null) {
|
|
95
|
+
const segment = declarationSegment(ancestor);
|
|
96
|
+
if (segment !== null)
|
|
97
|
+
enclosing.unshift(segment);
|
|
98
|
+
const parent = ancestor.parent();
|
|
99
|
+
if (parent === null || parent.id() === ancestor.id())
|
|
100
|
+
break;
|
|
101
|
+
ancestor = parent;
|
|
102
|
+
}
|
|
103
|
+
return [...enclosing, own].join('/');
|
|
104
|
+
}
|
|
105
|
+
function uniqueIds(bases) {
|
|
106
|
+
const taken = new Set(bases);
|
|
107
|
+
const nextSuffix = new Map();
|
|
108
|
+
const ids = [];
|
|
109
|
+
for (const base of bases) {
|
|
110
|
+
const suffixFrom = nextSuffix.get(base);
|
|
111
|
+
if (suffixFrom === undefined) {
|
|
112
|
+
nextSuffix.set(base, 1);
|
|
113
|
+
ids.push(base);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let suffix = suffixFrom;
|
|
117
|
+
while (taken.has(`${base}#${suffix}`))
|
|
118
|
+
suffix += 1;
|
|
119
|
+
const id = `${base}#${suffix}`;
|
|
120
|
+
nextSuffix.set(base, suffix + 1);
|
|
121
|
+
taken.add(id);
|
|
122
|
+
ids.push(id);
|
|
123
|
+
}
|
|
124
|
+
return ids;
|
|
125
|
+
}
|
|
126
|
+
function compileQuery(language, raw, prefix = '') {
|
|
127
|
+
const rule = raw.rule;
|
|
128
|
+
if (!isMapping(rule))
|
|
129
|
+
throw new Error(`ast-grep scope option ${prefix}rule must be a mapping`);
|
|
130
|
+
const constraints = optionalMapping(raw.constraints, `${prefix}constraints`);
|
|
131
|
+
const utils = optionalMapping(raw.utils, `${prefix}utils`);
|
|
132
|
+
const config = { rule };
|
|
133
|
+
if (constraints !== undefined)
|
|
134
|
+
config.constraints = constraints;
|
|
135
|
+
if (utils !== undefined)
|
|
136
|
+
config.utils = utils;
|
|
137
|
+
const nativeConfig = config;
|
|
138
|
+
parse(language, '').root().findAll(nativeConfig);
|
|
139
|
+
const specs = new Map();
|
|
140
|
+
collectCaptureSpecs([rule, constraints, utils], specs);
|
|
141
|
+
const captureSpecs = [...specs].map(([name, multiple]) => ({ name, multiple }));
|
|
142
|
+
return { nativeConfig, captureSpecs };
|
|
143
|
+
}
|
|
144
|
+
export function astGrep(options) {
|
|
145
|
+
const raw = options;
|
|
146
|
+
const language = raw.language;
|
|
147
|
+
if (typeof language !== 'string' || language === '') {
|
|
148
|
+
throw new Error('ast-grep scope option language must be a non-empty string');
|
|
149
|
+
}
|
|
150
|
+
const nativeLanguage = resolveAstGrepLanguage(language);
|
|
151
|
+
const tsxOptions = optionalMapping(raw.tsx, 'tsx');
|
|
152
|
+
if (tsxOptions !== undefined && nativeLanguage !== 'typescript') {
|
|
153
|
+
throw new Error('ast-grep scope option tsx is only supported for TypeScript');
|
|
154
|
+
}
|
|
155
|
+
const query = compileQuery(nativeLanguage, raw);
|
|
156
|
+
const tsxQuery = tsxOptions === undefined ? query : compileQuery('tsx', tsxOptions, 'tsx.');
|
|
157
|
+
if (nativeLanguage === 'typescript' && tsxOptions === undefined) {
|
|
158
|
+
parse('tsx', '').root().findAll(query.nativeConfig);
|
|
159
|
+
}
|
|
160
|
+
return (file) => {
|
|
161
|
+
const useTsx = nativeLanguage === 'typescript' && file.filepath.endsWith('.tsx');
|
|
162
|
+
const selected = useTsx ? tsxQuery : query;
|
|
163
|
+
const matches = parse(useTsx ? 'tsx' : nativeLanguage, file.source).root().findAll(selected.nativeConfig);
|
|
164
|
+
const occurrences = new Map();
|
|
165
|
+
const bases = [];
|
|
166
|
+
for (const node of matches) {
|
|
167
|
+
const kind = node.kind();
|
|
168
|
+
const occurrence = occurrences.get(kind) ?? 0;
|
|
169
|
+
occurrences.set(kind, occurrence + 1);
|
|
170
|
+
bases.push(identityOf(node, occurrence));
|
|
171
|
+
}
|
|
172
|
+
const ids = uniqueIds(bases);
|
|
173
|
+
return matches.map((node, index) => {
|
|
174
|
+
const text = node.text();
|
|
175
|
+
return {
|
|
176
|
+
id: ids[index],
|
|
177
|
+
source: text,
|
|
178
|
+
captures: readCaptures(node, selected.captureSpecs),
|
|
179
|
+
lines: textLineSpan(node.range().start.line + 1, text),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CsvOptions, CsvRow } from '../../app/ports.js';
|
|
2
|
+
import type { ScopeSelector } from '../../app/rule.js';
|
|
3
|
+
export declare function parseCsv(source: string, options?: CsvOptions): readonly CsvRow[];
|
|
4
|
+
export declare function csv(options?: CsvOptions): ScopeSelector<CsvRow>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { parse } from 'csv-parse/sync';
|
|
2
|
+
import { textLineSpan } from './lines.js';
|
|
3
|
+
const LEADING_BREAKS = /^(?:\r\n|\r|\n)+/;
|
|
4
|
+
const TRAILING_BREAK = /(?:\r\n|\r|\n)$/;
|
|
5
|
+
const LINE_BREAKS = /\r\n|\r|\n/g;
|
|
6
|
+
function isSingleCodePoint(value) {
|
|
7
|
+
const codePoint = value.codePointAt(0);
|
|
8
|
+
return codePoint !== undefined && value.length === (codePoint > 0xffff ? 2 : 1);
|
|
9
|
+
}
|
|
10
|
+
function resolveOptions(options) {
|
|
11
|
+
const headers = options?.headers === undefined ? true : options.headers;
|
|
12
|
+
if (typeof headers !== 'boolean')
|
|
13
|
+
throw new Error('The CSV "headers" option must be a boolean');
|
|
14
|
+
const delimiter = options?.delimiter === undefined ? ',' : options.delimiter;
|
|
15
|
+
if (typeof delimiter !== 'string'
|
|
16
|
+
|| !isSingleCodePoint(delimiter)
|
|
17
|
+
|| delimiter === '"'
|
|
18
|
+
|| delimiter === '\r'
|
|
19
|
+
|| delimiter === '\n') {
|
|
20
|
+
throw new Error('The CSV "delimiter" must be a single character other than CR, LF, or a quote');
|
|
21
|
+
}
|
|
22
|
+
const key = options?.key;
|
|
23
|
+
if (key !== undefined) {
|
|
24
|
+
if (typeof key !== 'string' || key === '')
|
|
25
|
+
throw new Error('The CSV "key" must be a non-empty string');
|
|
26
|
+
if (!headers)
|
|
27
|
+
throw new Error('The CSV "key" option requires headers');
|
|
28
|
+
}
|
|
29
|
+
return { headers, delimiter, key };
|
|
30
|
+
}
|
|
31
|
+
function parseRecords(source, delimiter) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = parse(source, {
|
|
35
|
+
bom: true,
|
|
36
|
+
cast: false,
|
|
37
|
+
delimiter,
|
|
38
|
+
info: true,
|
|
39
|
+
relax_column_count: false,
|
|
40
|
+
skip_empty_lines: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
45
|
+
throw new Error(`CSV source could not be parsed: ${reason}`);
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(parsed))
|
|
48
|
+
throw new Error('CSV source could not be parsed: unexpected parser output');
|
|
49
|
+
return parsed.map((entry) => {
|
|
50
|
+
const candidate = entry;
|
|
51
|
+
if (candidate === null || typeof candidate !== 'object') {
|
|
52
|
+
throw new Error('CSV source could not be parsed: unexpected record shape');
|
|
53
|
+
}
|
|
54
|
+
const record = 'record' in candidate ? candidate.record : undefined;
|
|
55
|
+
const info = 'info' in candidate ? candidate.info : undefined;
|
|
56
|
+
const endByte = info !== null && typeof info === 'object' && 'bytes' in info ? info.bytes : undefined;
|
|
57
|
+
if (!Array.isArray(record)
|
|
58
|
+
|| typeof endByte !== 'number'
|
|
59
|
+
|| !Number.isInteger(endByte)
|
|
60
|
+
|| endByte < 0) {
|
|
61
|
+
throw new Error('CSV source could not be parsed: unexpected record shape');
|
|
62
|
+
}
|
|
63
|
+
return { record, endByte };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function locateRecords(source, records) {
|
|
67
|
+
const located = [];
|
|
68
|
+
let charIndex = 0;
|
|
69
|
+
let byteIndex = 0;
|
|
70
|
+
let sliceStart = 0;
|
|
71
|
+
let startLine = 1;
|
|
72
|
+
for (const record of records) {
|
|
73
|
+
while (byteIndex < record.endByte) {
|
|
74
|
+
const codePoint = source.codePointAt(charIndex);
|
|
75
|
+
if (codePoint === undefined) {
|
|
76
|
+
throw new Error('CSV source could not be parsed: record offset exceeds the source');
|
|
77
|
+
}
|
|
78
|
+
byteIndex += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4;
|
|
79
|
+
charIndex += codePoint > 0xffff ? 2 : 1;
|
|
80
|
+
}
|
|
81
|
+
let sliced = source.slice(sliceStart, charIndex);
|
|
82
|
+
if (sliceStart === 0 && sliced.charCodeAt(0) === 0xfeff)
|
|
83
|
+
sliced = sliced.slice(1);
|
|
84
|
+
sliceStart = charIndex;
|
|
85
|
+
const leading = sliced.match(LEADING_BREAKS)?.[0] ?? '';
|
|
86
|
+
startLine += leading.match(LINE_BREAKS)?.length ?? 0;
|
|
87
|
+
const text = sliced.slice(leading.length).replace(TRAILING_BREAK, '');
|
|
88
|
+
located.push({ fields: record.record, source: text, lines: textLineSpan(startLine, text) });
|
|
89
|
+
startLine += (text.match(LINE_BREAKS)?.length ?? 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
return located;
|
|
92
|
+
}
|
|
93
|
+
function headerNames(located) {
|
|
94
|
+
if (located === undefined)
|
|
95
|
+
return [];
|
|
96
|
+
const names = new Set();
|
|
97
|
+
for (const name of located.fields) {
|
|
98
|
+
if (name === '')
|
|
99
|
+
throw new Error('CSV header names must be nonempty');
|
|
100
|
+
if (names.has(name))
|
|
101
|
+
throw new Error(`CSV header names must be unique but "${name}" appears twice`);
|
|
102
|
+
names.add(name);
|
|
103
|
+
}
|
|
104
|
+
return located.fields;
|
|
105
|
+
}
|
|
106
|
+
function toRecord(header, fields) {
|
|
107
|
+
const record = {};
|
|
108
|
+
header.forEach((name, index) => {
|
|
109
|
+
const value = fields[index];
|
|
110
|
+
if (value === undefined)
|
|
111
|
+
throw new Error('CSV record has fewer fields than its header');
|
|
112
|
+
Object.defineProperty(record, name, { value, enumerable: true, writable: true, configurable: true });
|
|
113
|
+
});
|
|
114
|
+
return record;
|
|
115
|
+
}
|
|
116
|
+
function rowsOf(located, options) {
|
|
117
|
+
if (!options.headers)
|
|
118
|
+
return located.map(record => record.fields);
|
|
119
|
+
const header = headerNames(located[0]);
|
|
120
|
+
return located.slice(1).map(record => toRecord(header, record.fields));
|
|
121
|
+
}
|
|
122
|
+
export function parseCsv(source, options) {
|
|
123
|
+
const resolved = resolveOptions(options);
|
|
124
|
+
return rowsOf(locateRecords(source, parseRecords(source, resolved.delimiter)), resolved);
|
|
125
|
+
}
|
|
126
|
+
function selectScopes(source, options) {
|
|
127
|
+
const located = locateRecords(source, parseRecords(source, options.delimiter));
|
|
128
|
+
if (!options.headers) {
|
|
129
|
+
return located.map((record, index) => ({
|
|
130
|
+
id: `row:${index + 1}`,
|
|
131
|
+
source: record.source,
|
|
132
|
+
data: record.fields,
|
|
133
|
+
lines: record.lines,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
const header = headerNames(located[0]);
|
|
137
|
+
const column = options.key;
|
|
138
|
+
if (column !== undefined && !header.includes(column)) {
|
|
139
|
+
throw new Error(`The CSV key column "${column}" is not present in the header`);
|
|
140
|
+
}
|
|
141
|
+
const ids = new Set();
|
|
142
|
+
return located.slice(1).map((record, index) => {
|
|
143
|
+
const data = toRecord(header, record.fields);
|
|
144
|
+
const key = column === undefined ? undefined : data[column];
|
|
145
|
+
if (column !== undefined && (key === undefined || key === '')) {
|
|
146
|
+
throw new Error(`The CSV key column "${column}" is blank in data row ${index + 1}`);
|
|
147
|
+
}
|
|
148
|
+
const id = key ?? `row:${index + 1}`;
|
|
149
|
+
if (ids.has(id))
|
|
150
|
+
throw new Error(`CSV scope ids must be unique but "${id}" appears twice`);
|
|
151
|
+
ids.add(id);
|
|
152
|
+
return { id, source: record.source, data, lines: record.lines };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
export function csv(options) {
|
|
156
|
+
const resolved = resolveOptions(options);
|
|
157
|
+
return ({ source }) => selectScopes(source, resolved);
|
|
158
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonValue, ScopeSelector } from '../../app/rule.js';
|
|
2
|
+
export interface JsonScopeOptions {
|
|
3
|
+
readonly path?: string;
|
|
4
|
+
readonly key?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseJson(source: string): JsonValue;
|
|
7
|
+
export declare function json<T = JsonValue>(options?: JsonScopeOptions): ScopeSelector<T>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { parseTree, printParseErrorCode } from 'jsonc-parser';
|
|
2
|
+
import { SourceLines } from './lines.js';
|
|
3
|
+
import { keyOf, pointerSegments, query, validatePath } from './structured.js';
|
|
4
|
+
const PARSE_OPTIONS = { disallowComments: true, allowTrailingComma: false, allowEmptyContent: false };
|
|
5
|
+
function memberOf(property) {
|
|
6
|
+
if (property.type !== 'property')
|
|
7
|
+
return undefined;
|
|
8
|
+
const pair = property.children ?? [];
|
|
9
|
+
const key = pair[0];
|
|
10
|
+
const value = pair[1];
|
|
11
|
+
if (key === undefined || value === undefined || pair.length !== 2)
|
|
12
|
+
return undefined;
|
|
13
|
+
return { name: String(key.value), value };
|
|
14
|
+
}
|
|
15
|
+
function propertyValue(node, name) {
|
|
16
|
+
for (const property of node.children ?? []) {
|
|
17
|
+
const member = memberOf(property);
|
|
18
|
+
if (member !== undefined && member.name === name)
|
|
19
|
+
return member.value;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
function objectValue(node) {
|
|
24
|
+
const result = {};
|
|
25
|
+
const names = new Set();
|
|
26
|
+
for (const property of node.children ?? []) {
|
|
27
|
+
const member = memberOf(property);
|
|
28
|
+
if (member === undefined)
|
|
29
|
+
throw new Error('JSON source contains an invalid object member');
|
|
30
|
+
if (names.has(member.name)) {
|
|
31
|
+
throw new Error(`JSON source contains a duplicate object key "${member.name}"`);
|
|
32
|
+
}
|
|
33
|
+
names.add(member.name);
|
|
34
|
+
Object.defineProperty(result, member.name, {
|
|
35
|
+
value: nodeValue(member.value),
|
|
36
|
+
enumerable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function nodeValue(node) {
|
|
44
|
+
switch (node.type) {
|
|
45
|
+
case 'object':
|
|
46
|
+
return objectValue(node);
|
|
47
|
+
case 'array':
|
|
48
|
+
return (node.children ?? []).map(nodeValue);
|
|
49
|
+
case 'null':
|
|
50
|
+
return null;
|
|
51
|
+
case 'boolean':
|
|
52
|
+
return node.value === true;
|
|
53
|
+
case 'number': {
|
|
54
|
+
const value = node.value;
|
|
55
|
+
if (!Number.isFinite(value))
|
|
56
|
+
throw new Error('JSON source contains a non-finite number');
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
case 'string':
|
|
60
|
+
return node.value;
|
|
61
|
+
default:
|
|
62
|
+
throw new Error('JSON source contains an unsupported value');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function parseDocument(source) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
const root = parseTree(source, errors, PARSE_OPTIONS);
|
|
68
|
+
const first = errors[0];
|
|
69
|
+
if (root === undefined || first !== undefined) {
|
|
70
|
+
const problem = first === undefined
|
|
71
|
+
? 'no JSON content'
|
|
72
|
+
: `${printParseErrorCode(first.error)} at offset ${first.offset}`;
|
|
73
|
+
throw new Error(`JSON source could not be parsed: ${problem}`);
|
|
74
|
+
}
|
|
75
|
+
return { data: nodeValue(root), root };
|
|
76
|
+
}
|
|
77
|
+
export function parseJson(source) {
|
|
78
|
+
return parseDocument(source).data;
|
|
79
|
+
}
|
|
80
|
+
function nodeAt(root, segments) {
|
|
81
|
+
let node = root;
|
|
82
|
+
for (const segment of segments) {
|
|
83
|
+
if (node.type === 'object') {
|
|
84
|
+
const member = propertyValue(node, segment);
|
|
85
|
+
if (member === undefined)
|
|
86
|
+
throw new Error(`The JSON pointer segment "${segment}" does not exist`);
|
|
87
|
+
node = member;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (node.type === 'array') {
|
|
91
|
+
const index = Number(segment);
|
|
92
|
+
const member = Number.isInteger(index) && index >= 0 ? (node.children ?? [])[index] : undefined;
|
|
93
|
+
if (member === undefined)
|
|
94
|
+
throw new Error(`The JSON pointer segment "${segment}" does not exist`);
|
|
95
|
+
node = member;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`The JSON pointer segment "${segment}" does not exist`);
|
|
99
|
+
}
|
|
100
|
+
return node;
|
|
101
|
+
}
|
|
102
|
+
function selectScopes(source, selector, keySelector) {
|
|
103
|
+
const document = parseDocument(source);
|
|
104
|
+
const lines = new SourceLines(source);
|
|
105
|
+
const matches = selector.trim() === '$'
|
|
106
|
+
? [{ pointer: '', value: document.data }]
|
|
107
|
+
: query(document.data, selector);
|
|
108
|
+
return matches.map((match) => {
|
|
109
|
+
const node = nodeAt(document.root, pointerSegments(match.pointer));
|
|
110
|
+
const start = node.offset;
|
|
111
|
+
const end = node.offset + node.length;
|
|
112
|
+
return {
|
|
113
|
+
id: keySelector === undefined ? (match.pointer === '' ? '$' : match.pointer) : keyOf(match.value, keySelector),
|
|
114
|
+
source: source.slice(start, end),
|
|
115
|
+
data: match.value,
|
|
116
|
+
lines: lines.span(start, end),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function json(options) {
|
|
121
|
+
const selector = options?.path === undefined ? '$' : options.path;
|
|
122
|
+
if (typeof selector !== 'string')
|
|
123
|
+
throw new Error('The JSON scope "path" must be a non-empty string');
|
|
124
|
+
validatePath(selector, 'JSON scope path');
|
|
125
|
+
const keySelector = options?.key;
|
|
126
|
+
if (keySelector !== undefined && typeof keySelector !== 'string') {
|
|
127
|
+
throw new Error('The JSON scope "key" must be a non-empty string');
|
|
128
|
+
}
|
|
129
|
+
if (keySelector !== undefined)
|
|
130
|
+
validatePath(keySelector, 'JSON scope key');
|
|
131
|
+
return ({ source }) => selectScopes(source, selector, keySelector);
|
|
132
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface LineSpan {
|
|
2
|
+
readonly start: number;
|
|
3
|
+
readonly end: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function textLineSpan(startLine: number, text: string): LineSpan;
|
|
6
|
+
export declare class SourceLines {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(source: string);
|
|
9
|
+
span(start: number, end: number): LineSpan;
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const TRAILING_LINE_BREAKS = /(?:\r\n|\r|\n)$/;
|
|
2
|
+
const LINE_BREAKS = /\r\n|[\r\n]/g;
|
|
3
|
+
export function textLineSpan(startLine, text) {
|
|
4
|
+
const breaks = text.replace(TRAILING_LINE_BREAKS, '').match(LINE_BREAKS);
|
|
5
|
+
return { start: startLine, end: startLine + (breaks === null ? 0 : breaks.length) };
|
|
6
|
+
}
|
|
7
|
+
export class SourceLines {
|
|
8
|
+
#starts;
|
|
9
|
+
constructor(source) {
|
|
10
|
+
const starts = [0];
|
|
11
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
12
|
+
const code = source.charCodeAt(index);
|
|
13
|
+
if (code === 10)
|
|
14
|
+
starts.push(index + 1);
|
|
15
|
+
else if (code === 13) {
|
|
16
|
+
if (source.charCodeAt(index + 1) === 10)
|
|
17
|
+
index += 1;
|
|
18
|
+
starts.push(index + 1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
this.#starts = starts;
|
|
22
|
+
}
|
|
23
|
+
span(start, end) {
|
|
24
|
+
const first = this.#lineAt(start);
|
|
25
|
+
if (end <= start)
|
|
26
|
+
return { start: first, end: first };
|
|
27
|
+
return { start: first, end: this.#lineAt(end - 1) };
|
|
28
|
+
}
|
|
29
|
+
#lineAt(offset) {
|
|
30
|
+
const starts = this.#starts;
|
|
31
|
+
let low = 0;
|
|
32
|
+
let high = starts.length - 1;
|
|
33
|
+
while (low < high) {
|
|
34
|
+
const middle = (low + high + 1) >> 1;
|
|
35
|
+
if (starts[middle] <= offset)
|
|
36
|
+
low = middle;
|
|
37
|
+
else
|
|
38
|
+
high = middle - 1;
|
|
39
|
+
}
|
|
40
|
+
return low + 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { JsonValue } from '../../app/rule.js';
|
|
2
|
+
export type ProtoSpan = {
|
|
3
|
+
readonly start: number;
|
|
4
|
+
readonly end: number;
|
|
5
|
+
};
|
|
6
|
+
export type OptionScalar = {
|
|
7
|
+
readonly kind: 'scalar';
|
|
8
|
+
readonly value: string | number | boolean;
|
|
9
|
+
readonly span: ProtoSpan;
|
|
10
|
+
};
|
|
11
|
+
export type OptionMessage = {
|
|
12
|
+
readonly kind: 'message';
|
|
13
|
+
readonly fields: Map<string, OptionField>;
|
|
14
|
+
span?: ProtoSpan;
|
|
15
|
+
};
|
|
16
|
+
export type OptionNode = OptionScalar | OptionMessage;
|
|
17
|
+
type OptionField = {
|
|
18
|
+
readonly values: OptionNode[];
|
|
19
|
+
repeated: boolean;
|
|
20
|
+
span: ProtoSpan;
|
|
21
|
+
};
|
|
22
|
+
export declare function optionMessage(span?: ProtoSpan): OptionMessage;
|
|
23
|
+
export declare function extendSpan(current: ProtoSpan | undefined, next: ProtoSpan): ProtoSpan;
|
|
24
|
+
export declare function addOptionField(message: OptionMessage, key: string, values: readonly OptionNode[], span: ProtoSpan, list: boolean): void;
|
|
25
|
+
export declare function assignOption(root: OptionMessage, path: readonly string[], values: readonly OptionNode[], valueSpan: ProtoSpan, list: boolean, entrySpan: ProtoSpan): void;
|
|
26
|
+
export declare function optionsJson(message: OptionMessage, pointer: string, spans: Map<string, ProtoSpan>): {
|
|
27
|
+
readonly [key: string]: JsonValue;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { pointerSegment } from './structured.js';
|
|
2
|
+
export function optionMessage(span) {
|
|
3
|
+
return span === undefined ? { kind: 'message', fields: new Map() } : { kind: 'message', fields: new Map(), span };
|
|
4
|
+
}
|
|
5
|
+
export function extendSpan(current, next) {
|
|
6
|
+
return current === undefined
|
|
7
|
+
? next
|
|
8
|
+
: { start: Math.min(current.start, next.start), end: Math.max(current.end, next.end) };
|
|
9
|
+
}
|
|
10
|
+
export function addOptionField(message, key, values, span, list) {
|
|
11
|
+
const field = message.fields.get(key);
|
|
12
|
+
if (field === undefined) {
|
|
13
|
+
message.fields.set(key, { values: [...values], repeated: list, span });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
field.values.push(...values);
|
|
17
|
+
field.repeated = true;
|
|
18
|
+
field.span = extendSpan(field.span, span);
|
|
19
|
+
}
|
|
20
|
+
export function assignOption(root, path, values, valueSpan, list, entrySpan) {
|
|
21
|
+
root.span = extendSpan(root.span, entrySpan);
|
|
22
|
+
let current = root;
|
|
23
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
24
|
+
const segment = path[index];
|
|
25
|
+
const field = current.fields.get(segment);
|
|
26
|
+
if (field === undefined) {
|
|
27
|
+
const child = optionMessage(entrySpan);
|
|
28
|
+
current.fields.set(segment, { values: [child], repeated: false, span: entrySpan });
|
|
29
|
+
current = child;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const child = field.values[0];
|
|
33
|
+
if (field.repeated || field.values.length !== 1 || child?.kind !== 'message') {
|
|
34
|
+
throw new Error(`Option "${path.join('.')}" sets a field inside a non-message value`);
|
|
35
|
+
}
|
|
36
|
+
child.span = extendSpan(child.span, entrySpan);
|
|
37
|
+
current = child;
|
|
38
|
+
}
|
|
39
|
+
addOptionField(current, path[path.length - 1], values, valueSpan, list);
|
|
40
|
+
}
|
|
41
|
+
export function optionsJson(message, pointer, spans) {
|
|
42
|
+
if (message.span !== undefined)
|
|
43
|
+
spans.set(pointer, message.span);
|
|
44
|
+
const result = {};
|
|
45
|
+
for (const [key, field] of message.fields) {
|
|
46
|
+
const childPointer = `${pointer}/${pointerSegment(key)}`;
|
|
47
|
+
if (field.repeated) {
|
|
48
|
+
spans.set(childPointer, field.span);
|
|
49
|
+
const values = field.values.map((value, index) => nodeJson(value, `${childPointer}/${index}`, spans));
|
|
50
|
+
Object.defineProperty(result, key, { value: values, enumerable: true, writable: true, configurable: true });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
Object.defineProperty(result, key, { value: nodeJson(field.values[0], childPointer, spans), enumerable: true, writable: true, configurable: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function nodeJson(node, pointer, spans) {
|
|
59
|
+
if (node.kind === 'scalar') {
|
|
60
|
+
spans.set(pointer, node.span);
|
|
61
|
+
return node.value;
|
|
62
|
+
}
|
|
63
|
+
return optionsJson(node, pointer, spans);
|
|
64
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { JsonValue } from '../../app/rule.js';
|
|
2
|
+
import { type ProtoSpan } from './protobuf-options.js';
|
|
3
|
+
export declare const DECLARATION_LISTS: readonly ['services', 'rpcs', 'messages', 'fields', 'oneofs', 'enums', 'enumValues', 'extensions'];
|
|
4
|
+
export type DeclarationList = (typeof DECLARATION_LISTS)[number];
|
|
5
|
+
export type ProtoDeclaration = {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly parent: string | null;
|
|
9
|
+
readonly options: {
|
|
10
|
+
readonly [key: string]: JsonValue;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export type ProtoDocument = {
|
|
14
|
+
readonly options: {
|
|
15
|
+
readonly [key: string]: JsonValue;
|
|
16
|
+
};
|
|
17
|
+
readonly services: readonly ProtoDeclaration[];
|
|
18
|
+
readonly rpcs: readonly ProtoDeclaration[];
|
|
19
|
+
readonly messages: readonly ProtoDeclaration[];
|
|
20
|
+
readonly fields: readonly ProtoDeclaration[];
|
|
21
|
+
readonly oneofs: readonly ProtoDeclaration[];
|
|
22
|
+
readonly enums: readonly ProtoDeclaration[];
|
|
23
|
+
readonly enumValues: readonly ProtoDeclaration[];
|
|
24
|
+
readonly extensions: readonly ProtoDeclaration[];
|
|
25
|
+
};
|
|
26
|
+
export type ProtoFile = {
|
|
27
|
+
readonly data: ProtoDocument;
|
|
28
|
+
readonly spans: ReadonlyMap<string, ProtoSpan>;
|
|
29
|
+
};
|
|
30
|
+
export declare function readProtoFile(source: string): ProtoFile;
|