@codefast/cli 0.12.0 → 0.13.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/CHANGELOG.md +36 -0
- package/README.md +97 -12
- package/dist/arrange/domain/ast/translator.js +42 -28
- package/dist/arrange/simplify/process-file.d.ts +1 -1
- package/dist/audit/assertions/cli-result.d.ts +13 -0
- package/dist/audit/assertions/cli-result.js +22 -0
- package/dist/audit/assertions/cli-schema.d.ts +18 -0
- package/dist/audit/assertions/cli-schema.js +12 -0
- package/dist/audit/assertions/domain/double-assertion.d.ts +20 -0
- package/dist/audit/assertions/domain/double-assertion.js +122 -0
- package/dist/audit/assertions/output.d.ts +7 -0
- package/dist/audit/assertions/output.js +21 -0
- package/dist/audit/assertions/prepare.d.ts +16 -0
- package/dist/audit/assertions/prepare.js +12 -0
- package/dist/audit/assertions/run.d.ts +14 -0
- package/dist/audit/assertions/run.js +40 -0
- package/dist/audit/command.js +45 -1
- package/dist/audit/constants/cli-result.d.ts +13 -0
- package/dist/audit/constants/cli-result.js +22 -0
- package/dist/audit/constants/cli-schema.d.ts +18 -0
- package/dist/audit/constants/cli-schema.js +12 -0
- package/dist/audit/constants/domain/constants.d.ts +8 -0
- package/dist/audit/constants/domain/constants.js +66 -0
- package/dist/audit/constants/output.d.ts +7 -0
- package/dist/audit/constants/output.js +21 -0
- package/dist/audit/constants/prepare.d.ts +16 -0
- package/dist/audit/constants/prepare.js +37 -0
- package/dist/audit/constants/run.d.ts +14 -0
- package/dist/audit/constants/run.js +64 -0
- package/dist/audit/display-names/domain/display-names.js +1 -9
- package/dist/audit/domain/types.d.ts +85 -0
- package/dist/audit/imports/domain/import-policy.js +4 -18
- package/dist/audit/publish/cli-result.d.ts +1 -1
- package/dist/audit/publish/cli-result.js +6 -3
- package/dist/audit/publish/domain/stylesheet-sources.d.ts +22 -0
- package/dist/audit/publish/domain/stylesheet-sources.js +64 -0
- package/dist/audit/publish/output.js +12 -3
- package/dist/audit/publish/run.d.ts +2 -1
- package/dist/audit/publish/run.js +30 -1
- package/dist/audit/publish/shipped-files.d.ts +21 -0
- package/dist/audit/publish/shipped-files.js +36 -0
- package/dist/core/config/schema.d.ts +5 -0
- package/dist/core/config/schema.js +2 -0
- package/dist/core/filesystem/filesystem.d.ts +3 -4
- package/dist/core/filesystem/node.js +1 -7
- package/dist/core/oxc-node.d.ts +32 -0
- package/dist/core/oxc-node.js +25 -0
- package/dist/core/source-position.d.ts +15 -0
- package/dist/core/source-position.js +26 -0
- package/dist/mirror/dist-filesystem-node.js +4 -14
- package/dist/pack-slim/run.js +1 -1
- package/dist/tag/domain/version-summary.d.ts +5 -2
- package/dist/tag/writer/since-writer.js +2 -4
- package/package.json +4 -4
- package/dist/mirror/domain/dirent-guard.d.ts +0 -10
- package/dist/mirror/domain/dirent-guard.js +0 -15
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuditCommandPrelude } from "#audit/prepare";
|
|
2
|
+
import type { AppError } from "#core/errors";
|
|
3
|
+
import type { Filesystem } from "#core/filesystem/filesystem";
|
|
4
|
+
import type { Result } from "#core/result";
|
|
5
|
+
/**
|
|
6
|
+
* Loads config and resolves the scan target for `audit assertions`.
|
|
7
|
+
*
|
|
8
|
+
* @remarks Defaults to the repo root, tests included: a double assertion in a test silences the
|
|
9
|
+
* compiler on the code the test is meant to hold to its types.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.13.0
|
|
12
|
+
*/
|
|
13
|
+
export declare function prepareAssertionAudit(fs: Filesystem, args: {
|
|
14
|
+
readonly currentWorkingDirectory: string;
|
|
15
|
+
readonly rawTarget: string | undefined;
|
|
16
|
+
}): Promise<Result<AuditCommandPrelude, AppError>>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { prepareRepoRootAudit } from "#audit/prepare";
|
|
2
|
+
/**
|
|
3
|
+
* Loads config and resolves the scan target for `audit assertions`.
|
|
4
|
+
*
|
|
5
|
+
* @remarks Defaults to the repo root, tests included: a double assertion in a test silences the
|
|
6
|
+
* compiler on the code the test is meant to hold to its types.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.13.0
|
|
9
|
+
*/
|
|
10
|
+
export async function prepareAssertionAudit(fs, args) {
|
|
11
|
+
return prepareRepoRootAudit(fs, args, (config) => config.audit?.assertions?.allowlist ?? []);
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AssertionAuditResult } from "#audit/domain/types";
|
|
2
|
+
import { AppError } from "#core/errors";
|
|
3
|
+
import type { Filesystem } from "#core/filesystem/filesystem";
|
|
4
|
+
import type { Result } from "#core/result";
|
|
5
|
+
/**
|
|
6
|
+
* Scans a target path for double assertions through `unknown` or `any`, tests included.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.13.0
|
|
9
|
+
*/
|
|
10
|
+
export declare function runAssertionAudit(fs: Filesystem, args: {
|
|
11
|
+
readonly rootDir: string;
|
|
12
|
+
readonly targetPath: string;
|
|
13
|
+
readonly allowlist: ReadonlyArray<string>;
|
|
14
|
+
}): Result<AssertionAuditResult, AppError>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { auditDoubleAssertionSource } from "#audit/assertions/domain/double-assertion";
|
|
3
|
+
import { AppError, messageFrom } from "#core/errors";
|
|
4
|
+
import { err, ok } from "#core/result";
|
|
5
|
+
import { walkTsxFiles } from "#core/workspace/typescript-walk";
|
|
6
|
+
/**
|
|
7
|
+
* Scans a target path for double assertions through `unknown` or `any`, tests included.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.13.0
|
|
10
|
+
*/
|
|
11
|
+
export function runAssertionAudit(fs, args) {
|
|
12
|
+
try {
|
|
13
|
+
const allowlist = new Set(args.allowlist);
|
|
14
|
+
const { rootDir, targetPath } = args;
|
|
15
|
+
const filesToScan = fs.statSync(targetPath).isFile() ? [targetPath] : walkTsxFiles(targetPath, fs);
|
|
16
|
+
const files = [];
|
|
17
|
+
let violationCount = 0;
|
|
18
|
+
let allowlistedCount = 0;
|
|
19
|
+
for (const absolutePath of filesToScan) {
|
|
20
|
+
const relativePath = path.relative(rootDir, absolutePath).split(path.sep).join("/");
|
|
21
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
22
|
+
const remaining = auditDoubleAssertionSource(absolutePath, content).filter(({ raw }) => {
|
|
23
|
+
const isAllowed = allowlist.has(raw) || allowlist.has(`${relativePath}:${raw}`);
|
|
24
|
+
if (isAllowed) {
|
|
25
|
+
allowlistedCount++;
|
|
26
|
+
}
|
|
27
|
+
return !isAllowed;
|
|
28
|
+
});
|
|
29
|
+
if (remaining.length === 0) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
violationCount += remaining.length;
|
|
33
|
+
files.push({ relativePath, violations: remaining });
|
|
34
|
+
}
|
|
35
|
+
return ok({ files, violationCount, allowlistedCount, scannedFileCount: filesToScan.length });
|
|
36
|
+
}
|
|
37
|
+
catch (caughtError) {
|
|
38
|
+
return err(new AppError("INFRA_FAILURE", messageFrom(caughtError), caughtError));
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/audit/command.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { exitCodeForAssertionAuditResult, formatAssertionAuditJsonOutput } from "#audit/assertions/cli-result";
|
|
3
|
+
import { assertionAuditRunRequestSchema } from "#audit/assertions/cli-schema";
|
|
4
|
+
import { presentAssertionAuditResult } from "#audit/assertions/output";
|
|
5
|
+
import { prepareAssertionAudit } from "#audit/assertions/prepare";
|
|
6
|
+
import { runAssertionAudit } from "#audit/assertions/run";
|
|
2
7
|
import { exitCodeForCommentAuditResult, formatCommentAuditJsonOutput } from "#audit/comments/cli-result";
|
|
3
8
|
import { commentAuditRunRequestSchema } from "#audit/comments/cli-schema";
|
|
4
9
|
import { presentCommentAuditResult } from "#audit/comments/output";
|
|
5
10
|
import { prepareCommentAudit } from "#audit/comments/prepare";
|
|
6
11
|
import { runCommentAudit } from "#audit/comments/run";
|
|
12
|
+
import { exitCodeForConstantAuditResult, formatConstantAuditJsonOutput } from "#audit/constants/cli-result";
|
|
13
|
+
import { constantAuditRunRequestSchema } from "#audit/constants/cli-schema";
|
|
14
|
+
import { presentConstantAuditResult } from "#audit/constants/output";
|
|
15
|
+
import { prepareConstantAudit } from "#audit/constants/prepare";
|
|
16
|
+
import { runConstantAudit } from "#audit/constants/run";
|
|
7
17
|
import { exitCodeForDisplayNameAuditResult, formatDisplayNameAuditJsonOutput } from "#audit/display-names/cli-result";
|
|
8
18
|
import { displayNameAuditRunRequestSchema } from "#audit/display-names/cli-schema";
|
|
9
19
|
import { presentDisplayNameAuditResult } from "#audit/display-names/output";
|
|
@@ -79,6 +89,22 @@ const importsCheck = {
|
|
|
79
89
|
formatJson: formatImportsAuditJsonOutput,
|
|
80
90
|
exitCode: exitCodeForImportsAuditResult,
|
|
81
91
|
};
|
|
92
|
+
const assertionsCheck = {
|
|
93
|
+
name: "assertions",
|
|
94
|
+
description: "Report double type assertions through unknown or any (x as unknown as T), tests included",
|
|
95
|
+
targetHelp: "Directory or file to scan (default: the repo root)",
|
|
96
|
+
schema: assertionAuditRunRequestSchema,
|
|
97
|
+
prepare: prepareAssertionAudit,
|
|
98
|
+
buildRequest: baseAuditRequest,
|
|
99
|
+
run: (fs, request) => runAssertionAudit(fs, {
|
|
100
|
+
rootDir: request.rootDir,
|
|
101
|
+
targetPath: request.targetPath,
|
|
102
|
+
allowlist: request.allowlist ?? [],
|
|
103
|
+
}),
|
|
104
|
+
present: presentAssertionAuditResult,
|
|
105
|
+
formatJson: formatAssertionAuditJsonOutput,
|
|
106
|
+
exitCode: exitCodeForAssertionAuditResult,
|
|
107
|
+
};
|
|
82
108
|
const displayNamesCheck = {
|
|
83
109
|
name: "display-names",
|
|
84
110
|
description: "Report token(), tag() and module display names that break the <namespace>:<Name> convention",
|
|
@@ -95,6 +121,22 @@ const displayNamesCheck = {
|
|
|
95
121
|
formatJson: formatDisplayNameAuditJsonOutput,
|
|
96
122
|
exitCode: exitCodeForDisplayNameAuditResult,
|
|
97
123
|
};
|
|
124
|
+
const constantsCheck = {
|
|
125
|
+
name: "constants",
|
|
126
|
+
description: "Report numeric constants whose comment names none of the three kinds a number may be",
|
|
127
|
+
targetHelp: "Directory or file to scan (default: audit.constants.target from config)",
|
|
128
|
+
schema: constantAuditRunRequestSchema,
|
|
129
|
+
prepare: prepareConstantAudit,
|
|
130
|
+
buildRequest: baseAuditRequest,
|
|
131
|
+
run: (fs, request) => runConstantAudit(fs, {
|
|
132
|
+
rootDir: request.rootDir,
|
|
133
|
+
targetPath: request.targetPath,
|
|
134
|
+
allowlist: request.allowlist ?? [],
|
|
135
|
+
}),
|
|
136
|
+
present: presentConstantAuditResult,
|
|
137
|
+
formatJson: formatConstantAuditJsonOutput,
|
|
138
|
+
exitCode: exitCodeForConstantAuditResult,
|
|
139
|
+
};
|
|
98
140
|
const commentsCheck = {
|
|
99
141
|
name: "comments",
|
|
100
142
|
description: "Report section dividers that are not in the repo's one allowed form",
|
|
@@ -117,7 +159,7 @@ const commentsCheck = {
|
|
|
117
159
|
};
|
|
118
160
|
const publishCheck = {
|
|
119
161
|
name: "publish",
|
|
120
|
-
description: "Report what breaks a consumer's install: #/ imports
|
|
162
|
+
description: "Report what breaks a consumer's install: #/ imports, unshipped exports/imports targets, and stylesheet @source paths reaching nothing shipped",
|
|
121
163
|
targetHelp: "Directory or file to scan (default: the repo root)",
|
|
122
164
|
schema: publishAuditRunRequestSchema,
|
|
123
165
|
prepare: preparePublishAudit,
|
|
@@ -156,7 +198,9 @@ export function createAuditCommand() {
|
|
|
156
198
|
registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(rtlCheck));
|
|
157
199
|
registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(linksCheck));
|
|
158
200
|
registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(importsCheck));
|
|
201
|
+
registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(assertionsCheck));
|
|
159
202
|
registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(displayNamesCheck));
|
|
203
|
+
registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(constantsCheck));
|
|
160
204
|
registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(commentsCheck));
|
|
161
205
|
registerPipelineSubcommand(cmd, nodeFilesystem, auditCheckToPipeline(publishCheck));
|
|
162
206
|
return cmd;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConstantAuditResult } from "#audit/domain/types";
|
|
2
|
+
/**
|
|
3
|
+
* Exit `1` when any non-allowlisted numeric constant is unlabelled.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.11.0
|
|
6
|
+
*/
|
|
7
|
+
export declare function exitCodeForConstantAuditResult(result: ConstantAuditResult): number;
|
|
8
|
+
/**
|
|
9
|
+
* Machine-readable numeric-constant summary for `--json`.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.11.0
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatConstantAuditJsonOutput(result: ConstantAuditResult, rootDir: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CLI_EXIT_GENERAL_ERROR, CLI_EXIT_SUCCESS } from "#core/exit-codes";
|
|
2
|
+
/**
|
|
3
|
+
* Exit `1` when any non-allowlisted numeric constant is unlabelled.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.11.0
|
|
6
|
+
*/
|
|
7
|
+
export function exitCodeForConstantAuditResult(result) {
|
|
8
|
+
return result.violationCount > 0 ? CLI_EXIT_GENERAL_ERROR : CLI_EXIT_SUCCESS;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Machine-readable numeric-constant summary for `--json`.
|
|
12
|
+
*
|
|
13
|
+
* @since 0.11.0
|
|
14
|
+
*/
|
|
15
|
+
export function formatConstantAuditJsonOutput(result, rootDir) {
|
|
16
|
+
return JSON.stringify({
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
ok: result.violationCount === 0,
|
|
19
|
+
cwd: rootDir,
|
|
20
|
+
result,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Resolved request for a single numeric-constant audit run.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.11.0
|
|
6
|
+
*/
|
|
7
|
+
export type ConstantAuditRunRequest = {
|
|
8
|
+
readonly rootDir: string;
|
|
9
|
+
readonly targetPath: string;
|
|
10
|
+
readonly allowlist?: ReadonlyArray<string> | undefined;
|
|
11
|
+
readonly json: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Zod schema for {@link ConstantAuditRunRequest}.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.11.0
|
|
17
|
+
*/
|
|
18
|
+
export declare const constantAuditRunRequestSchema: z.ZodType<ConstantAuditRunRequest>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for {@link ConstantAuditRunRequest}.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.11.0
|
|
6
|
+
*/
|
|
7
|
+
export const constantAuditRunRequestSchema = z.object({
|
|
8
|
+
rootDir: z.string().min(1),
|
|
9
|
+
targetPath: z.string().min(1),
|
|
10
|
+
allowlist: z.array(z.string()).optional(),
|
|
11
|
+
json: z.boolean(),
|
|
12
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The numeric-constant convention: a `const NAME = <number>` in a source tree names the kind of number it is. */
|
|
2
|
+
import type { ConstantViolation } from "#audit/domain/types";
|
|
3
|
+
/**
|
|
4
|
+
* Scans one TypeScript source for numeric constants whose comment names none of the three kinds.
|
|
5
|
+
*
|
|
6
|
+
* @since 0.11.0
|
|
7
|
+
*/
|
|
8
|
+
export declare function auditNumericConstants(sourceText: string): Array<ConstantViolation>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three kinds a numeric constant may be, each named by one phrase in the comment above it.
|
|
3
|
+
*
|
|
4
|
+
* @remarks A width of the machine, a value the contract fixes, or a figure derived from the data a
|
|
5
|
+
* bind hands in. A count that merely looks reasonable is none of them, and the audit says so.
|
|
6
|
+
*/
|
|
7
|
+
const KIND_PHRASES = [
|
|
8
|
+
"a constant of the machine",
|
|
9
|
+
"a value the contract fixes",
|
|
10
|
+
"derived from bind-time data",
|
|
11
|
+
];
|
|
12
|
+
/** An upper-case `const` bound to a numeric literal, with an optional type annotation. */
|
|
13
|
+
const NUMERIC_CONST = /^\s*(?:export\s+)?const\s+([A-Z][A-Z0-9_]*)\s*(?::[^=]+)?=\s*(-?\d[\d_]*(?:\.\d+)?)(?![\w.])/;
|
|
14
|
+
/** Values that stand for absence or identity, never for a tuned size: any distinct value would do. */
|
|
15
|
+
const SENTINEL_VALUES = new Set(["0", "1", "-1"]);
|
|
16
|
+
/**
|
|
17
|
+
* Scans one TypeScript source for numeric constants whose comment names none of the three kinds.
|
|
18
|
+
*
|
|
19
|
+
* @since 0.11.0
|
|
20
|
+
*/
|
|
21
|
+
export function auditNumericConstants(sourceText) {
|
|
22
|
+
const lines = sourceText.split("\n");
|
|
23
|
+
const violations = [];
|
|
24
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
25
|
+
const match = NUMERIC_CONST.exec(lines[index]);
|
|
26
|
+
if (match === null || SENTINEL_VALUES.has(match[2])) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const comment = commentAbove(lines, index);
|
|
30
|
+
if (KIND_PHRASES.some((phrase) => comment.includes(phrase))) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
violations.push({
|
|
34
|
+
line: index + 1,
|
|
35
|
+
raw: `${match[1]} = ${match[2]}`,
|
|
36
|
+
reason: `no kind named above it — say which it is: ${KIND_PHRASES.join(" · ")}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return violations;
|
|
40
|
+
}
|
|
41
|
+
/** The doc block or run of `//` lines directly above a line, as one text; empty when there is none. */
|
|
42
|
+
function commentAbove(lines, index) {
|
|
43
|
+
let cursor = index - 1;
|
|
44
|
+
const gathered = [];
|
|
45
|
+
while (cursor >= 0) {
|
|
46
|
+
const line = lines[cursor].trim();
|
|
47
|
+
if (line.startsWith("//")) {
|
|
48
|
+
gathered.unshift(line);
|
|
49
|
+
cursor -= 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (line.endsWith("*/")) {
|
|
53
|
+
// Walk back to the opening of the block, collecting it whole.
|
|
54
|
+
while (cursor >= 0) {
|
|
55
|
+
gathered.unshift(lines[cursor].trim());
|
|
56
|
+
if (lines[cursor].trim().startsWith("/*")) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
cursor -= 1;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
return gathered.join(" ");
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { logger } from "#core/logger";
|
|
2
|
+
/**
|
|
3
|
+
* Human-readable numeric-constant report.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.11.0
|
|
6
|
+
*/
|
|
7
|
+
export function presentConstantAuditResult(result) {
|
|
8
|
+
for (const file of result.files) {
|
|
9
|
+
logger.out(`\n${file.relativePath}`);
|
|
10
|
+
for (const { line, raw, reason } of file.violations) {
|
|
11
|
+
logger.out(` ${line}: ${raw} → ${reason}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const allowlistSuffix = result.allowlistedCount > 0 ? ` (${result.allowlistedCount} allowlisted)` : "";
|
|
15
|
+
if (result.violationCount > 0) {
|
|
16
|
+
logger.out(`\n✖ ${result.violationCount} numeric constant(s) with no kind named${allowlistSuffix}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
logger.out(`✓ Every numeric constant names its kind across ${result.scannedFileCount} source file(s)${allowlistSuffix}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuditCommandPrelude } from "#audit/prepare";
|
|
2
|
+
import { AppError } from "#core/errors";
|
|
3
|
+
import type { Filesystem } from "#core/filesystem/filesystem";
|
|
4
|
+
import type { Result } from "#core/result";
|
|
5
|
+
/**
|
|
6
|
+
* Loads config and resolves the scan target for `audit constants`.
|
|
7
|
+
*
|
|
8
|
+
* @remarks The target is the library tree the convention holds over, from `audit.constants.target`
|
|
9
|
+
* unless the command names one.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.11.0
|
|
12
|
+
*/
|
|
13
|
+
export declare function prepareConstantAudit(fs: Filesystem, args: {
|
|
14
|
+
readonly currentWorkingDirectory: string;
|
|
15
|
+
readonly rawTarget: string | undefined;
|
|
16
|
+
}): Promise<Result<AuditCommandPrelude, AppError>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { resolveRepoRelativePath } from "#audit/prepare";
|
|
2
|
+
import { loadCodefastConfig } from "#core/config";
|
|
3
|
+
import { AppError, messageFrom } from "#core/errors";
|
|
4
|
+
import { err, ok } from "#core/result";
|
|
5
|
+
import { resolveProjectRoot } from "#core/workspace/resolver";
|
|
6
|
+
/**
|
|
7
|
+
* Loads config and resolves the scan target for `audit constants`.
|
|
8
|
+
*
|
|
9
|
+
* @remarks The target is the library tree the convention holds over, from `audit.constants.target`
|
|
10
|
+
* unless the command names one.
|
|
11
|
+
*
|
|
12
|
+
* @since 0.11.0
|
|
13
|
+
*/
|
|
14
|
+
export async function prepareConstantAudit(fs, args) {
|
|
15
|
+
let rootDir;
|
|
16
|
+
try {
|
|
17
|
+
rootDir = fs.canonicalPathSync(resolveProjectRoot(args.currentWorkingDirectory, fs).rootDir);
|
|
18
|
+
}
|
|
19
|
+
catch (caughtError) {
|
|
20
|
+
return err(new AppError("INFRA_FAILURE", messageFrom(caughtError), caughtError));
|
|
21
|
+
}
|
|
22
|
+
const loadedOutcome = await loadCodefastConfig(rootDir, fs);
|
|
23
|
+
if (!loadedOutcome.ok) {
|
|
24
|
+
return loadedOutcome;
|
|
25
|
+
}
|
|
26
|
+
const { config } = loadedOutcome.value;
|
|
27
|
+
const constantsConfig = config.audit?.constants ?? {};
|
|
28
|
+
const resolvedTargetInput = args.rawTarget ?? constantsConfig.target;
|
|
29
|
+
if (resolvedTargetInput === undefined) {
|
|
30
|
+
return err(new AppError("VALIDATION_ERROR", "audit constants needs a target: pass one, or set audit.constants.target in codefast.config"));
|
|
31
|
+
}
|
|
32
|
+
const targetPath = resolveRepoRelativePath(args.rawTarget !== undefined ? args.currentWorkingDirectory : rootDir, resolvedTargetInput);
|
|
33
|
+
if (!fs.existsSync(targetPath)) {
|
|
34
|
+
return err(new AppError("NOT_FOUND", `Not found: ${targetPath}`));
|
|
35
|
+
}
|
|
36
|
+
return ok({ rootDir, targetPath: fs.canonicalPathSync(targetPath), allowlist: constantsConfig.allowlist ?? [] });
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConstantAuditResult } from "#audit/domain/types";
|
|
2
|
+
import { AppError } from "#core/errors";
|
|
3
|
+
import type { Filesystem } from "#core/filesystem/filesystem";
|
|
4
|
+
import type { Result } from "#core/result";
|
|
5
|
+
/**
|
|
6
|
+
* Scans a target path's library sources for numeric constants that name none of the three kinds.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.11.0
|
|
9
|
+
*/
|
|
10
|
+
export declare function runConstantAudit(fs: Filesystem, args: {
|
|
11
|
+
readonly rootDir: string;
|
|
12
|
+
readonly targetPath: string;
|
|
13
|
+
readonly allowlist: ReadonlyArray<string>;
|
|
14
|
+
}): Result<ConstantAuditResult, AppError>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { auditNumericConstants } from "#audit/constants/domain/constants";
|
|
3
|
+
import { AppError, messageFrom } from "#core/errors";
|
|
4
|
+
import { err, ok } from "#core/result";
|
|
5
|
+
import { walkTsxFiles } from "#core/workspace/typescript-walk";
|
|
6
|
+
/**
|
|
7
|
+
* Trees the convention does not reach: a test fixes a value to observe it, a benchmark sizes a
|
|
8
|
+
* workload, and an app or example is a consumer, not the library.
|
|
9
|
+
*/
|
|
10
|
+
const SKIPPED_SEGMENTS = new Set([
|
|
11
|
+
"tests",
|
|
12
|
+
"benchmarks",
|
|
13
|
+
"apps",
|
|
14
|
+
"examples",
|
|
15
|
+
"node_modules",
|
|
16
|
+
"dist",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Scans a target path's library sources for numeric constants that name none of the three kinds.
|
|
20
|
+
*
|
|
21
|
+
* @since 0.11.0
|
|
22
|
+
*/
|
|
23
|
+
export function runConstantAudit(fs, args) {
|
|
24
|
+
try {
|
|
25
|
+
const allowlist = new Set(args.allowlist);
|
|
26
|
+
const { rootDir, targetPath } = args;
|
|
27
|
+
const filesToScan = collectScanPaths(fs, rootDir, targetPath);
|
|
28
|
+
const files = [];
|
|
29
|
+
let violationCount = 0;
|
|
30
|
+
let allowlistedCount = 0;
|
|
31
|
+
for (const absolutePath of filesToScan) {
|
|
32
|
+
const relativePath = toPosixPath(path.relative(rootDir, absolutePath));
|
|
33
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
34
|
+
const remaining = auditNumericConstants(content).filter(({ raw }) => {
|
|
35
|
+
const name = raw.split(" ")[0];
|
|
36
|
+
const isAllowed = allowlist.has(name) || allowlist.has(`${relativePath}:${name}`);
|
|
37
|
+
if (isAllowed) {
|
|
38
|
+
allowlistedCount++;
|
|
39
|
+
}
|
|
40
|
+
return !isAllowed;
|
|
41
|
+
});
|
|
42
|
+
if (remaining.length === 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
violationCount += remaining.length;
|
|
46
|
+
files.push({ relativePath, violations: remaining });
|
|
47
|
+
}
|
|
48
|
+
return ok({ files, violationCount, allowlistedCount, scannedFileCount: filesToScan.length });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return err(new AppError("INFRA_FAILURE", messageFrom(error), error));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The `.ts` files under a `src` directory of a library package, below the target. */
|
|
55
|
+
function collectScanPaths(fs, rootDir, targetPath) {
|
|
56
|
+
const candidates = fs.statSync(targetPath).isDirectory() ? walkTsxFiles(targetPath, fs) : [targetPath];
|
|
57
|
+
return candidates.filter((absolutePath) => {
|
|
58
|
+
const segments = toPosixPath(path.relative(rootDir, absolutePath)).split("/");
|
|
59
|
+
return segments.includes("src") && !segments.some((segment) => SKIPPED_SEGMENTS.has(segment));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function toPosixPath(filePath) {
|
|
63
|
+
return filePath.split(path.sep).join("/");
|
|
64
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lineOfOffset } from "#core/source-position";
|
|
1
2
|
/** The owner: a kebab-case package, app or feature slug, or a scoped package name. */
|
|
2
3
|
const NAMESPACE = /^(?:@[a-z0-9-]+\/)?[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3
4
|
/** A token or module stands for a type or a unit of composition. */
|
|
@@ -59,13 +60,4 @@ function reasonFor(kind, name) {
|
|
|
59
60
|
return PASCAL_CASE.test(local)
|
|
60
61
|
? null
|
|
61
62
|
: `${label} '${local}' in '${name}' is not PascalCase — a ${kind} stands for a ${kind === "token" ? "type" : "unit of composition"}`;
|
|
62
|
-
}
|
|
63
|
-
function lineOfOffset(sourceText, offset) {
|
|
64
|
-
let line = 1;
|
|
65
|
-
for (let index = 0; index < offset; index++) {
|
|
66
|
-
if (sourceText.charCodeAt(index) === 10) {
|
|
67
|
-
line++;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return line;
|
|
71
63
|
}
|
|
@@ -73,6 +73,37 @@ export type ImportsAuditResult = {
|
|
|
73
73
|
readonly allowlistedCount: number;
|
|
74
74
|
readonly scannedFileCount: number;
|
|
75
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* A double assertion through `unknown` or `any`, or a directive that keeps one without cause.
|
|
78
|
+
*
|
|
79
|
+
* @since 0.13.0
|
|
80
|
+
*/
|
|
81
|
+
export type AssertionViolation = {
|
|
82
|
+
readonly line: number;
|
|
83
|
+
/** The assertion or the directive as written, up to its first line break. */
|
|
84
|
+
readonly raw: string;
|
|
85
|
+
readonly reason: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The type-assertion violations found in one file.
|
|
89
|
+
*
|
|
90
|
+
* @since 0.13.0
|
|
91
|
+
*/
|
|
92
|
+
export type AssertionFileViolations = {
|
|
93
|
+
readonly relativePath: string;
|
|
94
|
+
readonly violations: Array<AssertionViolation>;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Outcome of one `audit assertions` run.
|
|
98
|
+
*
|
|
99
|
+
* @since 0.13.0
|
|
100
|
+
*/
|
|
101
|
+
export type AssertionAuditResult = {
|
|
102
|
+
readonly files: Array<AssertionFileViolations>;
|
|
103
|
+
readonly violationCount: number;
|
|
104
|
+
readonly allowlistedCount: number;
|
|
105
|
+
readonly scannedFileCount: number;
|
|
106
|
+
};
|
|
76
107
|
/**
|
|
77
108
|
* A `token()`, `tag()` or module display name that breaks the display-name convention.
|
|
78
109
|
*
|
|
@@ -104,6 +135,37 @@ export type DisplayNameAuditResult = {
|
|
|
104
135
|
readonly allowlistedCount: number;
|
|
105
136
|
readonly scannedFileCount: number;
|
|
106
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* A numeric constant whose comment names none of the three kinds.
|
|
140
|
+
*
|
|
141
|
+
* @since 0.11.0
|
|
142
|
+
*/
|
|
143
|
+
export type ConstantViolation = {
|
|
144
|
+
readonly line: number;
|
|
145
|
+
/** The declaration's name and value, as `NAME = 32`. */
|
|
146
|
+
readonly raw: string;
|
|
147
|
+
readonly reason: string;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* The numeric-constant violations found in one file.
|
|
151
|
+
*
|
|
152
|
+
* @since 0.11.0
|
|
153
|
+
*/
|
|
154
|
+
export type ConstantFileViolations = {
|
|
155
|
+
readonly relativePath: string;
|
|
156
|
+
readonly violations: Array<ConstantViolation>;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Outcome of one `audit constants` run.
|
|
160
|
+
*
|
|
161
|
+
* @since 0.11.0
|
|
162
|
+
*/
|
|
163
|
+
export type ConstantAuditResult = {
|
|
164
|
+
readonly files: Array<ConstantFileViolations>;
|
|
165
|
+
readonly violationCount: number;
|
|
166
|
+
readonly allowlistedCount: number;
|
|
167
|
+
readonly scannedFileCount: number;
|
|
168
|
+
};
|
|
107
169
|
/**
|
|
108
170
|
* A broken link or anchor found by the link audit.
|
|
109
171
|
*
|
|
@@ -200,6 +262,28 @@ export type UnshippedTargetViolation = {
|
|
|
200
262
|
readonly subpath: string;
|
|
201
263
|
readonly target: string;
|
|
202
264
|
};
|
|
265
|
+
/**
|
|
266
|
+
* A path a stylesheet registers with Tailwind's `@source`, as written.
|
|
267
|
+
*
|
|
268
|
+
* @since 0.13.0
|
|
269
|
+
*/
|
|
270
|
+
export type StylesheetSource = {
|
|
271
|
+
readonly line: number;
|
|
272
|
+
readonly pattern: string;
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* A shipped stylesheet whose `@source` paths reach no file its package's tarball ships.
|
|
276
|
+
*
|
|
277
|
+
* @since 0.13.0
|
|
278
|
+
*/
|
|
279
|
+
export type UnreachableStylesheetViolation = {
|
|
280
|
+
readonly packageName: string;
|
|
281
|
+
/** Repo-relative path of the stylesheet. */
|
|
282
|
+
readonly stylesheet: string;
|
|
283
|
+
readonly sources: Array<StylesheetSource>;
|
|
284
|
+
/** The `files` entries missing on disk, which is how an unbuilt `dist` shows up. */
|
|
285
|
+
readonly missingFilesEntries: Array<string>;
|
|
286
|
+
};
|
|
203
287
|
/**
|
|
204
288
|
* Outcome of one `audit publish` run.
|
|
205
289
|
*
|
|
@@ -208,6 +292,7 @@ export type UnshippedTargetViolation = {
|
|
|
208
292
|
export type PublishAuditResult = {
|
|
209
293
|
readonly legacyImportFiles: Array<LegacySubpathFile>;
|
|
210
294
|
readonly unshipped: Array<UnshippedTargetViolation>;
|
|
295
|
+
readonly unreachableStylesheets: Array<UnreachableStylesheetViolation>;
|
|
211
296
|
readonly legacyImportCount: number;
|
|
212
297
|
readonly scannedFileCount: number;
|
|
213
298
|
readonly packageCount: number;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { parseSync } from "oxc-parser";
|
|
2
|
+
import { isOxcNode, programStatements } from "#core/oxc-node";
|
|
3
|
+
import { firstLineOf, lineOfOffset } from "#core/source-position";
|
|
2
4
|
/**
|
|
3
5
|
* The import policies enforced across the monorepo: React members by name (never a namespace,
|
|
4
6
|
* default, or implicit `React.*` UMD global), and Zod as a namespace repo-wide — the house form,
|
|
@@ -20,9 +22,6 @@ export const defaultImportPolicyRules = [
|
|
|
20
22
|
"locale set into any bundle that reaches it)",
|
|
21
23
|
},
|
|
22
24
|
];
|
|
23
|
-
function isOxcNode(value) {
|
|
24
|
-
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
25
|
-
}
|
|
26
25
|
function isIdentifierNamed(node, name) {
|
|
27
26
|
return isOxcNode(node) && node.type === "Identifier" && node.name === name;
|
|
28
27
|
}
|
|
@@ -41,7 +40,7 @@ function importedName(specifier) {
|
|
|
41
40
|
*/
|
|
42
41
|
export function auditImportPolicySource(filePath, sourceText, rules) {
|
|
43
42
|
const { program } = parseSync(filePath, sourceText);
|
|
44
|
-
const statements = program
|
|
43
|
+
const statements = programStatements(program);
|
|
45
44
|
const violations = [];
|
|
46
45
|
const boundUmdNames = new Set();
|
|
47
46
|
for (const rule of rules) {
|
|
@@ -89,7 +88,7 @@ export function auditImportPolicySource(filePath, sourceText, rules) {
|
|
|
89
88
|
}
|
|
90
89
|
}
|
|
91
90
|
for (const rule of rules) {
|
|
92
|
-
if (rule.umdGlobal !== undefined && !boundUmdNames.has(rule.umdGlobal)) {
|
|
91
|
+
if (rule.umdGlobal !== undefined && !boundUmdNames.has(rule.umdGlobal) && isOxcNode(program)) {
|
|
93
92
|
collectUmdGlobalReferences(program, sourceText, rule, violations);
|
|
94
93
|
}
|
|
95
94
|
}
|
|
@@ -124,17 +123,4 @@ function collectUmdGlobalReferences(node, sourceText, rule, violations) {
|
|
|
124
123
|
collectUmdGlobalReferences(value, sourceText, rule, violations);
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
|
-
}
|
|
128
|
-
function lineOfOffset(sourceText, offset) {
|
|
129
|
-
let line = 1;
|
|
130
|
-
for (let index = 0; index < offset; index++) {
|
|
131
|
-
if (sourceText.charCodeAt(index) === 10) {
|
|
132
|
-
line++;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return line;
|
|
136
|
-
}
|
|
137
|
-
function firstLineOf(text) {
|
|
138
|
-
const newlineIndex = text.indexOf("\n");
|
|
139
|
-
return newlineIndex === -1 ? text : text.slice(0, newlineIndex);
|
|
140
126
|
}
|