@codefast/cli 0.13.0 → 0.14.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 +29 -0
- package/README.md +58 -39
- package/dist/audit/command.js +23 -22
- package/dist/audit/comments/domain/comment-content.d.ts +12 -0
- package/dist/audit/comments/domain/comment-content.js +17 -4
- package/dist/audit/domain/types.d.ts +32 -31
- package/dist/audit/layers/cli-result.d.ts +13 -0
- package/dist/audit/{constants → layers}/cli-result.js +6 -6
- package/dist/audit/layers/cli-schema.d.ts +29 -0
- package/dist/audit/layers/cli-schema.js +17 -0
- package/dist/audit/layers/domain/layering.d.ts +46 -0
- package/dist/audit/layers/domain/layering.js +191 -0
- package/dist/audit/layers/output.d.ts +7 -0
- package/dist/audit/{constants → layers}/output.js +5 -5
- package/dist/audit/layers/prepare.d.ts +25 -0
- package/dist/audit/layers/prepare.js +66 -0
- package/dist/audit/layers/run.d.ts +19 -0
- package/dist/audit/layers/run.js +62 -0
- package/dist/audit/prepare.d.ts +13 -1
- package/dist/audit/prepare.js +16 -1
- package/dist/core/config/schema.d.ts +12 -4
- package/dist/core/config/schema.js +13 -1
- package/dist/tag/cli-result.d.ts +3 -0
- package/dist/tag/cli-result.js +8 -2
- package/dist/tag/domain/types.d.ts +15 -0
- package/dist/tag/output.js +13 -6
- package/dist/tag/run.js +2 -0
- package/dist/tag/writer/since-writer.d.ts +5 -0
- package/dist/tag/writer/since-writer.js +41 -9
- package/package.json +1 -1
- package/dist/audit/constants/cli-result.d.ts +0 -13
- package/dist/audit/constants/cli-schema.d.ts +0 -18
- package/dist/audit/constants/cli-schema.js +0 -12
- package/dist/audit/constants/domain/constants.d.ts +0 -8
- package/dist/audit/constants/domain/constants.js +0 -66
- package/dist/audit/constants/output.d.ts +0 -7
- package/dist/audit/constants/prepare.d.ts +0 -16
- package/dist/audit/constants/prepare.js +0 -37
- package/dist/audit/constants/run.d.ts +0 -14
- package/dist/audit/constants/run.js +0 -64
|
@@ -28,5 +28,10 @@ export declare class TagSinceWriter {
|
|
|
28
28
|
private jsDocHasSinceTag;
|
|
29
29
|
private makeJSDocSinceLine;
|
|
30
30
|
private makeSinceOnlyJSDocBlock;
|
|
31
|
+
/**
|
|
32
|
+
* Returns whether the line above `anchor` holds a `//` note, which a fresh block would stack under,
|
|
33
|
+
* or a directive, which would then govern the block instead of the declaration.
|
|
34
|
+
*/
|
|
35
|
+
private isDocBlockLineTaken;
|
|
31
36
|
private makeDeclarationSinceLine;
|
|
32
37
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { parseSync } from "oxc-parser";
|
|
2
|
+
import { isDirectiveLine, isNoteLine } from "#audit/comments/domain/comment-content";
|
|
2
3
|
import { isOxcNode, programStatements } from "#core/oxc-node";
|
|
4
|
+
import { lineOfOffset } from "#core/source-position";
|
|
3
5
|
import { applyEditsDescending, indentOfLineContaining } from "#core/source-text-edit";
|
|
4
6
|
/**
|
|
5
|
-
* Top-level statement kinds that carry a `@since` tag
|
|
6
|
-
*
|
|
7
|
+
* Top-level statement kinds that carry a `@since` tag. `TSDeclareFunction` is a function with no body —
|
|
8
|
+
* an overload signature or a `declare function` — which the `.d.ts` keeps with its own doc block.
|
|
7
9
|
*/
|
|
8
10
|
const TAGGABLE_DECLARATION_TYPES = new Set([
|
|
9
11
|
"FunctionDeclaration",
|
|
12
|
+
"TSDeclareFunction",
|
|
10
13
|
"ClassDeclaration",
|
|
11
14
|
"TSInterfaceDeclaration",
|
|
12
15
|
"TSTypeAliasDeclaration",
|
|
@@ -19,6 +22,14 @@ function identifierName(node) {
|
|
|
19
22
|
}
|
|
20
23
|
return undefined;
|
|
21
24
|
}
|
|
25
|
+
function lineAboveOffset(sourceText, offset) {
|
|
26
|
+
const lineStart = sourceText.lastIndexOf("\n", offset - 1) + 1;
|
|
27
|
+
if (lineStart === 0) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const lineAboveStart = lineStart >= 2 ? sourceText.lastIndexOf("\n", lineStart - 2) + 1 : 0;
|
|
31
|
+
return sourceText.slice(lineAboveStart, lineStart - 1);
|
|
32
|
+
}
|
|
22
33
|
/**
|
|
23
34
|
* The writer that adds missing `@since` tags to a file's exported declarations.
|
|
24
35
|
*
|
|
@@ -36,20 +47,34 @@ export class TagSinceWriter {
|
|
|
36
47
|
const statements = programStatements(program);
|
|
37
48
|
const jsDocComments = comments.filter((comment) => comment.type === "Block" && comment.value.startsWith("*"));
|
|
38
49
|
const edits = [];
|
|
50
|
+
const blockedDeclarations = [];
|
|
39
51
|
for (const declaration of this.collectExportedDeclarations(statements)) {
|
|
40
|
-
const
|
|
52
|
+
const existing = this.associatedJsDoc(declaration, jsDocComments, sourceText);
|
|
53
|
+
if (existing === undefined && this.isDocBlockLineTaken(declaration, sourceText)) {
|
|
54
|
+
const names = this.taggableDeclarationOf(declaration)?.names ?? [];
|
|
55
|
+
blockedDeclarations.push({
|
|
56
|
+
filePath,
|
|
57
|
+
line: lineOfOffset(sourceText, declaration.start),
|
|
58
|
+
name: names.length > 0 ? names.join(", ") : "default",
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const edit = this.makeDeclarationSinceLine(declaration, existing, sourceText, version);
|
|
41
63
|
if (edit) {
|
|
42
64
|
edits.push(edit);
|
|
43
65
|
}
|
|
44
66
|
}
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
// A blocked declaration holds its whole file back, so every reported line matches the file on disk.
|
|
68
|
+
const appliedEdits = blockedDeclarations.length > 0 ? [] : edits;
|
|
69
|
+
if (appliedEdits.length > 0 && write) {
|
|
70
|
+
const updated = applyEditsDescending(sourceText, appliedEdits);
|
|
47
71
|
this.fs.writeFileSync(filePath, updated, "utf8");
|
|
48
72
|
}
|
|
49
73
|
return {
|
|
50
74
|
filePath,
|
|
51
|
-
taggedDeclarations:
|
|
52
|
-
|
|
75
|
+
taggedDeclarations: appliedEdits.length,
|
|
76
|
+
blockedDeclarations,
|
|
77
|
+
changed: appliedEdits.length > 0,
|
|
53
78
|
};
|
|
54
79
|
}
|
|
55
80
|
/**
|
|
@@ -211,8 +236,15 @@ export class TagSinceWriter {
|
|
|
211
236
|
const tag = this.sinceDocumentationTag;
|
|
212
237
|
return `/**\n${declarationIndent} * ${tag} ${version}\n${declarationIndent} */`;
|
|
213
238
|
}
|
|
214
|
-
|
|
215
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Returns whether the line above `anchor` holds a `//` note, which a fresh block would stack under,
|
|
241
|
+
* or a directive, which would then govern the block instead of the declaration.
|
|
242
|
+
*/
|
|
243
|
+
isDocBlockLineTaken(anchor, sourceText) {
|
|
244
|
+
const lineAbove = lineAboveOffset(sourceText, anchor.start);
|
|
245
|
+
return lineAbove !== undefined && (isNoteLine(lineAbove) || isDirectiveLine(lineAbove));
|
|
246
|
+
}
|
|
247
|
+
makeDeclarationSinceLine(anchor, existing, sourceText, version) {
|
|
216
248
|
if (existing) {
|
|
217
249
|
if (this.jsDocHasSinceTag(existing)) {
|
|
218
250
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,13 +0,0 @@
|
|
|
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;
|
|
@@ -1,18 +0,0 @@
|
|
|
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>;
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
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>;
|
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
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>>;
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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>;
|
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
}
|