@gobing-ai/ts-rule-engine 0.3.1 → 0.3.2
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 +328 -58
- package/dist/config/extensions.d.ts +10 -7
- package/dist/config/extensions.d.ts.map +1 -1
- package/dist/config/extensions.js +48 -23
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +17 -12
- package/dist/engine.d.ts +13 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +107 -45
- package/dist/evaluators/agent-detection-evaluator.d.ts.map +1 -1
- package/dist/evaluators/agent-detection-evaluator.js +2 -9
- package/dist/evaluators/coverage-gate-evaluator.d.ts.map +1 -1
- package/dist/evaluators/coverage-gate-evaluator.js +4 -5
- package/dist/evaluators/exit-code-evaluator.d.ts.map +1 -1
- package/dist/evaluators/exit-code-evaluator.js +6 -23
- package/dist/evaluators/file-utils.d.ts +25 -1
- package/dist/evaluators/file-utils.d.ts.map +1 -1
- package/dist/evaluators/file-utils.js +48 -8
- package/dist/evaluators/forbidden-import-evaluator.js +2 -10
- package/dist/evaluators/path-evaluator.d.ts.map +1 -1
- package/dist/evaluators/path-evaluator.js +5 -18
- package/dist/evaluators/regex-evaluator.js +3 -11
- package/dist/evaluators/ripgrep-evaluator.d.ts +50 -0
- package/dist/evaluators/ripgrep-evaluator.d.ts.map +1 -0
- package/dist/evaluators/ripgrep-evaluator.js +145 -0
- package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -1
- package/dist/evaluators/schema-artifact-evaluator.js +3 -7
- package/dist/evaluators/sg-evaluator.d.ts +10 -2
- package/dist/evaluators/sg-evaluator.d.ts.map +1 -1
- package/dist/evaluators/sg-evaluator.js +21 -4
- package/dist/evaluators/test-location-evaluator.d.ts +2 -2
- package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
- package/dist/evaluators/test-location-evaluator.js +2 -15
- package/dist/events.d.ts +33 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +0 -0
- package/dist/fixers/fixers.d.ts +1 -1
- package/dist/fixers/fixers.d.ts.map +1 -1
- package/dist/fixers/fixers.js +4 -5
- package/dist/fixers/test-stub-fixer.d.ts +1 -1
- package/dist/fixers/test-stub-fixer.d.ts.map +1 -1
- package/dist/fixers/test-stub-fixer.js +3 -4
- package/dist/host/builtins.d.ts.map +1 -1
- package/dist/host/builtins.js +5 -3
- package/dist/host/rule-engine-host.d.ts +1 -1
- package/dist/host/rule-engine-host.d.ts.map +1 -1
- package/dist/host/rule-engine-host.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/package.json +4 -5
- package/src/config/extensions.ts +58 -29
- package/src/config/loader.ts +27 -12
- package/src/engine.ts +132 -47
- package/src/evaluators/agent-detection-evaluator.ts +2 -8
- package/src/evaluators/coverage-gate-evaluator.ts +4 -5
- package/src/evaluators/exit-code-evaluator.ts +6 -23
- package/src/evaluators/file-utils.ts +70 -8
- package/src/evaluators/forbidden-import-evaluator.ts +2 -9
- package/src/evaluators/path-evaluator.ts +5 -18
- package/src/evaluators/regex-evaluator.ts +4 -11
- package/src/evaluators/ripgrep-evaluator.ts +167 -0
- package/src/evaluators/schema-artifact-evaluator.ts +3 -8
- package/src/evaluators/sg-evaluator.ts +21 -4
- package/src/evaluators/test-location-evaluator.ts +3 -16
- package/src/events.ts +13 -0
- package/src/fixers/fixers.ts +12 -6
- package/src/fixers/test-stub-fixer.ts +4 -5
- package/src/host/builtins.ts +5 -3
- package/src/host/rule-engine-host.ts +1 -1
- package/src/index.ts +8 -1
- package/src/types.ts +7 -0
- package/dist/host/capability-registry.d.ts +0 -10
- package/dist/host/capability-registry.d.ts.map +0 -1
- package/dist/host/capability-registry.js +0 -9
- package/src/host/capability-registry.ts +0 -9
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { dirnamePath, NodeFileSystem, relativePath, resolvePath, walkDir, } from '@gobing-ai/ts-runtime';
|
|
2
|
+
/**
|
|
3
|
+
* Directory names pruned from every file walk — heavy or generated trees that no rule
|
|
4
|
+
* should scan. Shared so subprocess-backed evaluators (e.g. `sg`) can forward the same
|
|
5
|
+
* skip-list to the external tool instead of relying on each rule to remember it.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_EXCLUDES = new Set(['.git', 'node_modules', 'dist', '.coverage', '.astro', '.wrangler']);
|
|
4
8
|
/** Resolve source files for evaluators using conservative path-fragment matching. */
|
|
5
9
|
export async function discoverFiles(options) {
|
|
6
10
|
const fs = options.fs ?? new NodeFileSystem();
|
|
7
|
-
const allFiles = await walkDir(options.workdir, fs);
|
|
11
|
+
const allFiles = await walkDir(options.workdir, fs, DEFAULT_EXCLUDES);
|
|
8
12
|
return allFiles
|
|
9
|
-
.map((path) =>
|
|
13
|
+
.map((path) => relativePath(options.workdir, path))
|
|
10
14
|
.filter((path) => !path.split('/').some((segment) => DEFAULT_EXCLUDES.has(segment)))
|
|
11
15
|
.filter((path) => matchesAny(path, options.include) &&
|
|
12
16
|
(options.exclude === undefined || !matchesAny(path, options.exclude)));
|
|
13
17
|
}
|
|
14
18
|
/** Read a file from a workdir-relative path. */
|
|
15
19
|
export async function readWorkdirFile(workdir, filePath, fs = new NodeFileSystem()) {
|
|
16
|
-
return await fs.readFile(
|
|
20
|
+
return await fs.readFile(resolvePath(workdir, filePath));
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Discover in-scope files and read each once — the shared scaffolding behind the
|
|
@@ -44,11 +48,11 @@ async function discoverFilesByGlob(workdir, include, exclude, fs) {
|
|
|
44
48
|
}
|
|
45
49
|
/** Ensure a path is workdir-relative for findings. */
|
|
46
50
|
export function relativeToWorkdir(workdir, path) {
|
|
47
|
-
return
|
|
51
|
+
return relativePath(workdir, resolvePath(path));
|
|
48
52
|
}
|
|
49
53
|
/** Return parent directory for a workdir-relative path. */
|
|
50
54
|
export function relativeParent(path) {
|
|
51
|
-
const parent =
|
|
55
|
+
const parent = dirnamePath(path);
|
|
52
56
|
return parent === '.' ? '' : parent;
|
|
53
57
|
}
|
|
54
58
|
/** Return true when a path matches any supplied fragment or suffix. */
|
|
@@ -108,6 +112,42 @@ export function escapeRegExp(value) {
|
|
|
108
112
|
export function stringArray(value) {
|
|
109
113
|
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : undefined;
|
|
110
114
|
}
|
|
115
|
+
function requiredConfigError(kind, key, evaluator) {
|
|
116
|
+
const who = evaluator !== undefined ? `${evaluator} evaluator` : 'evaluator';
|
|
117
|
+
return new Error(`${who} requires ${kind} config "${key}"`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read a string entry from a rule's evaluator config. Returns the value when it is a string,
|
|
121
|
+
* the `fallback` when one is supplied, otherwise throws — so required keys fail loudly.
|
|
122
|
+
*/
|
|
123
|
+
export function configString(config, key, fallback, options = {}) {
|
|
124
|
+
const value = config[key];
|
|
125
|
+
if (typeof value === 'string')
|
|
126
|
+
return value;
|
|
127
|
+
if (fallback !== undefined)
|
|
128
|
+
return fallback;
|
|
129
|
+
throw requiredConfigError('string', key, options.evaluator);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Read a string-array entry from a rule's evaluator config. A bare string is coerced to a
|
|
133
|
+
* single-element array. Returns the `fallback` when one is supplied and the value is absent;
|
|
134
|
+
* otherwise throws — so required keys fail loudly.
|
|
135
|
+
*/
|
|
136
|
+
export function configArray(config, key, fallback, options = {}) {
|
|
137
|
+
const value = config[key];
|
|
138
|
+
if (Array.isArray(value) && value.every((item) => typeof item === 'string'))
|
|
139
|
+
return value;
|
|
140
|
+
if (typeof value === 'string')
|
|
141
|
+
return [value];
|
|
142
|
+
if (fallback !== undefined)
|
|
143
|
+
return fallback;
|
|
144
|
+
throw requiredConfigError('string[]', key, options.evaluator);
|
|
145
|
+
}
|
|
146
|
+
/** Read a finite-number entry from a rule's evaluator config, falling back when absent or invalid. */
|
|
147
|
+
export function configNumber(config, key, fallback) {
|
|
148
|
+
const value = config[key];
|
|
149
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
150
|
+
}
|
|
111
151
|
/**
|
|
112
152
|
* Split a leading ripgrep/PCRE-style `(?flags)` inline group off a regex source.
|
|
113
153
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createFinding, } from '../types.js';
|
|
2
|
-
import { escapeRegExp, scanFiles, stringArray } from './file-utils.js';
|
|
2
|
+
import { configArray, escapeRegExp, scanFiles, stringArray } from './file-utils.js';
|
|
3
3
|
/**
|
|
4
4
|
* Detects forbidden imports / API usage.
|
|
5
5
|
*
|
|
@@ -26,7 +26,7 @@ export class ForbiddenImportEvaluator {
|
|
|
26
26
|
}
|
|
27
27
|
/** Legacy path: `{ patterns: string[] }`, substring-matched against import specifiers. */
|
|
28
28
|
async evaluateSimple(rule, context, config) {
|
|
29
|
-
const forbidden =
|
|
29
|
+
const forbidden = configArray(config, 'patterns', undefined, { evaluator: 'forbidden-import' });
|
|
30
30
|
const files = await scanFiles({
|
|
31
31
|
workdir: context.workdir,
|
|
32
32
|
include: rule.include ?? ['.ts', '.tsx', '.js', '.jsx'],
|
|
@@ -94,11 +94,3 @@ function compileEntry(entry) {
|
|
|
94
94
|
}
|
|
95
95
|
return { regex: new RegExp(entry.pattern), label: entry.pattern };
|
|
96
96
|
}
|
|
97
|
-
function arrayConfig(config, key) {
|
|
98
|
-
const value = config[key];
|
|
99
|
-
if (Array.isArray(value) && value.every((item) => typeof item === 'string'))
|
|
100
|
-
return value;
|
|
101
|
-
if (typeof value === 'string')
|
|
102
|
-
return [value];
|
|
103
|
-
throw new Error(`forbidden-import evaluator requires string[] config "${key}"`);
|
|
104
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/path-evaluator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"path-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/path-evaluator.ts"],"names":[],"mappings":"AACA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;GASG;AACH,qBAAa,aAAc,YAAW,aAAa;IAC/C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;;IAMpC,kEAAkE;IAC5D,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IASzF,qEAAqE;YACvD,YAAY;IAuC1B,uEAAuE;YACzD,gBAAgB;CAmBjC"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { NodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
1
|
+
import { joinPath, NodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
3
2
|
import { createFinding, } from '../types.js';
|
|
4
|
-
import { discoverFiles, matchesGlob } from './file-utils.js';
|
|
3
|
+
import { configArray, configString, discoverFiles, matchesGlob } from './file-utils.js';
|
|
5
4
|
/**
|
|
6
5
|
* Evaluates file/directory presence constraints.
|
|
7
6
|
*
|
|
@@ -56,11 +55,11 @@ export class PathEvaluator {
|
|
|
56
55
|
}
|
|
57
56
|
/** Explicit form: exact `paths` checked with `mode` require/forbid. */
|
|
58
57
|
async evaluateExplicit(rule, context, config) {
|
|
59
|
-
const paths =
|
|
60
|
-
const mode =
|
|
58
|
+
const paths = configArray(config, 'paths', undefined, { evaluator: 'path' });
|
|
59
|
+
const mode = configString(config, 'mode', 'require');
|
|
61
60
|
const findings = [];
|
|
62
61
|
for (const path of paths) {
|
|
63
|
-
const exists = await this.fs.exists(
|
|
62
|
+
const exists = await this.fs.exists(joinPath(context.workdir, path));
|
|
64
63
|
if (mode === 'forbid' && exists) {
|
|
65
64
|
findings.push(createFinding(rule, `Forbidden path exists: ${path}`, path, { code: 'path:forbidden' }));
|
|
66
65
|
}
|
|
@@ -71,15 +70,3 @@ export class PathEvaluator {
|
|
|
71
70
|
return { findings, fixes: [] };
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
|
-
function arrayConfig(config, key) {
|
|
75
|
-
const value = config[key];
|
|
76
|
-
if (Array.isArray(value) && value.every((item) => typeof item === 'string'))
|
|
77
|
-
return value;
|
|
78
|
-
if (typeof value === 'string')
|
|
79
|
-
return [value];
|
|
80
|
-
throw new Error(`path evaluator requires config "${key}" (string[]) or "must" (present|absent)`);
|
|
81
|
-
}
|
|
82
|
-
function stringConfig(config, key, fallback) {
|
|
83
|
-
const value = config[key];
|
|
84
|
-
return typeof value === 'string' ? value : fallback;
|
|
85
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createFinding, } from '../types.js';
|
|
2
|
-
import { parseInlineFlags, scanFiles } from './file-utils.js';
|
|
2
|
+
import { configString, parseInlineFlags, scanFiles } from './file-utils.js';
|
|
3
3
|
/**
|
|
4
4
|
* Evaluates whether source files match or avoid a regex pattern.
|
|
5
5
|
*
|
|
@@ -16,8 +16,8 @@ export class RegexEvaluator {
|
|
|
16
16
|
/** Evaluate regex-based presence or absence constraints. */
|
|
17
17
|
async evaluate(rule, context) {
|
|
18
18
|
const config = rule.evaluator.config ?? {};
|
|
19
|
-
const { pattern, flags } = normalizePattern(
|
|
20
|
-
const mode =
|
|
19
|
+
const { pattern, flags } = normalizePattern(configString(config, 'pattern', undefined, { evaluator: 'regex' }), configString(config, 'flags', ''), config.multiline === true);
|
|
20
|
+
const mode = configString(config, 'mode', 'forbid');
|
|
21
21
|
const regex = new RegExp(pattern, flags);
|
|
22
22
|
const files = await scanFiles({
|
|
23
23
|
workdir: context.workdir,
|
|
@@ -81,14 +81,6 @@ function normalizePattern(rawPattern, rawFlags, multiline) {
|
|
|
81
81
|
flagSet.add('s');
|
|
82
82
|
return { pattern, flags: [...flagSet].join('') };
|
|
83
83
|
}
|
|
84
|
-
function stringConfig(config, key, fallback) {
|
|
85
|
-
const value = config[key];
|
|
86
|
-
if (typeof value === 'string')
|
|
87
|
-
return value;
|
|
88
|
-
if (fallback !== undefined)
|
|
89
|
-
return fallback;
|
|
90
|
-
throw new Error(`regex evaluator requires string config "${key}"`);
|
|
91
|
-
}
|
|
92
84
|
/** Return the one-based line containing a string offset. */
|
|
93
85
|
function lineForOffset(content, offset) {
|
|
94
86
|
let line = 1;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ProcessExecutor } from '@gobing-ai/ts-runtime';
|
|
2
|
+
import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Evaluates source files against a regex using the `rg` (ripgrep) CLI.
|
|
5
|
+
*
|
|
6
|
+
* This is the real ripgrep-backed engine registered under the `rg` rule type. Unlike
|
|
7
|
+
* the {@link import('./regex-evaluator').RegexEvaluator} (`regex` type, JS `RegExp`),
|
|
8
|
+
* it runs ripgrep's linear-time Rust regex engine: ReDoS-immune, parallel, and pruning
|
|
9
|
+
* heavy trees during traversal. The dialect differs from JS `RegExp` — no lookbehind or
|
|
10
|
+
* backreferences — so rule patterns must be ripgrep-compatible (enforced by the
|
|
11
|
+
* `rg-dialect` spur rule; see {@link isRipgrepCompatiblePattern}).
|
|
12
|
+
*
|
|
13
|
+
* ## Options (in `evaluator.config`)
|
|
14
|
+
* - `pattern` — ripgrep regex to search for (required).
|
|
15
|
+
* - `mode` — `forbid` (default): each match is a finding. `require`: a finding per file
|
|
16
|
+
* that lacks the pattern.
|
|
17
|
+
* - `multiline` — when `true`, patterns may span lines (`rg -U --multiline-dotall`).
|
|
18
|
+
*
|
|
19
|
+
* Inline flags like `(?i)` are passed through to ripgrep, which supports them natively.
|
|
20
|
+
*
|
|
21
|
+
* The rule's `include` globs and `exclude` globs (plus {@link DEFAULT_EXCLUDES}) are
|
|
22
|
+
* forwarded as `--glob` / `--glob '!…'` so ripgrep prunes during traversal rather than
|
|
23
|
+
* walking everything — the same skip-list the in-process discovery path uses, applied
|
|
24
|
+
* regardless of whether the workspace is a git repo or has a `.gitignore`.
|
|
25
|
+
*/
|
|
26
|
+
export declare class RipgrepEvaluator implements RuleEvaluator {
|
|
27
|
+
private readonly executor;
|
|
28
|
+
constructor(executor?: ProcessExecutor);
|
|
29
|
+
/** Run ripgrep and emit findings for matches (forbid) or absent files (require). */
|
|
30
|
+
evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Report whether a regex `pattern` is safe to run under ripgrep's engine.
|
|
34
|
+
*
|
|
35
|
+
* ripgrep's Rust `regex` crate is linear-time and therefore omits features that require
|
|
36
|
+
* backtracking — lookbehind and backreferences. A pattern using them works under the JS
|
|
37
|
+
* `regex` evaluator but fails to compile under `rg`. The `rg-dialect` spur rule and the
|
|
38
|
+
* downstream rule-file converter use this to keep incompatible patterns on the `regex`
|
|
39
|
+
* type instead of silently breaking them on `rg`.
|
|
40
|
+
*
|
|
41
|
+
* @returns `{ compatible: true }` or `{ compatible: false, feature }` naming the first
|
|
42
|
+
* unsupported construct found.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isRipgrepCompatiblePattern(pattern: string): {
|
|
45
|
+
compatible: true;
|
|
46
|
+
} | {
|
|
47
|
+
compatible: false;
|
|
48
|
+
feature: string;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=ripgrep-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ripgrep-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/ripgrep-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,gBAAiB,YAAW,aAAa;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;gBAE/B,QAAQ,GAAE,eAA2C;IAIjE,oFAAoF;IAC9E,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CA2B5F;AA+ED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACtC,OAAO,EAAE,MAAM,GAChB;IAAE,UAAU,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAK/D"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { NodeProcessExecutor } from '@gobing-ai/ts-runtime';
|
|
2
|
+
import { createFinding, } from '../types.js';
|
|
3
|
+
import { configString, DEFAULT_EXCLUDES } from './file-utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Evaluates source files against a regex using the `rg` (ripgrep) CLI.
|
|
6
|
+
*
|
|
7
|
+
* This is the real ripgrep-backed engine registered under the `rg` rule type. Unlike
|
|
8
|
+
* the {@link import('./regex-evaluator.js').RegexEvaluator} (`regex` type, JS `RegExp`),
|
|
9
|
+
* it runs ripgrep's linear-time Rust regex engine: ReDoS-immune, parallel, and pruning
|
|
10
|
+
* heavy trees during traversal. The dialect differs from JS `RegExp` — no lookbehind or
|
|
11
|
+
* backreferences — so rule patterns must be ripgrep-compatible (enforced by the
|
|
12
|
+
* `rg-dialect` spur rule; see {@link isRipgrepCompatiblePattern}).
|
|
13
|
+
*
|
|
14
|
+
* ## Options (in `evaluator.config`)
|
|
15
|
+
* - `pattern` — ripgrep regex to search for (required).
|
|
16
|
+
* - `mode` — `forbid` (default): each match is a finding. `require`: a finding per file
|
|
17
|
+
* that lacks the pattern.
|
|
18
|
+
* - `multiline` — when `true`, patterns may span lines (`rg -U --multiline-dotall`).
|
|
19
|
+
*
|
|
20
|
+
* Inline flags like `(?i)` are passed through to ripgrep, which supports them natively.
|
|
21
|
+
*
|
|
22
|
+
* The rule's `include` globs and `exclude` globs (plus {@link DEFAULT_EXCLUDES}) are
|
|
23
|
+
* forwarded as `--glob` / `--glob '!…'` so ripgrep prunes during traversal rather than
|
|
24
|
+
* walking everything — the same skip-list the in-process discovery path uses, applied
|
|
25
|
+
* regardless of whether the workspace is a git repo or has a `.gitignore`.
|
|
26
|
+
*/
|
|
27
|
+
export class RipgrepEvaluator {
|
|
28
|
+
executor;
|
|
29
|
+
constructor(executor = new NodeProcessExecutor()) {
|
|
30
|
+
this.executor = executor;
|
|
31
|
+
}
|
|
32
|
+
/** Run ripgrep and emit findings for matches (forbid) or absent files (require). */
|
|
33
|
+
async evaluate(rule, context) {
|
|
34
|
+
const config = rule.evaluator.config ?? {};
|
|
35
|
+
const pattern = configString(config, 'pattern', undefined, { evaluator: 'rg' });
|
|
36
|
+
const mode = configString(config, 'mode', 'forbid');
|
|
37
|
+
const multiline = config.multiline === true;
|
|
38
|
+
const args = buildArgs(pattern, mode, multiline, rule.include ?? [], rule.exclude ?? []);
|
|
39
|
+
const result = await this.executor.run({
|
|
40
|
+
command: 'rg',
|
|
41
|
+
args,
|
|
42
|
+
cwd: context.workdir,
|
|
43
|
+
timeout: 60_000,
|
|
44
|
+
rejectOnError: false,
|
|
45
|
+
label: 'rg',
|
|
46
|
+
});
|
|
47
|
+
// ripgrep exits 0 with matches, 1 when none, 2 on error. Treat 2 (or any non-0/1
|
|
48
|
+
// with stderr) as a hard failure so a broken pattern or missing `rg` fails loud.
|
|
49
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
50
|
+
const detail = result.stderr.trim();
|
|
51
|
+
throw new Error(`rg failed (exit ${result.exitCode})${detail.length > 0 ? `: ${detail}` : ''}`);
|
|
52
|
+
}
|
|
53
|
+
return mode === 'require'
|
|
54
|
+
? { findings: requireFindings(rule, pattern, result.stdout), fixes: [] }
|
|
55
|
+
: { findings: forbidFindings(rule, pattern, result.stdout), fixes: [] };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Build the ripgrep argument list for the given mode and scope. */
|
|
59
|
+
function buildArgs(pattern, mode, multiline, include, exclude) {
|
|
60
|
+
const args = [];
|
|
61
|
+
if (multiline)
|
|
62
|
+
args.push('-U', '--multiline-dotall');
|
|
63
|
+
if (mode === 'require') {
|
|
64
|
+
// Files lacking the pattern, one path per line.
|
|
65
|
+
args.push('--files-without-match');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Structured events carrying file + line_number for precise findings.
|
|
69
|
+
args.push('--json');
|
|
70
|
+
}
|
|
71
|
+
// Scope: includes as positive globs, DEFAULT_EXCLUDES + rule excludes as negated globs
|
|
72
|
+
// so ripgrep prunes during traversal (not after) regardless of .gitignore presence.
|
|
73
|
+
for (const glob of include)
|
|
74
|
+
args.push('--glob', glob);
|
|
75
|
+
for (const dir of DEFAULT_EXCLUDES)
|
|
76
|
+
args.push('--glob', `!**/${dir}/**`);
|
|
77
|
+
for (const glob of exclude)
|
|
78
|
+
args.push('--glob', `!${glob}`);
|
|
79
|
+
// `--` guards against a pattern that begins with `-`.
|
|
80
|
+
args.push('--', pattern);
|
|
81
|
+
return args;
|
|
82
|
+
}
|
|
83
|
+
/** Parse `rg --json` match events into one finding per matched line. */
|
|
84
|
+
function forbidFindings(rule, pattern, stdout) {
|
|
85
|
+
const findings = [];
|
|
86
|
+
for (const line of stdout.split('\n')) {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (trimmed.length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
let event;
|
|
91
|
+
try {
|
|
92
|
+
event = JSON.parse(trimmed);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue; // Skip non-JSON noise.
|
|
96
|
+
}
|
|
97
|
+
if (event.type !== 'match')
|
|
98
|
+
continue;
|
|
99
|
+
const file = event.data?.path?.text;
|
|
100
|
+
const lineNumber = event.data?.line_number;
|
|
101
|
+
if (typeof file !== 'string' || typeof lineNumber !== 'number')
|
|
102
|
+
continue;
|
|
103
|
+
findings.push(createFinding(rule, `forbidden pattern found: ${pattern}`, file, {
|
|
104
|
+
line: lineNumber,
|
|
105
|
+
code: 'rg:found',
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
return findings;
|
|
109
|
+
}
|
|
110
|
+
/** Build one finding per file path printed by `rg --files-without-match`. */
|
|
111
|
+
function requireFindings(rule, pattern, stdout) {
|
|
112
|
+
const findings = [];
|
|
113
|
+
for (const line of stdout.split('\n')) {
|
|
114
|
+
const file = line.trim();
|
|
115
|
+
if (file.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
findings.push(createFinding(rule, `required pattern not found: ${pattern}`, file, { code: 'rg:missing' }));
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
}
|
|
121
|
+
/** JS-`RegExp`-only constructs that ripgrep's Rust regex engine does not support. */
|
|
122
|
+
const JS_ONLY_REGEX_FEATURES = [
|
|
123
|
+
{ name: 'lookbehind', test: /\(\?<[=!]/ },
|
|
124
|
+
{ name: 'backreference', test: /\\[1-9]/ },
|
|
125
|
+
{ name: 'named backreference', test: /\\k<[^>]+>/ },
|
|
126
|
+
];
|
|
127
|
+
/**
|
|
128
|
+
* Report whether a regex `pattern` is safe to run under ripgrep's engine.
|
|
129
|
+
*
|
|
130
|
+
* ripgrep's Rust `regex` crate is linear-time and therefore omits features that require
|
|
131
|
+
* backtracking — lookbehind and backreferences. A pattern using them works under the JS
|
|
132
|
+
* `regex` evaluator but fails to compile under `rg`. The `rg-dialect` spur rule and the
|
|
133
|
+
* downstream rule-file converter use this to keep incompatible patterns on the `regex`
|
|
134
|
+
* type instead of silently breaking them on `rg`.
|
|
135
|
+
*
|
|
136
|
+
* @returns `{ compatible: true }` or `{ compatible: false, feature }` naming the first
|
|
137
|
+
* unsupported construct found.
|
|
138
|
+
*/
|
|
139
|
+
export function isRipgrepCompatiblePattern(pattern) {
|
|
140
|
+
for (const { name, test } of JS_ONLY_REGEX_FEATURES) {
|
|
141
|
+
if (test.test(pattern))
|
|
142
|
+
return { compatible: false, feature: name };
|
|
143
|
+
}
|
|
144
|
+
return { compatible: true };
|
|
145
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-artifact-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/schema-artifact-evaluator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema-artifact-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/schema-artifact-evaluator.ts"],"names":[],"mappings":"AACA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;;;;GAYG;AACH,qBAAa,uBAAwB,YAAW,aAAa;IACzD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;;IAMpC,oDAAoD;IAC9C,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAiG5F"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { NodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
1
|
+
import { joinPath, NodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
3
2
|
import { createFinding, } from '../types.js';
|
|
3
|
+
import { stringArray } from './file-utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Evaluates JSON schema artifact files for structural integrity.
|
|
6
6
|
*
|
|
@@ -31,7 +31,7 @@ export class SchemaArtifactEvaluator {
|
|
|
31
31
|
const requiredDefs = stringArray(config.requiredDefs);
|
|
32
32
|
const requireRequiredArray = config.requireRequiredArray === true;
|
|
33
33
|
// Check existence.
|
|
34
|
-
const absolutePath =
|
|
34
|
+
const absolutePath = joinPath(context.workdir, file);
|
|
35
35
|
const exists = await this.fs.exists(absolutePath);
|
|
36
36
|
if (!exists) {
|
|
37
37
|
return {
|
|
@@ -96,7 +96,3 @@ export class SchemaArtifactEvaluator {
|
|
|
96
96
|
return { findings, fixes: [] };
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
/** Return a string array if value is a string array, otherwise undefined. */
|
|
100
|
-
function stringArray(value) {
|
|
101
|
-
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : undefined;
|
|
102
|
-
}
|
|
@@ -7,8 +7,16 @@ import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type
|
|
|
7
7
|
* - `pattern` — ast-grep pattern to search for (required).
|
|
8
8
|
* - `language` — language for ast-grep parsing (default: `typescript`).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Scope is forwarded to `sg` via `--globs` so the subprocess **prunes during traversal**
|
|
11
|
+
* rather than walking everything and filtering after:
|
|
12
|
+
* - the rule's `include` globs become positive `--globs` patterns;
|
|
13
|
+
* - {@link DEFAULT_EXCLUDES} (`node_modules`, `dist`, …) and the rule's `exclude` globs
|
|
14
|
+
* become negated `--globs '!…'` patterns, so heavy generated trees are never descended
|
|
15
|
+
* into even when a rule forgets to exclude them. ast-grep takes the later glob on
|
|
16
|
+
* precedence, so exclusions are appended after includes.
|
|
17
|
+
*
|
|
18
|
+
* The rule's `exclude` is also re-applied in-process as a belt-and-suspenders against any
|
|
19
|
+
* difference between ast-grep's glob semantics and {@link matchesGlob}.
|
|
12
20
|
*/
|
|
13
21
|
export declare class SgEvaluator implements RuleEvaluator {
|
|
14
22
|
private readonly executor;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sg-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/sg-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB
|
|
1
|
+
{"version":3,"file":"sg-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/sg-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,WAAY,YAAW,aAAa;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;gBAE/B,QAAQ,GAAE,eAA2C;IAIjE,wDAAwD;IAClD,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAyD5F"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodeProcessExecutor } from '@gobing-ai/ts-runtime';
|
|
2
2
|
import { createFinding, } from '../types.js';
|
|
3
|
-
import { matchesGlob } from './file-utils.js';
|
|
3
|
+
import { DEFAULT_EXCLUDES, matchesGlob } from './file-utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Evaluates source code against an ast-grep pattern using the `sg` CLI.
|
|
6
6
|
*
|
|
@@ -8,8 +8,16 @@ import { matchesGlob } from './file-utils.js';
|
|
|
8
8
|
* - `pattern` — ast-grep pattern to search for (required).
|
|
9
9
|
* - `language` — language for ast-grep parsing (default: `typescript`).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Scope is forwarded to `sg` via `--globs` so the subprocess **prunes during traversal**
|
|
12
|
+
* rather than walking everything and filtering after:
|
|
13
|
+
* - the rule's `include` globs become positive `--globs` patterns;
|
|
14
|
+
* - {@link DEFAULT_EXCLUDES} (`node_modules`, `dist`, …) and the rule's `exclude` globs
|
|
15
|
+
* become negated `--globs '!…'` patterns, so heavy generated trees are never descended
|
|
16
|
+
* into even when a rule forgets to exclude them. ast-grep takes the later glob on
|
|
17
|
+
* precedence, so exclusions are appended after includes.
|
|
18
|
+
*
|
|
19
|
+
* The rule's `exclude` is also re-applied in-process as a belt-and-suspenders against any
|
|
20
|
+
* difference between ast-grep's glob semantics and {@link matchesGlob}.
|
|
13
21
|
*/
|
|
14
22
|
export class SgEvaluator {
|
|
15
23
|
executor;
|
|
@@ -27,8 +35,17 @@ export class SgEvaluator {
|
|
|
27
35
|
const include = rule.include ?? [];
|
|
28
36
|
const exclude = rule.exclude ?? [];
|
|
29
37
|
const args = ['run', '--pattern', pattern, '--lang', language, '--json'];
|
|
38
|
+
// Positive include globs first.
|
|
30
39
|
for (const glob of include) {
|
|
31
|
-
args.push(
|
|
40
|
+
args.push('--globs', glob);
|
|
41
|
+
}
|
|
42
|
+
// Negated globs prune at traversal time. Default excludes go first so a rule's own
|
|
43
|
+
// exclude (later → higher precedence in ast-grep) can still re-include if intended.
|
|
44
|
+
for (const dir of DEFAULT_EXCLUDES) {
|
|
45
|
+
args.push('--globs', `!**/${dir}/**`);
|
|
46
|
+
}
|
|
47
|
+
for (const glob of exclude) {
|
|
48
|
+
args.push('--globs', `!${glob}`);
|
|
32
49
|
}
|
|
33
50
|
const result = await this.executor.run({
|
|
34
51
|
command: 'sg',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CapabilityRegistry } from '
|
|
2
|
-
import type
|
|
1
|
+
import type { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
|
|
2
|
+
import { type TestPathResolver } from '../resolvers/test-path-resolver';
|
|
3
3
|
import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
|
|
4
4
|
/**
|
|
5
5
|
* Evaluator that enforces where test files live and, optionally, that every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-location-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/test-location-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"test-location-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/test-location-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,KAAK,gBAAgB,EAA8B,MAAM,iCAAiC,CAAC;AACpG,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAWlB;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IAE3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IADvC,kFAAkF;gBACrD,SAAS,CAAC,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,YAAA;IAE7E,0EAA0E;IACpE,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAyDzF;;;;;OAKG;IACH,OAAO,CAAC,cAAc;CASzB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TypeScriptTestPathResolver } from '../resolvers/test-path-resolver.js';
|
|
1
2
|
import { createFinding, } from '../types.js';
|
|
2
3
|
import { discoverFiles, matchesGlob } from './file-utils.js';
|
|
3
4
|
/**
|
|
@@ -88,18 +89,4 @@ export class TestLocationEvaluator {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
/** Built-in TypeScript convention used when no resolver registry is injected. */
|
|
91
|
-
const TYPESCRIPT_FALLBACK =
|
|
92
|
-
name: 'typescript',
|
|
93
|
-
resolveTestPath(srcRelPath) {
|
|
94
|
-
if (srcRelPath.includes('.test.') || srcRelPath.includes('.spec.'))
|
|
95
|
-
return srcRelPath;
|
|
96
|
-
const srcIdx = srcRelPath.indexOf('/src/');
|
|
97
|
-
if (srcIdx !== -1) {
|
|
98
|
-
const pkg = srcRelPath.slice(0, srcIdx);
|
|
99
|
-
const rel = srcRelPath.slice(srcIdx + '/src/'.length).replace(/\.(ts|tsx|js|jsx)$/, '.test.ts');
|
|
100
|
-
return `${pkg}/tests/${rel}`;
|
|
101
|
-
}
|
|
102
|
-
const withoutExt = srcRelPath.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
103
|
-
return `tests/${withoutExt.replace(/^src\//, '')}.test.ts`;
|
|
104
|
-
},
|
|
105
|
-
};
|
|
92
|
+
const TYPESCRIPT_FALLBACK = new TypeScriptTestPathResolver();
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Typed event map for rule-engine run observability. All events prefixed `rule.`. */
|
|
2
|
+
export type RuleEngineEvents = {
|
|
3
|
+
/** Emitted before the first rule is evaluated. */
|
|
4
|
+
'rule.run.start': (data: {
|
|
5
|
+
rules: number;
|
|
6
|
+
total: number;
|
|
7
|
+
}) => void;
|
|
8
|
+
/** Emitted immediately before a single rule's evaluator is invoked. */
|
|
9
|
+
'rule.eval.start': (data: {
|
|
10
|
+
ruleId: string;
|
|
11
|
+
index: number;
|
|
12
|
+
total: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
/** Emitted after a single rule evaluation finishes successfully. */
|
|
15
|
+
'rule.eval.done': (data: {
|
|
16
|
+
ruleId: string;
|
|
17
|
+
findings: number;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}) => void;
|
|
20
|
+
/** Emitted when a rule evaluator throws (in addition to the `kind:'error'` finding). */
|
|
21
|
+
'rule.eval.error': (data: {
|
|
22
|
+
ruleId: string;
|
|
23
|
+
error: string;
|
|
24
|
+
}) => void;
|
|
25
|
+
/** Emitted after the last rule finishes (or was short-circuited). */
|
|
26
|
+
'rule.run.done': (data: {
|
|
27
|
+
rules: number;
|
|
28
|
+
findings: number;
|
|
29
|
+
durationMs: number;
|
|
30
|
+
stoppedEarly: boolean;
|
|
31
|
+
}) => void;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,GAAG;IAC3B,kDAAkD;IAClD,gBAAgB,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnE,uEAAuE;IACvE,iBAAiB,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACpF,oEAAoE;IACpE,gBAAgB,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3F,wFAAwF;IACxF,iBAAiB,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,qEAAqE;IACrE,eAAe,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACnH,CAAC"}
|
package/dist/events.js
ADDED
|
File without changes
|
package/dist/fixers/fixers.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module rule-engine/fixers
|
|
6
6
|
*/
|
|
7
7
|
import { NodeFileSystem, type ProcessExecutor } from '@gobing-ai/ts-runtime';
|
|
8
|
-
import type { CapabilityRegistry } from '
|
|
8
|
+
import type { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
|
|
9
9
|
import type { TestPathResolver } from '../resolvers/test-path-resolver';
|
|
10
10
|
import type { ConstraintFinding, ConstraintRule, Fix, FixMode, RuleContext } from '../types';
|
|
11
11
|
/** Numeric ordering for fix authority; higher means more write authority. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixers.d.ts","sourceRoot":"","sources":["../../src/fixers/fixers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"fixers.d.ts","sourceRoot":"","sources":["../../src/fixers/fixers.ts"],"names":[],"mappings":"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"}
|
package/dist/fixers/fixers.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module rule-engine/fixers
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { NodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
7
|
+
import { isAbsolutePath, joinPath, NodeFileSystem, relativePath, resolvePath, } from '@gobing-ai/ts-runtime';
|
|
9
8
|
import { TestStubFixer } from './test-stub-fixer.js';
|
|
10
9
|
/** Numeric ordering for fix authority; higher means more write authority. */
|
|
11
10
|
export const FIX_MODE_RANK = {
|
|
@@ -30,7 +29,7 @@ export function builtInFixers(host, exec) {
|
|
|
30
29
|
}
|
|
31
30
|
/** Resolve a workdir-relative or absolute path to an absolute path. */
|
|
32
31
|
export function resolveWorkdirPath(workdir, filePath) {
|
|
33
|
-
return
|
|
32
|
+
return isAbsolutePath(filePath) ? filePath : joinPath(workdir, filePath);
|
|
34
33
|
}
|
|
35
34
|
/** Apply byte-range fixes to files, optionally returning a dry-run diff only. */
|
|
36
35
|
export async function applyFixes(workdir, fixes, dryRun = false, fs = new NodeFileSystem()) {
|
|
@@ -111,8 +110,8 @@ function selectNonOverlappingFixes(fixes) {
|
|
|
111
110
|
}
|
|
112
111
|
/** Return true when absPath is at or below workdir. */
|
|
113
112
|
function isInsideWorkdir(workdir, absPath) {
|
|
114
|
-
const rel =
|
|
115
|
-
return rel === '' || (!rel.startsWith('..') && !
|
|
113
|
+
const rel = relativePath(resolvePath(workdir), resolvePath(absPath));
|
|
114
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolutePath(rel));
|
|
116
115
|
}
|
|
117
116
|
/** Return true when [start, end] is a valid byte range for a string of contentLength bytes. */
|
|
118
117
|
function isValidRange(start, end, contentLength) {
|