@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @codefast/cli
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#951](https://github.com/codefastlabs/codefast/pull/951) Add `codefast audit assertions`, which reports every double type assertion through `unknown` or `any` —
|
|
8
|
+
`x as unknown as T`, `(x as unknown) as T`, `<T><unknown>x` and the `any` spellings — in `.ts`/`.tsx` files, tests
|
|
9
|
+
included. Where the erasure is the point, keep one with `// codefast-allow-double-assertion: <reason>` on its line or
|
|
10
|
+
the line above; a directive with no reason, or one that keeps no assertion, is reported too. Exceptions can also go in
|
|
11
|
+
`audit.assertions.allowlist`.
|
|
12
|
+
|
|
13
|
+
`Filesystem.readdir` is replaced by `readdirEntries(path, { recursive })`, which always returns directory entries: every
|
|
14
|
+
caller asked for them, and the old `string[] | DirectoryEntry[]` union forced each one to cast or guard.
|
|
15
|
+
|
|
16
|
+
- [#894](https://github.com/codefastlabs/codefast/pull/894) `codefast audit constants` reports every upper-case `const` bound to a number in a library's sources whose comment names
|
|
17
|
+
none of the three kinds a number may be — a constant of the machine, a value the contract fixes, or one derived from
|
|
18
|
+
bind-time data. Sentinel values (`0`, `1`, `-1`) are exempt; `audit.constants.target` and `audit.constants.allowlist` in
|
|
19
|
+
`codefast.config` scope and except it.
|
|
20
|
+
|
|
21
|
+
- [#980](https://github.com/codefastlabs/codefast/pull/980) `audit publish` also reports a shipped stylesheet whose Tailwind `@source` paths reach none of the files the slimmed
|
|
22
|
+
tarball ships, naming any `files` entry missing on disk so an unbuilt `dist` reads as such. A workspace resolves those
|
|
23
|
+
paths against `src`, so only the published layout used to show the failure.
|
|
24
|
+
|
|
25
|
+
- [#976](https://github.com/codefastlabs/codefast/pull/976) `engines.node` is now `>=24.0.0`, up from `>=22.12.0`, and Node 22 is no longer supported. Node 24.0.0 is the first
|
|
26
|
+
release with explicit resource management built in (`using`, `await using`, `DisposableStack`, `AsyncDisposableStack`,
|
|
27
|
+
`SuppressedError`) and all of ES2025, so the packages use both as the platform ships them instead of shimming them for
|
|
28
|
+
an older line, and the CI matrix runs the unit suite on 24.0.0 itself. Move to Node 24, or stay on the current minor
|
|
29
|
+
while a deployment still runs Node 22.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [#965](https://github.com/codefastlabs/codefast/pull/965) The optional `typescript` peer is now `>=7.0.0`. It was published as the repository's own `^7.0.2` pin, which excluded
|
|
34
|
+
TypeScript 8 and would have risen with each pin bump instead of staying at the supported floor.
|
|
35
|
+
|
|
36
|
+
- [#965](https://github.com/codefastlabs/codefast/pull/965) `README.md` now states TypeScript 7 or later as the floor for the package's types — the one compiler every `@codefast/*`
|
|
37
|
+
package is built and checked with. No code or declaration changed.
|
|
38
|
+
|
|
3
39
|
## 0.12.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -22,7 +22,9 @@ encode an opinionated house style (called out below) that you can adopt, ignore,
|
|
|
22
22
|
|
|
23
23
|
## Requirements
|
|
24
24
|
|
|
25
|
-
- **Node.js ≥
|
|
25
|
+
- **Node.js ≥ 24** (the CLI is published as ESM).
|
|
26
|
+
- **TypeScript ≥ 7** to type-check a config written with `defineConfig`, and for
|
|
27
|
+
`arrange simplify --fold-variant-classname`, the one command that loads `typescript` (an optional peer).
|
|
26
28
|
- **A project root — workspace or single package.** Commands resolve their root by walking up from the current
|
|
27
29
|
directory: the nearest `pnpm-workspace.yaml` marks a **workspace** (every package under it is in scope), and with no
|
|
28
30
|
workspace file the nearest `package.json` marks a **single package** (that one package is the whole scope). Only
|
|
@@ -108,7 +110,10 @@ codefast # Codefast monorepo developer CLI
|
|
|
108
110
|
│ ├─ rtl [target] # physical-direction Tailwind classes to make logical / rtl:-paired
|
|
109
111
|
│ ├─ links [target] # markdown links pointing at a missing path/anchor
|
|
110
112
|
│ ├─ imports [target] # banned import forms (React by-name, Zod namespace in front-end, …)
|
|
113
|
+
│ ├─ assertions [target] # double type assertions through unknown/any (x as unknown as T)
|
|
114
|
+
│ ├─ constants [target] # numeric constants whose comment names none of the three kinds
|
|
111
115
|
│ ├─ display-names [target] # token()/tag()/module names breaking the <namespace>:<Name> convention
|
|
116
|
+
│ ├─ publish [target] # what breaks a consumer's install: #/ imports, unshipped targets, @source paths
|
|
112
117
|
│ └─ comments [target] # section dividers not in the one allowed form
|
|
113
118
|
│ └─ --fix # rewrite every fixable divider in place (the only audit that writes)
|
|
114
119
|
│ (each audit also takes [target] + --json)
|
|
@@ -139,14 +144,17 @@ Every command also responds to `--help`; each command's section below explains w
|
|
|
139
144
|
| `audit rtl` | Report physical-direction Tailwind classes that should be logical | no |
|
|
140
145
|
| `audit links` | Report markdown cross-references that resolve to nothing | no |
|
|
141
146
|
| `audit imports` | Enforce the import policy (React by-name, Zod namespace in front-end) | no (report only) |
|
|
147
|
+
| `audit assertions` | Report double type assertions through `unknown` / `any`, tests included | no |
|
|
148
|
+
| `audit constants` | Require every tuned numeric constant to name the kind of number it is | no |
|
|
142
149
|
| `audit display-names` | Enforce the `namespace:Name` display-name convention | no |
|
|
150
|
+
| `audit publish` | Report what would break a consumer's install of a published package | no |
|
|
143
151
|
| `audit comments` | Check doc-comment conventions; repair section dividers | `--fix` only |
|
|
144
152
|
|
|
145
|
-
**Which of these are for you?** `arrange`, `mirror`, `pack-slim`, `tag`,
|
|
146
|
-
work for any pnpm workspace or single package that builds with `tsc`. The
|
|
147
|
-
style (logical Tailwind directions, named React imports, a specific
|
|
148
|
-
for `@codefast/di` tokens
|
|
149
|
-
their scope.
|
|
153
|
+
**Which of these are for you?** `arrange`, `mirror`, `pack-slim`, `tag`, `audit links`, `audit assertions`, and
|
|
154
|
+
`audit publish` are general-purpose — they work for any pnpm workspace or single package that builds with `tsc`. The
|
|
155
|
+
other five audits encode codefast's own house style (logical Tailwind directions, named React imports, a specific
|
|
156
|
+
comment/divider grammar, a `namespace:Name` scheme for `@codefast/di` tokens, a named kind for every tuned numeric
|
|
157
|
+
constant). Adopt them if they fit your project; otherwise skip them, or use an allowlist to narrow their scope.
|
|
150
158
|
|
|
151
159
|
## `arrange`
|
|
152
160
|
|
|
@@ -244,12 +252,13 @@ apply (see the [Configuration](#configuration) example for their shape):
|
|
|
244
252
|
## `pack-slim`
|
|
245
253
|
|
|
246
254
|
Slims a published package down to what a consumer's `tsc` and Node actually read, so the npm tarball ships `dist`
|
|
247
|
-
runtime and types
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
result is **never
|
|
255
|
+
runtime and types, plus any `src` subtree an export still points into (a raw stylesheet export such as `./css/*`). Where
|
|
256
|
+
`mirror` writes the full exports — including the `source` condition — for local development, `pack-slim` removes that
|
|
257
|
+
development lane for publish: it drops the rest of `src` from `files`, every `source` condition from
|
|
258
|
+
`exports`/`imports`, every `imports` entry left pointing outside `files`, every script that is not an install or publish
|
|
259
|
+
lifecycle hook, `devDependencies`, and the `dist` source maps plus their dangling `sourceMappingURL` directives. Private
|
|
260
|
+
packages are skipped. It is meant to run on an ephemeral CI checkout right before publish, so its result is **never
|
|
261
|
+
committed**.
|
|
253
262
|
|
|
254
263
|
Because that result must never be committed, `pack-slim` refuses to write when the git working tree has uncommitted
|
|
255
264
|
tracked changes — a guard against an accidental local run landing on real work. `--dry-run` is exempt (it writes
|
|
@@ -332,6 +341,50 @@ codefast audit imports --json # machine-readable summary
|
|
|
332
341
|
Configure exceptions via `audit.imports.allowlist` — each entry is the offending source text as written or
|
|
333
342
|
`repo/relative/path.tsx:<text>`.
|
|
334
343
|
|
|
344
|
+
### `audit assertions`
|
|
345
|
+
|
|
346
|
+
Reports every double type assertion through `unknown` or `any` in `.ts`/`.tsx` files, tests included —
|
|
347
|
+
`x as unknown as T`, `(x as unknown) as T`, `<T><unknown>x` and the `any` spellings. The pair tells the compiler two
|
|
348
|
+
types are unrelated and silences it anyway; no oxlint rule targets it (`typescript/no-unnecessary-type-assertion`
|
|
349
|
+
catches the redundant ones only). Make the types agree, narrow with a type guard, or — where the erasure is the point —
|
|
350
|
+
keep it with a directive that states why, on the assertion's line or the line above:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
// codefast-allow-double-assertion: the host stores each plugin's config untyped; the plugin owns its shape
|
|
354
|
+
const config = host.configFor(plugin.id) as unknown as PluginConfig;
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
A directive with no reason, or one that keeps no assertion, is itself reported, so a kept assertion cannot outlive its
|
|
358
|
+
cause.
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
codefast audit assertions # whole repo
|
|
362
|
+
codefast audit assertions packages/di # explicit target
|
|
363
|
+
codefast audit assertions --json # machine-readable summary
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Configure exceptions via `audit.assertions.allowlist` — each entry is the assertion as written or
|
|
367
|
+
`repo/relative/path.ts:<assertion>` — though the inline directive keeps the reason beside the code it excuses.
|
|
368
|
+
|
|
369
|
+
### `audit publish`
|
|
370
|
+
|
|
371
|
+
Reports what would break a consumer's install of a published package, while it can still be fixed:
|
|
372
|
+
|
|
373
|
+
- a `#/`-prefixed internal import, which Node's ESM resolver rejects on Node 24 before 24.14 while every in-repo runner
|
|
374
|
+
accepts it;
|
|
375
|
+
- an `exports`/`imports` target the slimmed manifest does not ship, found by applying the same slim as `pack-slim`;
|
|
376
|
+
- a shipped stylesheet whose Tailwind `@source` paths reach none of the files the tarball ships. A workspace still
|
|
377
|
+
resolves them against `src`, so only the published layout shows it: the consumer's Tailwind registers no class names
|
|
378
|
+
and the components render unstyled.
|
|
379
|
+
|
|
380
|
+
It reads the built output, so run it after the build; a `files` entry missing on disk, such as an unbuilt `dist`, is
|
|
381
|
+
named in the report. Private packages are skipped, and it takes no allowlist.
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
codefast audit publish # every published package in the workspace
|
|
385
|
+
codefast audit publish --json # machine-readable summary
|
|
386
|
+
```
|
|
387
|
+
|
|
335
388
|
### `audit comments`
|
|
336
389
|
|
|
337
390
|
_House style._ Checks doc-comment conventions. Section dividers not in the one allowed form are mechanical, so `--fix`
|
|
@@ -367,6 +420,30 @@ codefast audit display-names --json # machine-readable summary
|
|
|
367
420
|
Configure exceptions via `audit.displayNames.allowlist` — each entry is the call as written, through its closing quote
|
|
368
421
|
(or parenthesis when the name is the only argument), or `repo/relative/path.ts:<call>`.
|
|
369
422
|
|
|
423
|
+
### `audit constants`
|
|
424
|
+
|
|
425
|
+
_House style._ Holds library sources to one rule for tuned numbers: a numeric constant says which kind of number it is.
|
|
426
|
+
It flags an upper-case `const NAME = <number>` in a `.ts`/`.tsx` file under a `src` directory — tests, benchmarks, apps,
|
|
427
|
+
and examples are out of scope — unless the comment directly above it, a `/* … */` block or a run of `//` lines, names
|
|
428
|
+
one of three kinds:
|
|
429
|
+
|
|
430
|
+
- `a constant of the machine` — a width of the platform the code runs on;
|
|
431
|
+
- `a value the contract fixes` — a number the documented contract promises;
|
|
432
|
+
- `derived from bind-time data` — a figure computed from what a caller hands in.
|
|
433
|
+
|
|
434
|
+
A count that merely looks reasonable is none of them, so it is reported. `0`, `1`, and `-1` are skipped: they stand for
|
|
435
|
+
absence or identity, not for a tuned size.
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
codefast audit constants # uses audit.constants.target from config
|
|
439
|
+
codefast audit constants packages/di/src # explicit target
|
|
440
|
+
codefast audit constants --json # machine-readable summary
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
With no `[target]`, the scan root is `audit.constants.target` from the config; when neither is set the command fails.
|
|
444
|
+
Configure exceptions via `audit.constants.allowlist` — each entry is the constant's name or `repo/relative/path.ts:NAME`
|
|
445
|
+
— for a measured policy that has to stay a tuned number.
|
|
446
|
+
|
|
370
447
|
## Configuration
|
|
371
448
|
|
|
372
449
|
**You do not need a config file.** Every command has sensible defaults and works with none. Add a `codefast.config.*`
|
|
@@ -500,7 +577,12 @@ export default {
|
|
|
500
577
|
links: { allowlist: [] }, // bare link target, or `repo/relative/doc.md:target`
|
|
501
578
|
comments: { allowlist: [] }, // divider as written, or `repo/relative/path.ts:<divider>`
|
|
502
579
|
imports: { allowlist: [] }, // offending import text as written, or `repo/relative/path.tsx:<text>`
|
|
580
|
+
assertions: { allowlist: [] }, // assertion as written, or `repo/relative/path.ts:<assertion>`
|
|
503
581
|
displayNames: { allowlist: [] }, // call as written, or `repo/relative/path.ts:<call>`
|
|
582
|
+
constants: {
|
|
583
|
+
target: "packages/core/src", // default scan root when no CLI arg is passed
|
|
584
|
+
allowlist: [], // constant name, or `repo/relative/path.ts:NAME`
|
|
585
|
+
},
|
|
504
586
|
},
|
|
505
587
|
};
|
|
506
588
|
```
|
|
@@ -551,7 +633,10 @@ pnpm run cli:audit:links # codefast audit links
|
|
|
551
633
|
pnpm run cli:audit:rtl # codefast audit rtl
|
|
552
634
|
pnpm run cli:audit:comments # codefast audit comments
|
|
553
635
|
pnpm run cli:audit:imports # codefast audit imports
|
|
636
|
+
pnpm run cli:audit:assertions # codefast audit assertions
|
|
554
637
|
pnpm run cli:audit:display-names # codefast audit display-names
|
|
638
|
+
pnpm run cli:audit:constants # codefast audit constants
|
|
639
|
+
pnpm run cli:audit:publish # codefast audit publish
|
|
555
640
|
```
|
|
556
641
|
|
|
557
642
|
`pnpm run version-packages` runs `changeset version` and then `codefast tag`, so published APIs are stamped at release,
|
|
@@ -4,9 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { parseSync } from "oxc-parser";
|
|
6
6
|
import { DomainBinaryOperator, DomainSyntaxKind } from "#arrange/domain/ast/ast-node";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { isOxcNode, programStatements } from "#core/oxc-node";
|
|
8
|
+
/**
|
|
9
|
+
* Stands in for a child while its parent is built first, so the child has a parent to point at; the
|
|
10
|
+
* build replaces it before the node leaves the translator.
|
|
11
|
+
*/
|
|
12
|
+
const PENDING_NODE = Object.freeze({
|
|
13
|
+
kind: DomainSyntaxKind.Unknown,
|
|
14
|
+
pos: -1,
|
|
15
|
+
end: -1,
|
|
16
|
+
parent: null,
|
|
17
|
+
children: [],
|
|
18
|
+
});
|
|
19
|
+
/** The same stand-in for a child typed as an identifier. */
|
|
20
|
+
const PENDING_IDENTIFIER = Object.freeze({
|
|
21
|
+
kind: DomainSyntaxKind.Identifier,
|
|
22
|
+
pos: -1,
|
|
23
|
+
end: -1,
|
|
24
|
+
parent: null,
|
|
25
|
+
text: "",
|
|
26
|
+
});
|
|
10
27
|
function nodeName(node) {
|
|
11
28
|
return typeof node.name === "string" ? node.name : "";
|
|
12
29
|
}
|
|
@@ -87,15 +104,13 @@ export class TypeScriptAstTranslator {
|
|
|
87
104
|
end: node.end,
|
|
88
105
|
parent,
|
|
89
106
|
importClause: undefined,
|
|
90
|
-
moduleSpecifier:
|
|
107
|
+
moduleSpecifier: PENDING_NODE,
|
|
91
108
|
};
|
|
92
109
|
if (specifiers.length > 0) {
|
|
93
110
|
self.importClause = this.buildImportClause(node, specifiers, self);
|
|
94
111
|
}
|
|
95
112
|
const source = isOxcNode(node.source) ? node.source : undefined;
|
|
96
|
-
self.moduleSpecifier = source
|
|
97
|
-
? this.translateNode(source, self)
|
|
98
|
-
: this.translateUnknown(node, self);
|
|
113
|
+
self.moduleSpecifier = source ? this.translateNode(source, self) : this.translateUnknown(node, self);
|
|
99
114
|
return self;
|
|
100
115
|
}
|
|
101
116
|
buildImportClause(declaration, specifiers, parent) {
|
|
@@ -131,7 +146,7 @@ export class TypeScriptAstTranslator {
|
|
|
131
146
|
pos: specifier.start,
|
|
132
147
|
end: specifier.end,
|
|
133
148
|
parent,
|
|
134
|
-
name:
|
|
149
|
+
name: PENDING_IDENTIFIER,
|
|
135
150
|
};
|
|
136
151
|
self.name = this.translateIdentifier(local, self);
|
|
137
152
|
return self;
|
|
@@ -158,7 +173,7 @@ export class TypeScriptAstTranslator {
|
|
|
158
173
|
end: specifier.end,
|
|
159
174
|
parent,
|
|
160
175
|
propertyName: undefined,
|
|
161
|
-
name:
|
|
176
|
+
name: PENDING_IDENTIFIER,
|
|
162
177
|
};
|
|
163
178
|
if (imported && imported.type === "Identifier" && nodeName(imported) !== nodeName(local)) {
|
|
164
179
|
self.propertyName = this.translateIdentifier(imported, self);
|
|
@@ -220,7 +235,7 @@ export class TypeScriptAstTranslator {
|
|
|
220
235
|
pos,
|
|
221
236
|
end,
|
|
222
237
|
parent,
|
|
223
|
-
expression:
|
|
238
|
+
expression: PENDING_NODE,
|
|
224
239
|
arguments: [],
|
|
225
240
|
};
|
|
226
241
|
self.expression = this.translateNode(callee, self);
|
|
@@ -238,8 +253,8 @@ export class TypeScriptAstTranslator {
|
|
|
238
253
|
pos,
|
|
239
254
|
end,
|
|
240
255
|
parent,
|
|
241
|
-
expression:
|
|
242
|
-
name:
|
|
256
|
+
expression: PENDING_NODE,
|
|
257
|
+
name: PENDING_IDENTIFIER,
|
|
243
258
|
};
|
|
244
259
|
self.expression = this.translateNode(object, self);
|
|
245
260
|
self.name = this.translateNode(property, self);
|
|
@@ -273,8 +288,8 @@ export class TypeScriptAstTranslator {
|
|
|
273
288
|
pos,
|
|
274
289
|
end,
|
|
275
290
|
parent,
|
|
276
|
-
name:
|
|
277
|
-
initializer:
|
|
291
|
+
name: PENDING_NODE,
|
|
292
|
+
initializer: PENDING_NODE,
|
|
278
293
|
};
|
|
279
294
|
self.name = this.translateNode(key, self);
|
|
280
295
|
self.initializer = this.translateNode(value, self);
|
|
@@ -305,7 +320,7 @@ export class TypeScriptAstTranslator {
|
|
|
305
320
|
pos,
|
|
306
321
|
end,
|
|
307
322
|
parent,
|
|
308
|
-
expression:
|
|
323
|
+
expression: PENDING_NODE,
|
|
309
324
|
};
|
|
310
325
|
self.expression = this.translateNode(argument, self);
|
|
311
326
|
return self;
|
|
@@ -317,7 +332,7 @@ export class TypeScriptAstTranslator {
|
|
|
317
332
|
pos,
|
|
318
333
|
end,
|
|
319
334
|
parent,
|
|
320
|
-
expression:
|
|
335
|
+
expression: PENDING_NODE,
|
|
321
336
|
};
|
|
322
337
|
self.expression = this.translateNode(expression, self);
|
|
323
338
|
return self;
|
|
@@ -329,7 +344,7 @@ export class TypeScriptAstTranslator {
|
|
|
329
344
|
pos,
|
|
330
345
|
end,
|
|
331
346
|
parent,
|
|
332
|
-
expression:
|
|
347
|
+
expression: PENDING_NODE,
|
|
333
348
|
};
|
|
334
349
|
self.expression = this.translateNode(expression, self);
|
|
335
350
|
return self;
|
|
@@ -341,7 +356,7 @@ export class TypeScriptAstTranslator {
|
|
|
341
356
|
pos,
|
|
342
357
|
end,
|
|
343
358
|
parent,
|
|
344
|
-
expression:
|
|
359
|
+
expression: PENDING_NODE,
|
|
345
360
|
};
|
|
346
361
|
self.expression = this.translateNode(expression, self);
|
|
347
362
|
return self;
|
|
@@ -353,7 +368,7 @@ export class TypeScriptAstTranslator {
|
|
|
353
368
|
pos,
|
|
354
369
|
end,
|
|
355
370
|
parent,
|
|
356
|
-
expression:
|
|
371
|
+
expression: PENDING_NODE,
|
|
357
372
|
};
|
|
358
373
|
self.expression = this.translateNode(expression, self);
|
|
359
374
|
return self;
|
|
@@ -367,9 +382,9 @@ export class TypeScriptAstTranslator {
|
|
|
367
382
|
pos,
|
|
368
383
|
end,
|
|
369
384
|
parent,
|
|
370
|
-
condition:
|
|
371
|
-
whenTrue:
|
|
372
|
-
whenFalse:
|
|
385
|
+
condition: PENDING_NODE,
|
|
386
|
+
whenTrue: PENDING_NODE,
|
|
387
|
+
whenFalse: PENDING_NODE,
|
|
373
388
|
};
|
|
374
389
|
self.condition = this.translateNode(test, self);
|
|
375
390
|
self.whenTrue = this.translateNode(consequent, self);
|
|
@@ -384,9 +399,9 @@ export class TypeScriptAstTranslator {
|
|
|
384
399
|
pos,
|
|
385
400
|
end,
|
|
386
401
|
parent,
|
|
387
|
-
left:
|
|
402
|
+
left: PENDING_NODE,
|
|
388
403
|
operator: this.mapBinaryOperator(node.operator),
|
|
389
|
-
right:
|
|
404
|
+
right: PENDING_NODE,
|
|
390
405
|
};
|
|
391
406
|
self.left = this.translateNode(left, self);
|
|
392
407
|
self.right = this.translateNode(right, self);
|
|
@@ -399,7 +414,7 @@ export class TypeScriptAstTranslator {
|
|
|
399
414
|
pos,
|
|
400
415
|
end,
|
|
401
416
|
parent,
|
|
402
|
-
expression:
|
|
417
|
+
expression: PENDING_NODE,
|
|
403
418
|
};
|
|
404
419
|
self.expression = this.translateNode(expression, self);
|
|
405
420
|
return self;
|
|
@@ -412,7 +427,7 @@ export class TypeScriptAstTranslator {
|
|
|
412
427
|
pos,
|
|
413
428
|
end,
|
|
414
429
|
parent,
|
|
415
|
-
name:
|
|
430
|
+
name: PENDING_NODE,
|
|
416
431
|
initializer: undefined,
|
|
417
432
|
};
|
|
418
433
|
self.name = this.translateNode(name, self);
|
|
@@ -442,8 +457,7 @@ export class TypeScriptAstTranslator {
|
|
|
442
457
|
parseDomainSourceFile(filePath, sourceText) {
|
|
443
458
|
// Best-effort parse to mirror `ts.createSourceFile` leniency — recoverable syntax
|
|
444
459
|
// errors still yield a usable tree, so `parseSync` errors are intentionally ignored.
|
|
445
|
-
const
|
|
446
|
-
const statements = program.body.map((statement) => this.translateNode(statement, null));
|
|
460
|
+
const statements = programStatements(parseSync(filePath, sourceText).program).map((statement) => this.translateNode(statement, null));
|
|
447
461
|
return {
|
|
448
462
|
fileName: filePath,
|
|
449
463
|
text: sourceText,
|
|
@@ -6,7 +6,7 @@ import type { Filesystem } from "#core/filesystem/filesystem";
|
|
|
6
6
|
*
|
|
7
7
|
* @since 0.3.16-canary.0
|
|
8
8
|
*/
|
|
9
|
-
export declare function processArrangeSimplifyFile(fs: Filesystem, args: {
|
|
9
|
+
export declare function processArrangeSimplifyFile(fs: Pick<Filesystem, "readFileSync" | "writeFileSync">, args: {
|
|
10
10
|
readonly filePath: string;
|
|
11
11
|
readonly write: boolean;
|
|
12
12
|
readonly probe?: VariantClassNameProbe | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AssertionAuditResult } from "#audit/domain/types";
|
|
2
|
+
/**
|
|
3
|
+
* Exit `1` when any non-allowlisted type-assertion violation remains.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.13.0
|
|
6
|
+
*/
|
|
7
|
+
export declare function exitCodeForAssertionAuditResult(result: AssertionAuditResult): number;
|
|
8
|
+
/**
|
|
9
|
+
* Machine-readable type-assertion summary for `--json`.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.13.0
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatAssertionAuditJsonOutput(result: AssertionAuditResult, 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 type-assertion violation remains.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.13.0
|
|
6
|
+
*/
|
|
7
|
+
export function exitCodeForAssertionAuditResult(result) {
|
|
8
|
+
return result.violationCount > 0 ? CLI_EXIT_GENERAL_ERROR : CLI_EXIT_SUCCESS;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Machine-readable type-assertion summary for `--json`.
|
|
12
|
+
*
|
|
13
|
+
* @since 0.13.0
|
|
14
|
+
*/
|
|
15
|
+
export function formatAssertionAuditJsonOutput(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 type-assertion audit run.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.13.0
|
|
6
|
+
*/
|
|
7
|
+
export type AssertionAuditRunRequest = {
|
|
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 AssertionAuditRunRequest}.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.13.0
|
|
17
|
+
*/
|
|
18
|
+
export declare const assertionAuditRunRequestSchema: z.ZodType<AssertionAuditRunRequest>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for {@link AssertionAuditRunRequest}.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.13.0
|
|
6
|
+
*/
|
|
7
|
+
export const assertionAuditRunRequestSchema = 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,20 @@
|
|
|
1
|
+
import type { AssertionViolation } from "#audit/domain/types";
|
|
2
|
+
/**
|
|
3
|
+
* The comment that keeps one double assertion, with the reason it has to stay.
|
|
4
|
+
*
|
|
5
|
+
* @remarks It covers an assertion on its own line or the line below it, and must carry a reason
|
|
6
|
+
* after the colon; one that covers nothing is reported, so a kept assertion cannot outlive its cause.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.13.0
|
|
9
|
+
*/
|
|
10
|
+
export declare const DOUBLE_ASSERTION_DIRECTIVE = "codefast-allow-double-assertion";
|
|
11
|
+
/**
|
|
12
|
+
* Scans one TypeScript source for double assertions through `unknown` or `any`, and for directives
|
|
13
|
+
* that keep none or give no reason.
|
|
14
|
+
*
|
|
15
|
+
* @remarks `x as unknown as T`, `(x as unknown) as T` and `<T><unknown>x` are one shape: the
|
|
16
|
+
* compiler found the two types unrelated, and the pair silences it instead of reconciling them.
|
|
17
|
+
*
|
|
18
|
+
* @since 0.13.0
|
|
19
|
+
*/
|
|
20
|
+
export declare function auditDoubleAssertionSource(filePath: string, sourceText: string): Array<AssertionViolation>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { parseSync } from "oxc-parser";
|
|
2
|
+
import { isOxcNode } from "#core/oxc-node";
|
|
3
|
+
import { firstLineOf, lineOfOffset } from "#core/source-position";
|
|
4
|
+
/**
|
|
5
|
+
* The comment that keeps one double assertion, with the reason it has to stay.
|
|
6
|
+
*
|
|
7
|
+
* @remarks It covers an assertion on its own line or the line below it, and must carry a reason
|
|
8
|
+
* after the colon; one that covers nothing is reported, so a kept assertion cannot outlive its cause.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.13.0
|
|
11
|
+
*/
|
|
12
|
+
export const DOUBLE_ASSERTION_DIRECTIVE = "codefast-allow-double-assertion";
|
|
13
|
+
const DOUBLE_ASSERTION_REASON = "double assertion through `unknown`/`any` — make the types agree, narrow with a type guard, or keep it " +
|
|
14
|
+
`with // ${DOUBLE_ASSERTION_DIRECTIVE}: <reason>`;
|
|
15
|
+
// Most files hold neither, so a text probe spares them the parse.
|
|
16
|
+
const CANDIDATE_TEXT = new RegExp(`\\bas\\s+(?:unknown|any)\\b|<(?:unknown|any)>|${DOUBLE_ASSERTION_DIRECTIVE}`);
|
|
17
|
+
function isAssertion(node) {
|
|
18
|
+
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
19
|
+
}
|
|
20
|
+
/** The assertion an outer one wraps, through any parentheses, when it erases to `unknown` or `any`. */
|
|
21
|
+
function erasingInnerAssertion(outer) {
|
|
22
|
+
let inner = outer.expression;
|
|
23
|
+
while (isOxcNode(inner) && inner.type === "ParenthesizedExpression") {
|
|
24
|
+
inner = inner.expression;
|
|
25
|
+
}
|
|
26
|
+
if (!isOxcNode(inner) || !isAssertion(inner) || !isOxcNode(inner.typeAnnotation)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const erasedTo = inner.typeAnnotation.type;
|
|
30
|
+
return erasedTo === "TSUnknownKeyword" || erasedTo === "TSAnyKeyword" ? inner : undefined;
|
|
31
|
+
}
|
|
32
|
+
function collectDoubleAssertions(node, found) {
|
|
33
|
+
let next = node;
|
|
34
|
+
if (isAssertion(node)) {
|
|
35
|
+
const inner = erasingInnerAssertion(node);
|
|
36
|
+
if (inner !== undefined) {
|
|
37
|
+
found.push(node);
|
|
38
|
+
// The erased pair is one finding; what it wraps is walked on its own.
|
|
39
|
+
next = inner;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const value of Object.values(next)) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
for (const item of value) {
|
|
45
|
+
if (isOxcNode(item)) {
|
|
46
|
+
collectDoubleAssertions(item, found);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (isOxcNode(value)) {
|
|
51
|
+
collectDoubleAssertions(value, found);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function parseDirective(sourceText, comment) {
|
|
56
|
+
if (comment.type !== "Line") {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const text = comment.value.trim();
|
|
60
|
+
if (!text.startsWith(DOUBLE_ASSERTION_DIRECTIVE)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const reason = /^:\s*(\S.*)$/.exec(text.slice(DOUBLE_ASSERTION_DIRECTIVE.length))?.[1] ?? "";
|
|
64
|
+
return {
|
|
65
|
+
line: lineOfOffset(sourceText, comment.start),
|
|
66
|
+
raw: sourceText.slice(comment.start, comment.end),
|
|
67
|
+
reason,
|
|
68
|
+
isUsed: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Scans one TypeScript source for double assertions through `unknown` or `any`, and for directives
|
|
73
|
+
* that keep none or give no reason.
|
|
74
|
+
*
|
|
75
|
+
* @remarks `x as unknown as T`, `(x as unknown) as T` and `<T><unknown>x` are one shape: the
|
|
76
|
+
* compiler found the two types unrelated, and the pair silences it instead of reconciling them.
|
|
77
|
+
*
|
|
78
|
+
* @since 0.13.0
|
|
79
|
+
*/
|
|
80
|
+
export function auditDoubleAssertionSource(filePath, sourceText) {
|
|
81
|
+
if (!CANDIDATE_TEXT.test(sourceText)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const { program, comments } = parseSync(filePath, sourceText);
|
|
85
|
+
const directives = comments.map((comment) => parseDirective(sourceText, comment)).filter((d) => d !== undefined);
|
|
86
|
+
const found = [];
|
|
87
|
+
if (isOxcNode(program)) {
|
|
88
|
+
collectDoubleAssertions(program, found);
|
|
89
|
+
}
|
|
90
|
+
const violations = [];
|
|
91
|
+
for (const assertion of found) {
|
|
92
|
+
const line = lineOfOffset(sourceText, assertion.start);
|
|
93
|
+
const directive = directives.find((candidate) => candidate.reason !== "" && (candidate.line === line || candidate.line === line - 1));
|
|
94
|
+
if (directive !== undefined) {
|
|
95
|
+
directive.isUsed = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
violations.push({
|
|
99
|
+
line,
|
|
100
|
+
raw: firstLineOf(sourceText.slice(assertion.start, assertion.end)),
|
|
101
|
+
reason: DOUBLE_ASSERTION_REASON,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (const directive of directives) {
|
|
105
|
+
if (directive.reason === "") {
|
|
106
|
+
violations.push({
|
|
107
|
+
line: directive.line,
|
|
108
|
+
raw: directive.raw,
|
|
109
|
+
reason: `a ${DOUBLE_ASSERTION_DIRECTIVE} directive states why after a colon`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (!directive.isUsed) {
|
|
113
|
+
violations.push({
|
|
114
|
+
line: directive.line,
|
|
115
|
+
raw: directive.raw,
|
|
116
|
+
reason: `this ${DOUBLE_ASSERTION_DIRECTIVE} directive keeps no double assertion — remove it`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
violations.sort((a, b) => a.line - b.line);
|
|
121
|
+
return violations;
|
|
122
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { logger } from "#core/logger";
|
|
2
|
+
/**
|
|
3
|
+
* Human-readable type-assertion report.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.13.0
|
|
6
|
+
*/
|
|
7
|
+
export function presentAssertionAuditResult(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} type-assertion violation(s)${allowlistSuffix}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
logger.out(`✓ No double assertions across ${result.scannedFileCount} file(s)${allowlistSuffix}`);
|
|
20
|
+
}
|
|
21
|
+
}
|