@konvert7/klint 0.20.0 → 0.22.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/README.md +11 -5
- package/cli.ts +113 -34
- package/package.json +7 -5
- package/rules/prefer-nullish-coalescing-assign.ts +48 -10
- package/tools/prepare-python-release.ts +76 -0
package/README.md
CHANGED
|
@@ -127,14 +127,14 @@ Maintainer migration notes live in [`docs/rust-engine.md`](docs/rust-engine.md).
|
|
|
127
127
|
| `ts` | Runs the full TypeScript implementation. This is the default. | Maximum compatibility |
|
|
128
128
|
| `rust` | Runs only rules supported by the Rust engine. Unsupported TS-only rules are rejected. | Strict Rust smoke tests and native-engine debugging |
|
|
129
129
|
| `compare` | Runs TS and Rust on the same supported config and fails if JSON output differs. Requires `--json`. | Parity verification while porting rules |
|
|
130
|
-
| `auto` | Runs Rust-supported rules in Rust, TS-only rules in TypeScript, then merges output. | Recommended experimental migration path |
|
|
130
|
+
| `auto` | Runs Rust-supported rules in Rust, TS-only rules and unsupported plugin defaults in TypeScript, then merges output. | Recommended experimental migration path |
|
|
131
131
|
|
|
132
132
|
Examples:
|
|
133
133
|
|
|
134
134
|
```sh
|
|
135
135
|
klint --engine auto
|
|
136
|
-
klint --engine rust --json # strict Rust-only run; rejects unsupported
|
|
137
|
-
klint --engine compare --json # parity check; rejects unsupported
|
|
136
|
+
klint --engine rust --json # strict Rust-only run; rejects unsupported rules
|
|
137
|
+
klint --engine compare --json # parity check; rejects unsupported rules
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
Rust currently supports:
|
|
@@ -145,6 +145,12 @@ Rust currently supports:
|
|
|
145
145
|
- `no-nested-template-literals`
|
|
146
146
|
- `no-consecutive-array-push`
|
|
147
147
|
- `no-string-match`
|
|
148
|
+
- `sonar/no-single-char-class`
|
|
149
|
+
- `sonar/prefer-at`
|
|
150
|
+
- `sonar/prefer-nullish-coalescing-assign`
|
|
151
|
+
- `sonar/prefer-string-replaceall`
|
|
152
|
+
- `sonar/prefer-string-raw`
|
|
153
|
+
- `sonar/prefer-string-raw-regexp`
|
|
148
154
|
|
|
149
155
|
These rules remain TypeScript-owned because they need TypeScript semantic information:
|
|
150
156
|
|
|
@@ -281,8 +287,8 @@ arch:
|
|
|
281
287
|
| `no-sync-in-async` | No | Sync filesystem calls inside async functions |
|
|
282
288
|
| `no-floating-promise` | Yes | Promise-returning call whose result is discarded |
|
|
283
289
|
| `no-misused-promises` | Yes | Async function passed where a sync callback is expected |
|
|
284
|
-
| `no-async-predicate` |
|
|
285
|
-
| `no-date-equality` |
|
|
290
|
+
| `no-async-predicate` | Yes | Async predicate passed to array filtering/search methods |
|
|
291
|
+
| `no-date-equality` | Yes | Date values compared by object identity |
|
|
286
292
|
| `no-optional-chain-on-non-nullable` | Yes | Optional chaining on values TypeScript knows are non-nullable |
|
|
287
293
|
| `no-object-in-template` | Yes | Object values interpolated into template strings |
|
|
288
294
|
| `no-nested-template-literals` | No | Template literals nested inside template literals |
|
package/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { type SpawnSyncReturns, spawnSync } from "node:child_process";
|
|
3
3
|
import {
|
|
4
4
|
cpSync,
|
|
5
5
|
existsSync,
|
|
@@ -27,12 +27,18 @@ import type {
|
|
|
27
27
|
import { BUILT_IN_PLUGINS } from "./plugins/index";
|
|
28
28
|
import { BUILT_IN_RULES } from "./rules/index";
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const RUST_SUPPORTED_RULES = new Set([
|
|
31
31
|
"no-consecutive-array-push",
|
|
32
32
|
"no-nested-template-literals",
|
|
33
33
|
"no-string-match",
|
|
34
34
|
"no-sync-in-async",
|
|
35
35
|
"no-unguarded-json-parse",
|
|
36
|
+
"sonar/no-single-char-class",
|
|
37
|
+
"sonar/prefer-at",
|
|
38
|
+
"sonar/prefer-nullish-coalescing-assign",
|
|
39
|
+
"sonar/prefer-string-replaceall",
|
|
40
|
+
"sonar/prefer-string-raw",
|
|
41
|
+
"sonar/prefer-string-raw-regexp",
|
|
36
42
|
]);
|
|
37
43
|
|
|
38
44
|
interface CliOptions {
|
|
@@ -106,7 +112,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
106
112
|
const root = resolve(configDir, raw.root ?? ".");
|
|
107
113
|
|
|
108
114
|
if (engine === "rust") {
|
|
109
|
-
runRustEngine({
|
|
115
|
+
runRustEngine({ fix, json, raw, root, rulesFile, startedAt });
|
|
110
116
|
return;
|
|
111
117
|
}
|
|
112
118
|
if (
|
|
@@ -163,7 +169,14 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
163
169
|
);
|
|
164
170
|
|
|
165
171
|
if (engine === "compare") {
|
|
166
|
-
runCompareEngine({
|
|
172
|
+
runCompareEngine({
|
|
173
|
+
fix,
|
|
174
|
+
json,
|
|
175
|
+
raw,
|
|
176
|
+
root,
|
|
177
|
+
rulesFile,
|
|
178
|
+
tsViolations: violations,
|
|
179
|
+
});
|
|
167
180
|
return;
|
|
168
181
|
}
|
|
169
182
|
|
|
@@ -246,14 +259,13 @@ function writeTextOutput(
|
|
|
246
259
|
}
|
|
247
260
|
|
|
248
261
|
function runRustEngine({
|
|
249
|
-
configDir,
|
|
250
262
|
fix,
|
|
251
263
|
json,
|
|
252
264
|
raw,
|
|
265
|
+
root,
|
|
253
266
|
rulesFile,
|
|
254
267
|
startedAt,
|
|
255
268
|
}: {
|
|
256
|
-
configDir: string;
|
|
257
269
|
fix: boolean;
|
|
258
270
|
json: boolean;
|
|
259
271
|
raw: {
|
|
@@ -261,6 +273,7 @@ function runRustEngine({
|
|
|
261
273
|
rules?: Record<string, RuleConfigValue>;
|
|
262
274
|
arch?: unknown;
|
|
263
275
|
};
|
|
276
|
+
root: string;
|
|
264
277
|
rulesFile?: string;
|
|
265
278
|
startedAt: number;
|
|
266
279
|
}): void {
|
|
@@ -275,16 +288,22 @@ function runRustEngine({
|
|
|
275
288
|
process.exit(1);
|
|
276
289
|
}
|
|
277
290
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
)
|
|
291
|
+
const { configDir: rustConfigDir, cleanup } = writeRustEngineConfig({ raw, root });
|
|
292
|
+
const command = resolveRustEngineCommand(rustConfigDir);
|
|
293
|
+
let result: SpawnSyncReturns<string>;
|
|
294
|
+
try {
|
|
295
|
+
if (process.argv.includes("--debug") || process.argv.includes("-debug")) {
|
|
296
|
+
process.stderr.write(
|
|
297
|
+
`[klint:debug] rust engine: ${command.bin} ${command.args.join(" ")}\n`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
result = spawnSync(command.bin, command.args, {
|
|
301
|
+
cwd: import.meta.dir,
|
|
302
|
+
encoding: "utf-8",
|
|
303
|
+
});
|
|
304
|
+
} finally {
|
|
305
|
+
cleanup();
|
|
283
306
|
}
|
|
284
|
-
const result = spawnSync(command.bin, command.args, {
|
|
285
|
-
cwd: import.meta.dir,
|
|
286
|
-
encoding: "utf-8",
|
|
287
|
-
});
|
|
288
307
|
|
|
289
308
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
290
309
|
if (json) {
|
|
@@ -301,14 +320,13 @@ function runRustEngine({
|
|
|
301
320
|
}
|
|
302
321
|
|
|
303
322
|
function runCompareEngine({
|
|
304
|
-
configDir,
|
|
305
323
|
fix,
|
|
306
324
|
json,
|
|
307
325
|
raw,
|
|
326
|
+
root,
|
|
308
327
|
rulesFile,
|
|
309
328
|
tsViolations,
|
|
310
329
|
}: {
|
|
311
|
-
configDir: string;
|
|
312
330
|
fix: boolean;
|
|
313
331
|
json: boolean;
|
|
314
332
|
raw: {
|
|
@@ -316,6 +334,7 @@ function runCompareEngine({
|
|
|
316
334
|
rules?: Record<string, RuleConfigValue>;
|
|
317
335
|
arch?: unknown;
|
|
318
336
|
};
|
|
337
|
+
root: string;
|
|
319
338
|
rulesFile?: string;
|
|
320
339
|
tsViolations: ReturnType<typeof runKlint>;
|
|
321
340
|
}): void {
|
|
@@ -336,11 +355,17 @@ function runCompareEngine({
|
|
|
336
355
|
}
|
|
337
356
|
|
|
338
357
|
const tsResult = toJsonCliResult(tsViolations);
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
358
|
+
const { configDir: rustConfigDir, cleanup } = writeRustEngineConfig({ raw, root });
|
|
359
|
+
const rustCommand = resolveRustEngineCommand(rustConfigDir);
|
|
360
|
+
let rustResult: SpawnSyncReturns<string>;
|
|
361
|
+
try {
|
|
362
|
+
rustResult = spawnSync(rustCommand.bin, rustCommand.args, {
|
|
363
|
+
cwd: import.meta.dir,
|
|
364
|
+
encoding: "utf-8",
|
|
365
|
+
});
|
|
366
|
+
} finally {
|
|
367
|
+
cleanup();
|
|
368
|
+
}
|
|
344
369
|
|
|
345
370
|
const rustStatus = rustResult.status ?? 1;
|
|
346
371
|
if (rustResult.stderr) process.stderr.write(rustResult.stderr);
|
|
@@ -367,6 +392,35 @@ function runCompareEngine({
|
|
|
367
392
|
process.exit(tsResult.status);
|
|
368
393
|
}
|
|
369
394
|
|
|
395
|
+
function writeRustEngineConfig({
|
|
396
|
+
raw,
|
|
397
|
+
root,
|
|
398
|
+
}: {
|
|
399
|
+
raw: {
|
|
400
|
+
include?: string[];
|
|
401
|
+
plugins?: string[];
|
|
402
|
+
rules?: Record<string, RuleConfigValue>;
|
|
403
|
+
arch?: unknown;
|
|
404
|
+
};
|
|
405
|
+
root: string;
|
|
406
|
+
}): { configDir: string; cleanup: () => void } {
|
|
407
|
+
const tempDir = mkdtempSync(join(tmpdir(), "klint-rust-engine-"));
|
|
408
|
+
writeFileSync(
|
|
409
|
+
join(tempDir, "klint.config.json"),
|
|
410
|
+
JSON.stringify({
|
|
411
|
+
root,
|
|
412
|
+
include: raw.include ?? ["."],
|
|
413
|
+
plugins: [],
|
|
414
|
+
rules: resolveEffectiveRules(raw.plugins, raw.rules ?? {}),
|
|
415
|
+
arch: raw.arch,
|
|
416
|
+
})
|
|
417
|
+
);
|
|
418
|
+
return {
|
|
419
|
+
configDir: tempDir,
|
|
420
|
+
cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
370
424
|
function runAutoEngine({
|
|
371
425
|
fix,
|
|
372
426
|
json,
|
|
@@ -401,12 +455,13 @@ function runAutoEngine({
|
|
|
401
455
|
process.exit(1);
|
|
402
456
|
}
|
|
403
457
|
|
|
404
|
-
const
|
|
458
|
+
const effectiveRules = resolveEffectiveRules(raw.plugins, raw.rules ?? {});
|
|
459
|
+
const { rustRules, tsRules } = splitRulesForAuto(effectiveRules);
|
|
405
460
|
const tsViolations = runKlint(
|
|
406
461
|
{
|
|
407
462
|
root,
|
|
408
463
|
include: raw.include ?? ["."],
|
|
409
|
-
plugins:
|
|
464
|
+
plugins: [],
|
|
410
465
|
rules: { ...customRulesMap, ...tsRules },
|
|
411
466
|
},
|
|
412
467
|
customRules
|
|
@@ -434,7 +489,7 @@ function splitRulesForAuto(rules: Record<string, RuleConfigValue>): {
|
|
|
434
489
|
const tsRules: Record<string, RuleConfigValue> = {};
|
|
435
490
|
|
|
436
491
|
for (const [name, value] of Object.entries(rules)) {
|
|
437
|
-
if (
|
|
492
|
+
if (RUST_SUPPORTED_RULES.has(name)) rustRules[name] = value;
|
|
438
493
|
else tsRules[name] = value;
|
|
439
494
|
}
|
|
440
495
|
|
|
@@ -643,31 +698,51 @@ function rustEngineUnsupportedReason({
|
|
|
643
698
|
}): string | undefined {
|
|
644
699
|
if (fix) return "KLINT_ENGINE=rust does not support --fix";
|
|
645
700
|
if (rulesFile) return "KLINT_ENGINE=rust does not support --rules";
|
|
646
|
-
|
|
647
|
-
|
|
701
|
+
let effectiveRules: Record<string, RuleConfigValue>;
|
|
702
|
+
try {
|
|
703
|
+
effectiveRules = resolveEffectiveRules(raw.plugins, raw.rules ?? {});
|
|
704
|
+
} catch (error) {
|
|
705
|
+
return error instanceof Error
|
|
706
|
+
? error.message
|
|
707
|
+
: "KLINT_ENGINE=rust failed to load plugins";
|
|
708
|
+
}
|
|
709
|
+
const activeSupportedRules = Object.entries(effectiveRules).filter(
|
|
648
710
|
([name, value]) =>
|
|
649
|
-
ruleConfigSeverity(value) !== "off" &&
|
|
711
|
+
ruleConfigSeverity(value) !== "off" && RUST_SUPPORTED_RULES.has(name)
|
|
650
712
|
);
|
|
651
|
-
const unsupportedRules = Object.entries(
|
|
713
|
+
const unsupportedRules = Object.entries(effectiveRules)
|
|
652
714
|
.filter(
|
|
653
715
|
([name, value]) =>
|
|
654
|
-
ruleConfigSeverity(value) !== "off" && !
|
|
716
|
+
ruleConfigSeverity(value) !== "off" && !RUST_SUPPORTED_RULES.has(name)
|
|
655
717
|
)
|
|
656
718
|
.map(([name]) => name);
|
|
657
719
|
if (!raw.arch && activeSupportedRules.length === 0 && unsupportedRules.length === 0) {
|
|
658
|
-
return "KLINT_ENGINE=rust requires an arch config or supported
|
|
720
|
+
return "KLINT_ENGINE=rust requires an arch config or supported rule";
|
|
659
721
|
}
|
|
660
722
|
if (unsupportedRules.length > 0) {
|
|
661
723
|
return [
|
|
662
|
-
"Rust engine currently supports arch rules and selected
|
|
724
|
+
"Rust engine currently supports arch rules and selected rules only",
|
|
663
725
|
"",
|
|
664
|
-
"Unsupported
|
|
726
|
+
"Unsupported rules:",
|
|
665
727
|
...unsupportedRules.map((rule) => `- ${rule}`),
|
|
666
728
|
].join("\n");
|
|
667
729
|
}
|
|
668
730
|
return undefined;
|
|
669
731
|
}
|
|
670
732
|
|
|
733
|
+
function resolveEffectiveRules(
|
|
734
|
+
plugins: string[] | undefined,
|
|
735
|
+
rules: Record<string, RuleConfigValue>
|
|
736
|
+
): Record<string, RuleConfigValue> {
|
|
737
|
+
const pluginDefaults: Record<string, RuleConfigValue> = {};
|
|
738
|
+
for (const pluginName of plugins ?? []) {
|
|
739
|
+
const plugin = BUILT_IN_PLUGINS[pluginName];
|
|
740
|
+
if (!plugin) throw new Error(`Unknown klint plugin: "${pluginName}"`);
|
|
741
|
+
Object.assign(pluginDefaults, plugin.rules);
|
|
742
|
+
}
|
|
743
|
+
return { ...pluginDefaults, ...rules };
|
|
744
|
+
}
|
|
745
|
+
|
|
671
746
|
function ruleConfigSeverity(value: RuleConfigValue): string | undefined {
|
|
672
747
|
if (typeof value === "string") return value;
|
|
673
748
|
return value.severity;
|
|
@@ -686,7 +761,11 @@ function jsonPayloadEquals(a: unknown, b: unknown): boolean {
|
|
|
686
761
|
}
|
|
687
762
|
|
|
688
763
|
function canonicalJson(value: unknown): unknown {
|
|
689
|
-
if (Array.isArray(value))
|
|
764
|
+
if (Array.isArray(value)) {
|
|
765
|
+
return value
|
|
766
|
+
.map(canonicalJson)
|
|
767
|
+
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
768
|
+
}
|
|
690
769
|
if (!value || typeof value !== "object") return value;
|
|
691
770
|
return Object.fromEntries(
|
|
692
771
|
Object.entries(value)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konvert7/klint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Architecture-as-Code linter for TypeScript. Enforce layer boundaries, import rules, and singleton patterns in YAML.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
"pack:check": "bun tests/pack-check.ts",
|
|
41
41
|
"native:stage": "bun tests/stage-native-binary.ts",
|
|
42
42
|
"native:smoke": "bun tests/smoke-native-binary.ts",
|
|
43
|
+
"python:stage": "bun tests/stage-python-wheel.ts",
|
|
44
|
+
"python:smoke": "bun tests/smoke-python-wheel.ts",
|
|
43
45
|
"native:release:dry-run": "bun tools/prepare-native-release.ts --version 0.0.0-dry-run",
|
|
44
46
|
"check": "biome check",
|
|
45
47
|
"check-write": "biome check --write",
|
|
@@ -62,10 +64,10 @@
|
|
|
62
64
|
"zod": "^4.4.3"
|
|
63
65
|
},
|
|
64
66
|
"optionalDependencies": {
|
|
65
|
-
"@konvert7/klint-darwin-arm64": "0.
|
|
66
|
-
"@konvert7/klint-darwin-x64": "0.
|
|
67
|
-
"@konvert7/klint-linux-x64": "0.
|
|
68
|
-
"@konvert7/klint-win32-x64": "0.
|
|
67
|
+
"@konvert7/klint-darwin-arm64": "0.22.0",
|
|
68
|
+
"@konvert7/klint-darwin-x64": "0.22.0",
|
|
69
|
+
"@konvert7/klint-linux-x64": "0.22.0",
|
|
70
|
+
"@konvert7/klint-win32-x64": "0.22.0"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
71
73
|
"@biomejs/biome": "2.4.7",
|
|
@@ -6,25 +6,20 @@ import type { KlintRule } from "../core/types";
|
|
|
6
6
|
export const preferNullishCoalescingAssign: KlintRule = {
|
|
7
7
|
meta: {
|
|
8
8
|
description:
|
|
9
|
-
"Flags
|
|
9
|
+
"Flags explicit nullish assignment guards — `x ??= y` is the dedicated nullish assignment operator.",
|
|
10
10
|
examples: ["sonar/prefer-nullish-coalescing-assign: warn"],
|
|
11
11
|
},
|
|
12
12
|
check({ files, root, fileContents }, violations) {
|
|
13
13
|
for (const file of files) {
|
|
14
14
|
const content = fileContents.get(file) ?? "";
|
|
15
15
|
walkAst(file, content, (node, src) => {
|
|
16
|
-
if (
|
|
17
|
-
!ts.isIfStatement(node) ||
|
|
18
|
-
node.elseStatement !== undefined ||
|
|
19
|
-
!ts.isPrefixUnaryExpression(node.expression) ||
|
|
20
|
-
node.expression.operator !== ts.SyntaxKind.ExclamationToken
|
|
21
|
-
)
|
|
22
|
-
return;
|
|
16
|
+
if (!ts.isIfStatement(node) || node.elseStatement !== undefined) return;
|
|
23
17
|
|
|
24
18
|
const assignExpr = extractAssignment(node.thenStatement);
|
|
25
19
|
if (!assignExpr) return;
|
|
26
20
|
|
|
27
|
-
const xText = node.expression
|
|
21
|
+
const xText = nullishGuardTarget(node.expression, src);
|
|
22
|
+
if (!xText) return;
|
|
28
23
|
if (assignExpr.left.getText(src) !== xText) return;
|
|
29
24
|
|
|
30
25
|
const yText = assignExpr.right.getText(src);
|
|
@@ -35,7 +30,7 @@ export const preferNullishCoalescingAssign: KlintRule = {
|
|
|
35
30
|
violations.push({
|
|
36
31
|
file: relativeSlashPath(root, file),
|
|
37
32
|
line: s + 1,
|
|
38
|
-
message: `Prefer \`${xText} ??= ${yText}\` over
|
|
33
|
+
message: `Prefer \`${xText} ??= ${yText}\` over explicit nullish guard assignment — ??= only assigns when null or undefined.`,
|
|
39
34
|
fix: {
|
|
40
35
|
startLine: s + 1,
|
|
41
36
|
endLine: e + 1,
|
|
@@ -66,6 +61,49 @@ function isAssign(node: ts.Expression): node is ts.BinaryExpression {
|
|
|
66
61
|
);
|
|
67
62
|
}
|
|
68
63
|
|
|
64
|
+
function nullishGuardTarget(
|
|
65
|
+
expression: ts.Expression,
|
|
66
|
+
src: ts.SourceFile
|
|
67
|
+
): string | undefined {
|
|
68
|
+
if (!ts.isBinaryExpression(expression)) return undefined;
|
|
69
|
+
|
|
70
|
+
const direct = equalityNullishTarget(expression, src);
|
|
71
|
+
if (direct) return direct.target;
|
|
72
|
+
|
|
73
|
+
if (expression.operatorToken.kind !== ts.SyntaxKind.BarBarToken) return undefined;
|
|
74
|
+
|
|
75
|
+
const left = equalityNullishTarget(expression.left, src);
|
|
76
|
+
const right = equalityNullishTarget(expression.right, src);
|
|
77
|
+
if (!left || !right || left.target !== right.target) return undefined;
|
|
78
|
+
|
|
79
|
+
const values = new Set([left.value, right.value]);
|
|
80
|
+
if (values.has("null") && values.has("undefined")) return left.target;
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function equalityNullishTarget(
|
|
85
|
+
expression: ts.Expression,
|
|
86
|
+
src: ts.SourceFile
|
|
87
|
+
): { target: string; value: "null" | "undefined" } | undefined {
|
|
88
|
+
if (!ts.isBinaryExpression(expression)) return undefined;
|
|
89
|
+
|
|
90
|
+
const operator = expression.operatorToken.kind;
|
|
91
|
+
if (
|
|
92
|
+
operator !== ts.SyntaxKind.EqualsEqualsToken &&
|
|
93
|
+
operator !== ts.SyntaxKind.EqualsEqualsEqualsToken
|
|
94
|
+
)
|
|
95
|
+
return undefined;
|
|
96
|
+
|
|
97
|
+
const left = expression.left.getText(src);
|
|
98
|
+
const right = expression.right.getText(src);
|
|
99
|
+
|
|
100
|
+
if (right === "null") return { target: left, value: "null" };
|
|
101
|
+
if (right === "undefined") return { target: left, value: "undefined" };
|
|
102
|
+
if (left === "null") return { target: right, value: "null" };
|
|
103
|
+
if (left === "undefined") return { target: right, value: "undefined" };
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
function getIndent(src: ts.SourceFile, node: ts.Node): string {
|
|
70
108
|
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
71
109
|
const lineStart = src.getPositionOfLineAndCharacter(line, 0);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
interface Options {
|
|
6
|
+
name: string;
|
|
7
|
+
root: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
12
|
+
const options = parseArgs();
|
|
13
|
+
const pythonRoot = join(options.root, "python");
|
|
14
|
+
const pyprojectPath = join(pythonRoot, "pyproject.toml");
|
|
15
|
+
const initPath = join(pythonRoot, "klint", "__init__.py");
|
|
16
|
+
|
|
17
|
+
const pyproject = readFileSync(pyprojectPath, "utf-8");
|
|
18
|
+
const nextPyproject = replaceRequired(
|
|
19
|
+
replaceRequired(pyproject, /^name = ".+"$/m, `name = "${options.name}"`, pyprojectPath),
|
|
20
|
+
/^version = ".+"$/m,
|
|
21
|
+
`version = "${options.version}"`,
|
|
22
|
+
pyprojectPath
|
|
23
|
+
);
|
|
24
|
+
writeFileSync(pyprojectPath, nextPyproject);
|
|
25
|
+
|
|
26
|
+
const init = readFileSync(initPath, "utf-8");
|
|
27
|
+
const nextInit = replaceRequired(
|
|
28
|
+
init,
|
|
29
|
+
/^__version__ = ".+"$/m,
|
|
30
|
+
`__version__ = "${options.version}"`,
|
|
31
|
+
initPath
|
|
32
|
+
);
|
|
33
|
+
writeFileSync(initPath, nextInit);
|
|
34
|
+
|
|
35
|
+
process.stdout.write(
|
|
36
|
+
`prepared Python release metadata: ${options.name}@${options.version}\n`
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function parseArgs(): Options {
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const name = valueAfter(args, "--name");
|
|
42
|
+
const version = valueAfter(args, "--version");
|
|
43
|
+
if (!name || !version) {
|
|
44
|
+
fail(
|
|
45
|
+
"Usage: bun tools/prepare-python-release.ts --name <package-name> --version <version> [--root <repo-root>]"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
version,
|
|
52
|
+
root: resolve(valueAfter(args, "--root") ?? repoRoot),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function replaceRequired(
|
|
57
|
+
input: string,
|
|
58
|
+
pattern: RegExp,
|
|
59
|
+
replacement: string,
|
|
60
|
+
path: string
|
|
61
|
+
): string {
|
|
62
|
+
if (!pattern.test(input)) {
|
|
63
|
+
fail(`Expected release metadata pattern ${pattern} in ${path}`);
|
|
64
|
+
}
|
|
65
|
+
return input.replace(pattern, replacement);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function valueAfter(args: string[], name: string): string | undefined {
|
|
69
|
+
const index = args.indexOf(name);
|
|
70
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fail(message: string): never {
|
|
74
|
+
process.stderr.write(`${message}\n`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|