vouchington-tooling 0.13.2 → 0.13.4
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 +5 -0
- package/ast-grep/rules/no-three-sequential-awaits-tsx.yml +178 -0
- package/ast-grep/rules/no-three-sequential-awaits.yml +169 -0
- package/dist/ast-grep-examples/companion-parity.d.mts +19 -0
- package/dist/ast-grep-examples/companion-parity.mjs +156 -0
- package/dist/ast-grep-examples/index.d.mts +2 -1
- package/dist/ast-grep-examples/index.mjs +1 -0
- package/dist/coverage-transport/outcome.d.mts +6 -5
- package/dist/coverage-transport/outcome.mjs +6 -8
- package/dist/gha-check-run/cli.d.mts +2 -0
- package/dist/gha-check-run/cli.mjs +81 -0
- package/dist/gha-check-run/github.d.mts +30 -0
- package/dist/gha-check-run/github.mjs +63 -0
- package/dist/gha-check-run/index.d.mts +2 -0
- package/dist/gha-check-run/index.mjs +2 -0
- package/dist/gha-post-review/cli.mjs +1 -1
- package/dist/gha-post-review/github.d.mts +6 -22
- package/dist/gha-post-review/github.mjs +8 -92
- package/dist/gha-post-review/post.d.mts +7 -1
- package/dist/gha-post-review/post.mjs +11 -3
- package/dist/gha-post-review/pull-io.d.mts +13 -0
- package/dist/gha-post-review/pull-io.mjs +88 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +1 -1
- package/dist/scc-complexity/baseline.mjs +5 -2
- package/dist/scc-complexity/constants.d.mts +1 -0
- package/dist/scc-complexity/constants.mjs +1 -0
- package/dist/scc-complexity/index.d.mts +1 -1
- package/dist/scc-complexity/index.mjs +24 -36
- package/dist/scc-complexity/paths.d.mts +2 -1
- package/dist/scc-complexity/paths.mjs +35 -2
- package/dist/scc-complexity/scope-config.d.mts +3 -0
- package/dist/scc-complexity/scope-config.mjs +35 -0
- package/dist/scc-complexity/workflow-command.d.mts +2 -0
- package/dist/scc-complexity/workflow-command.mjs +6 -0
- package/package.json +6 -1
|
@@ -4,11 +4,14 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { baselineEntryByScopeAndFile, normalizeSccComplexityBaseline, validateSccComplexityBaseline, } from './baseline.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { SCC_COMPLEXITY_LIMIT } from './constants.mjs';
|
|
8
|
+
import { canonicalRepoPath } from './paths.mjs';
|
|
8
9
|
import { parseSccComplexityValues } from './parser.mjs';
|
|
10
|
+
import { normalizeScopes, scopesFor } from './scope-config.mjs';
|
|
11
|
+
import { escapeWorkflowCommandMessage, escapeWorkflowCommandProperty } from './workflow-command.mjs';
|
|
9
12
|
const execFileAsync = promisify(execFile);
|
|
10
13
|
export { parseSccComplexityBaseline, SCC_COMPLEXITY_BASELINE_VERSION } from './baseline.mjs';
|
|
11
|
-
export
|
|
14
|
+
export { SCC_COMPLEXITY_LIMIT } from './constants.mjs';
|
|
12
15
|
const DEFAULT_INCLUDE_EXT = 'js,mts,jsx,ts,tsx';
|
|
13
16
|
const DEFAULT_EXCLUDE_DIR = '.git,fixtures,__tests__,test-helpers';
|
|
14
17
|
const DEFAULT_NOT_MATCH = String.raw `\.(test|spec)\.`;
|
|
@@ -31,10 +34,14 @@ export function buildSccArgs(options = {}) {
|
|
|
31
34
|
}
|
|
32
35
|
export async function checkSccComplexity(ctx, options = {}, runScc) {
|
|
33
36
|
if (!ctx.isInsideGitRepo)
|
|
34
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
errors: [
|
|
39
|
+
`::error::${escapeWorkflowCommandMessage(ctx.repoRoot)} is not inside a git repository`,
|
|
40
|
+
],
|
|
41
|
+
};
|
|
35
42
|
const dir = await mkdtemp(join(tmpdir(), options.tmpdirPrefix ?? DEFAULT_TMPDIR_PREFIX));
|
|
36
43
|
try {
|
|
37
|
-
const scopes = normalizeScopes(scopesFor(options), ctx.repoRoot);
|
|
44
|
+
const scopes = await normalizeScopes(scopesFor(options), ctx.repoRoot, options.limit);
|
|
38
45
|
const trackedFileSet = new Set([...ctx.trackedFileSet].map((file) => canonicalRepoPath(ctx.repoRoot, file)));
|
|
39
46
|
const baseline = options.baseline && normalizeSccComplexityBaseline(options.baseline, ctx.repoRoot);
|
|
40
47
|
const results = await runScopes(ctx, options, scopes, dir, trackedFileSet, runScc);
|
|
@@ -44,7 +51,7 @@ export async function checkSccComplexity(ctx, options = {}, runScc) {
|
|
|
44
51
|
}
|
|
45
52
|
catch (error) {
|
|
46
53
|
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
-
return { errors: [`::error::scc-complexity failed: ${message}`] };
|
|
54
|
+
return { errors: [`::error::scc-complexity failed: ${escapeWorkflowCommandMessage(message)}`] };
|
|
48
55
|
}
|
|
49
56
|
finally {
|
|
50
57
|
await rm(dir, { force: true, recursive: true });
|
|
@@ -55,41 +62,16 @@ export function parseSccComplexityViolations(json, trackedFileSet, limit = SCC_C
|
|
|
55
62
|
.filter((value) => value.complexity > limit)
|
|
56
63
|
.toSorted(compare);
|
|
57
64
|
}
|
|
58
|
-
function scopesFor(options) {
|
|
59
|
-
if (!options.scopes) {
|
|
60
|
-
if (options.baseline)
|
|
61
|
-
throw new Error('baseline requires named scopes');
|
|
62
|
-
return [{ includePaths: [], name: '', ...options }];
|
|
63
|
-
}
|
|
64
|
-
if (options.scopes.length === 0)
|
|
65
|
-
throw new Error('scopes must not be empty');
|
|
66
|
-
const names = new Set();
|
|
67
|
-
for (const scope of options.scopes) {
|
|
68
|
-
if (!scope.name || names.has(scope.name))
|
|
69
|
-
throw new Error(`scope name ${scope.name || '(empty)'} is invalid`);
|
|
70
|
-
if (scope.includePaths.length === 0 || scope.includePaths.some((path) => !path))
|
|
71
|
-
throw new Error(`scope ${scope.name} must include at least one path`);
|
|
72
|
-
names.add(scope.name);
|
|
73
|
-
}
|
|
74
|
-
return options.scopes;
|
|
75
|
-
}
|
|
76
|
-
function normalizeScopes(scopes, repoRoot) {
|
|
77
|
-
return scopes.map((scope) => {
|
|
78
|
-
const includePaths = scope.includePaths.map((path) => canonicalRepoPath(repoRoot, path));
|
|
79
|
-
if (includePaths.some((path) => !isInsideRepo(path)))
|
|
80
|
-
throw new Error(`scope ${scope.name} includes a path outside the repository`);
|
|
81
|
-
return { ...scope, includePaths };
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
65
|
async function runScopes(ctx, options, scopes, dir, trackedFileSet, runScc) {
|
|
85
66
|
const results = [];
|
|
86
67
|
for (const [index, scope] of scopes.entries()) {
|
|
87
68
|
const outputPath = join(dir, `scope-${index}.json`);
|
|
88
69
|
const resolve = runScc ?? ((path) => runSccJson(ctx.repoRoot, path, options, scope));
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
70
|
+
const values = parseSccComplexityValues(await resolve(outputPath, scope), trackedFileSet, ctx.repoRoot);
|
|
71
|
+
const outside = values.find((value) => !isInScopeResult(scope, value.file));
|
|
72
|
+
if (outside)
|
|
73
|
+
throw new Error(`scope ${scope.name} received result outside its paths: ${outside.file}`);
|
|
74
|
+
results.push({ scope, values });
|
|
93
75
|
}
|
|
94
76
|
return results;
|
|
95
77
|
}
|
|
@@ -106,9 +88,15 @@ function formatViolations(results, baseline) {
|
|
|
106
88
|
const detail = entry
|
|
107
89
|
? `scc complexity ${value.complexity} exceeds baseline ceiling ${entry.complexity}`
|
|
108
90
|
: `scc complexity ${value.complexity} exceeds ${limit}; simplify or split this file`;
|
|
109
|
-
return [
|
|
91
|
+
return [
|
|
92
|
+
`::error file=${escapeWorkflowCommandProperty(value.file)}::${escapeWorkflowCommandMessage(`${prefix}${value.file}: ${detail}`)}`,
|
|
93
|
+
];
|
|
110
94
|
}));
|
|
111
95
|
}
|
|
96
|
+
function isInScopeResult(scope, file) {
|
|
97
|
+
return (scope.includePaths.length === 0 ||
|
|
98
|
+
scope.includePaths.some((path) => file === path || file.startsWith(`${path}/`) || path === '.'));
|
|
99
|
+
}
|
|
112
100
|
async function runSccJson(repoRoot, outputPath, options, scope) {
|
|
113
101
|
try {
|
|
114
102
|
await execFileAsync(options.command ?? 'scc', [...buildSccArgs({ ...options, ...scope }), ...scope.includePaths, '--output', outputPath], { cwd: repoRoot, maxBuffer: 1024 * 1024 });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function canonicalRepoPath(repoRoot: string, path: string): string;
|
|
2
|
-
export declare function isInsideRepo(path: string): boolean;
|
|
3
2
|
export declare function isInScope(file: string, includePaths: readonly string[]): boolean;
|
|
3
|
+
export declare function canonicalScopePath(repoRoot: string, path: string): Promise<string>;
|
|
4
|
+
export declare function assertWorkflowCommandData(value: string, label: string): void;
|
|
@@ -1,11 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, win32 } from 'node:path';
|
|
2
3
|
export function canonicalRepoPath(repoRoot, path) {
|
|
3
4
|
const relativePath = relative(repoRoot, resolve(repoRoot, path.replaceAll('\\', '/'))).replaceAll('\\', '/');
|
|
4
5
|
return relativePath || '.';
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
+
function isInsideRepo(path) {
|
|
7
8
|
return path !== '..' && !path.startsWith('../');
|
|
8
9
|
}
|
|
9
10
|
export function isInScope(file, includePaths) {
|
|
10
11
|
return includePaths.some((includePath) => includePath === '.' || file === includePath || file.startsWith(`${includePath}/`));
|
|
11
12
|
}
|
|
13
|
+
export async function canonicalScopePath(repoRoot, path) {
|
|
14
|
+
if (isAbsolute(path) || win32.parse(path).root)
|
|
15
|
+
throw new Error('path is outside the repository');
|
|
16
|
+
const physicalRoot = await realpath(repoRoot);
|
|
17
|
+
const candidatePath = resolve(repoRoot, path.replaceAll('\\', '/'));
|
|
18
|
+
if (!isInsideRepo(canonicalRepoPath(repoRoot, candidatePath)))
|
|
19
|
+
throw new Error('path is outside the repository');
|
|
20
|
+
const physicalPath = await physicalPathFor(candidatePath, repoRoot, physicalRoot);
|
|
21
|
+
const canonicalPath = canonicalRepoPath(physicalRoot, physicalPath);
|
|
22
|
+
if (!isInsideRepo(canonicalPath))
|
|
23
|
+
throw new Error('path is outside the repository');
|
|
24
|
+
return canonicalPath;
|
|
25
|
+
}
|
|
26
|
+
export function assertWorkflowCommandData(value, label) {
|
|
27
|
+
if (/[\r\n]/.test(value))
|
|
28
|
+
throw new Error(`${label} contains a workflow command control character`);
|
|
29
|
+
}
|
|
30
|
+
async function physicalPathFor(path, repoRoot, physicalRoot) {
|
|
31
|
+
let existingPath = path;
|
|
32
|
+
while (existingPath !== repoRoot) {
|
|
33
|
+
try {
|
|
34
|
+
const physicalPath = await realpath(existingPath);
|
|
35
|
+
return resolve(physicalPath, relative(existingPath, path));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if ((await lstat(existingPath).catch(() => undefined))?.isSymbolicLink())
|
|
39
|
+
throw error;
|
|
40
|
+
existingPath = dirname(existingPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return resolve(physicalRoot, relative(repoRoot, path));
|
|
44
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SccComplexityOptions, SccComplexityScope } from './types.mts';
|
|
2
|
+
export declare function scopesFor(options: SccComplexityOptions): readonly SccComplexityScope[];
|
|
3
|
+
export declare function normalizeScopes(scopes: readonly SccComplexityScope[], repoRoot: string, topLevelLimit: number | undefined): Promise<readonly SccComplexityScope[]>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { assertWorkflowCommandData, canonicalScopePath } from './paths.mjs';
|
|
2
|
+
export function scopesFor(options) {
|
|
3
|
+
if (!options.scopes) {
|
|
4
|
+
if (options.baseline)
|
|
5
|
+
throw new Error('baseline requires named scopes');
|
|
6
|
+
return [{ includePaths: [], name: '', ...options }];
|
|
7
|
+
}
|
|
8
|
+
if (options.scopes.length === 0)
|
|
9
|
+
throw new Error('scopes must not be empty');
|
|
10
|
+
const names = new Set();
|
|
11
|
+
for (const scope of options.scopes) {
|
|
12
|
+
if (!scope.name || names.has(scope.name))
|
|
13
|
+
throw new Error(`scope name ${scope.name || '(empty)'} is invalid`);
|
|
14
|
+
assertWorkflowCommandData(scope.name, 'scope name');
|
|
15
|
+
if (scope.includePaths.length === 0 || scope.includePaths.some((path) => !path))
|
|
16
|
+
throw new Error(`scope ${scope.name} must include at least one path`);
|
|
17
|
+
for (const path of scope.includePaths)
|
|
18
|
+
assertWorkflowCommandData(path, `scope ${scope.name} path`);
|
|
19
|
+
names.add(scope.name);
|
|
20
|
+
}
|
|
21
|
+
return options.scopes;
|
|
22
|
+
}
|
|
23
|
+
export async function normalizeScopes(scopes, repoRoot, topLevelLimit) {
|
|
24
|
+
return Promise.all(scopes.map(async (scope) => {
|
|
25
|
+
let includePaths;
|
|
26
|
+
try {
|
|
27
|
+
includePaths = await Promise.all(scope.includePaths.map((path) => canonicalScopePath(repoRoot, path)));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error(`scope ${scope.name} includes a path outside the repository`);
|
|
31
|
+
}
|
|
32
|
+
const limit = scope.limit ?? topLevelLimit;
|
|
33
|
+
return limit === undefined ? { ...scope, includePaths } : { ...scope, includePaths, limit };
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function escapeWorkflowCommandMessage(value) {
|
|
2
|
+
return value.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A');
|
|
3
|
+
}
|
|
4
|
+
export function escapeWorkflowCommandProperty(value) {
|
|
5
|
+
return escapeWorkflowCommandMessage(value).replaceAll(':', '%3A').replaceAll(',', '%2C');
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -208,6 +208,11 @@
|
|
|
208
208
|
"import": "./dist/gha-claude-post-review/index.mjs",
|
|
209
209
|
"default": "./dist/gha-claude-post-review/index.mjs"
|
|
210
210
|
},
|
|
211
|
+
"./gha-check-run": {
|
|
212
|
+
"types": "./dist/gha-check-run/index.d.mts",
|
|
213
|
+
"import": "./dist/gha-check-run/index.mjs",
|
|
214
|
+
"default": "./dist/gha-check-run/index.mjs"
|
|
215
|
+
},
|
|
211
216
|
"./http-link-pagination": {
|
|
212
217
|
"types": "./dist/http-link-pagination/index.d.mts",
|
|
213
218
|
"import": "./dist/http-link-pagination/index.mjs",
|