redscript-mc 3.0.1 → 3.0.2
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/.github/workflows/ci.yml +1 -0
- package/README.md +119 -313
- package/README.zh.md +118 -314
- package/ROADMAP.md +5 -5
- package/dist/data/impl_test/function/counter/get.mcfunction +5 -0
- package/dist/data/impl_test/function/counter/inc.mcfunction +7 -0
- package/dist/data/impl_test/function/counter/new.mcfunction +4 -0
- package/dist/data/impl_test/function/load.mcfunction +1 -0
- package/dist/data/impl_test/function/test_impl.mcfunction +10 -0
- package/dist/data/minecraft/tags/function/load.json +5 -0
- package/dist/data/playground/function/load.mcfunction +1 -0
- package/dist/data/playground/function/start.mcfunction +4 -0
- package/dist/data/playground/function/start__say_macro_t1.mcfunction +1 -0
- package/dist/data/playground/function/stop.mcfunction +5 -0
- package/dist/data/playground/function/stop__say_macro_t0.mcfunction +1 -0
- package/dist/data/stdlib_queue8_test/function/__queue_append_apply.mcfunction +4 -0
- package/dist/data/stdlib_queue8_test/function/__queue_peek_apply.mcfunction +4 -0
- package/dist/data/stdlib_queue8_test/function/__queue_size_raw_apply.mcfunction +4 -0
- package/dist/data/stdlib_queue8_test/function/load.mcfunction +1 -0
- package/dist/data/stdlib_queue8_test/function/queue_clear.mcfunction +6 -0
- package/dist/data/stdlib_queue8_test/function/queue_empty__merge_1.mcfunction +5 -0
- package/dist/data/stdlib_queue8_test/function/queue_empty__then_0.mcfunction +5 -0
- package/dist/data/stdlib_queue8_test/function/queue_peek__merge_1.mcfunction +13 -0
- package/dist/data/stdlib_queue8_test/function/queue_peek__then_0.mcfunction +5 -0
- package/dist/data/stdlib_queue8_test/function/queue_pop__merge_1.mcfunction +15 -0
- package/dist/data/stdlib_queue8_test/function/queue_pop__then_0.mcfunction +5 -0
- package/dist/data/stdlib_queue8_test/function/queue_push__const_11.mcfunction +6 -0
- package/dist/data/stdlib_queue8_test/function/queue_push__const_22.mcfunction +6 -0
- package/dist/data/stdlib_queue8_test/function/queue_size.mcfunction +13 -0
- package/dist/data/stdlib_queue8_test/function/test_queue_push_and_size.mcfunction +13 -0
- package/dist/data/test/function/load.mcfunction +1 -0
- package/dist/data/test/function/say_at.mcfunction +6 -0
- package/dist/data/test/function/test.mcfunction +4 -0
- package/dist/pack.mcmeta +6 -0
- package/dist/package.json +1 -1
- package/dist/src/__tests__/formatter-extra.test.d.ts +7 -0
- package/dist/src/__tests__/formatter-extra.test.js +123 -0
- package/dist/src/__tests__/global-vars.test.d.ts +13 -0
- package/dist/src/__tests__/global-vars.test.js +156 -0
- package/dist/src/__tests__/lint/new-rules.test.d.ts +9 -0
- package/dist/src/__tests__/lint/new-rules.test.js +402 -0
- package/dist/src/__tests__/lsp-rename.test.d.ts +8 -0
- package/dist/src/__tests__/lsp-rename.test.js +157 -0
- package/dist/src/__tests__/mc-integration/say-fstring.test.d.ts +11 -0
- package/dist/src/__tests__/mc-integration/say-fstring.test.js +220 -0
- package/dist/src/__tests__/mc-integration/stdlib-coverage-2.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-3.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-4.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-5.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-6.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-7.test.js +1 -1
- package/dist/src/__tests__/mc-integration/stdlib-coverage-8.test.js +1 -1
- package/dist/src/__tests__/mc-syntax.test.js +4 -1
- package/dist/src/__tests__/monomorphize-coverage.test.d.ts +9 -0
- package/dist/src/__tests__/monomorphize-coverage.test.js +204 -0
- package/dist/src/__tests__/optimizer-cse.test.d.ts +7 -0
- package/dist/src/__tests__/optimizer-cse.test.js +226 -0
- package/dist/src/__tests__/parser.test.js +4 -13
- package/dist/src/__tests__/repl-server-extra.test.js +6 -7
- package/dist/src/__tests__/repl-server.test.js +5 -7
- package/dist/src/__tests__/stdlib/queue.test.js +6 -6
- package/dist/src/cli.js +0 -0
- package/dist/src/lexer/index.js +2 -1
- package/dist/src/lint/index.d.ts +12 -5
- package/dist/src/lint/index.js +730 -5
- package/dist/src/lsp/main.js +0 -0
- package/dist/src/mc-test/client.d.ts +21 -0
- package/dist/src/mc-test/client.js +34 -0
- package/dist/src/mir/lower.js +108 -6
- package/dist/src/optimizer/interprocedural.js +37 -2
- package/dist/src/parser/decl-parser.d.ts +19 -0
- package/dist/src/parser/decl-parser.js +323 -0
- package/dist/src/parser/expr-parser.d.ts +46 -0
- package/dist/src/parser/expr-parser.js +759 -0
- package/dist/src/parser/index.d.ts +8 -129
- package/dist/src/parser/index.js +13 -2262
- package/dist/src/parser/stmt-parser.d.ts +28 -0
- package/dist/src/parser/stmt-parser.js +577 -0
- package/dist/src/parser/type-parser.d.ts +20 -0
- package/dist/src/parser/type-parser.js +257 -0
- package/dist/src/parser/utils.d.ts +34 -0
- package/dist/src/parser/utils.js +141 -0
- package/docs/dev/README-mc-integration-tests.md +141 -0
- package/docs/lint-rules.md +162 -0
- package/docs/stdlib/bigint.md +2 -0
- package/editors/vscode/README.md +63 -41
- package/editors/vscode/out/extension.js +1881 -1776
- package/editors/vscode/out/lsp-server.js +4257 -3651
- package/editors/vscode/package-lock.json +3 -3
- package/editors/vscode/package.json +1 -1
- package/examples/loops-demo.mcrs +87 -0
- package/package.json +1 -1
- package/redscript-docs/docs/en/stdlib/advanced.md +629 -0
- package/redscript-docs/docs/en/stdlib/bigint.md +316 -0
- package/redscript-docs/docs/en/stdlib/bits.md +292 -0
- package/redscript-docs/docs/en/stdlib/bossbar.md +177 -0
- package/redscript-docs/docs/en/stdlib/calculus.md +289 -0
- package/redscript-docs/docs/en/stdlib/color.md +353 -0
- package/redscript-docs/docs/en/stdlib/combat.md +88 -0
- package/redscript-docs/docs/en/stdlib/cooldown.md +82 -0
- package/redscript-docs/docs/en/stdlib/dialog.md +155 -0
- package/redscript-docs/docs/en/stdlib/easing.md +558 -0
- package/redscript-docs/docs/en/stdlib/ecs.md +475 -0
- package/redscript-docs/docs/en/stdlib/effects.md +324 -0
- package/redscript-docs/docs/en/stdlib/events.md +3 -0
- package/redscript-docs/docs/en/stdlib/expr.md +45 -0
- package/redscript-docs/docs/en/stdlib/fft.md +141 -0
- package/redscript-docs/docs/en/stdlib/geometry.md +430 -0
- package/redscript-docs/docs/en/stdlib/graph.md +259 -0
- package/redscript-docs/docs/en/stdlib/heap.md +185 -0
- package/redscript-docs/docs/en/stdlib/interactions.md +179 -0
- package/redscript-docs/docs/en/stdlib/inventory.md +97 -0
- package/redscript-docs/docs/en/stdlib/linalg.md +557 -0
- package/redscript-docs/docs/en/stdlib/list.md +559 -0
- package/redscript-docs/docs/en/stdlib/map.md +140 -0
- package/redscript-docs/docs/en/stdlib/math.md +193 -0
- package/redscript-docs/docs/en/stdlib/math_hp.md +149 -0
- package/redscript-docs/docs/en/stdlib/matrix.md +403 -0
- package/redscript-docs/docs/en/stdlib/mobs.md +965 -0
- package/redscript-docs/docs/en/stdlib/noise.md +244 -0
- package/redscript-docs/docs/en/stdlib/ode.md +253 -0
- package/redscript-docs/docs/en/stdlib/parabola.md +342 -0
- package/redscript-docs/docs/en/stdlib/particles.md +311 -0
- package/redscript-docs/docs/en/stdlib/pathfind.md +255 -0
- package/redscript-docs/docs/en/stdlib/physics.md +493 -0
- package/redscript-docs/docs/en/stdlib/player.md +78 -0
- package/redscript-docs/docs/en/stdlib/quaternion.md +673 -0
- package/redscript-docs/docs/en/stdlib/queue.md +134 -0
- package/redscript-docs/docs/en/stdlib/random.md +223 -0
- package/redscript-docs/docs/en/stdlib/result.md +143 -0
- package/redscript-docs/docs/en/stdlib/scheduler.md +183 -0
- package/redscript-docs/docs/en/stdlib/set_int.md +190 -0
- package/redscript-docs/docs/en/stdlib/sets.md +101 -0
- package/redscript-docs/docs/en/stdlib/signal.md +400 -0
- package/redscript-docs/docs/en/stdlib/sort.md +104 -0
- package/redscript-docs/docs/en/stdlib/spawn.md +147 -0
- package/redscript-docs/docs/en/stdlib/state.md +142 -0
- package/redscript-docs/docs/en/stdlib/strings.md +154 -0
- package/redscript-docs/docs/en/stdlib/tags.md +3451 -0
- package/redscript-docs/docs/en/stdlib/teams.md +153 -0
- package/redscript-docs/docs/en/stdlib/timer.md +246 -0
- package/redscript-docs/docs/en/stdlib/vec.md +158 -0
- package/redscript-docs/docs/en/stdlib/world.md +298 -0
- package/redscript-docs/docs/zh/stdlib/advanced.md +615 -0
- package/redscript-docs/docs/zh/stdlib/bigint.md +316 -0
- package/redscript-docs/docs/zh/stdlib/bits.md +292 -0
- package/redscript-docs/docs/zh/stdlib/bossbar.md +170 -0
- package/redscript-docs/docs/zh/stdlib/calculus.md +287 -0
- package/redscript-docs/docs/zh/stdlib/color.md +353 -0
- package/redscript-docs/docs/zh/stdlib/combat.md +88 -0
- package/redscript-docs/docs/zh/stdlib/cooldown.md +84 -0
- package/redscript-docs/docs/zh/stdlib/dialog.md +152 -0
- package/redscript-docs/docs/zh/stdlib/easing.md +558 -0
- package/redscript-docs/docs/zh/stdlib/ecs.md +472 -0
- package/redscript-docs/docs/zh/stdlib/effects.md +324 -0
- package/redscript-docs/docs/zh/stdlib/events.md +3 -0
- package/redscript-docs/docs/zh/stdlib/expr.md +37 -0
- package/redscript-docs/docs/zh/stdlib/fft.md +128 -0
- package/redscript-docs/docs/zh/stdlib/geometry.md +430 -0
- package/redscript-docs/docs/zh/stdlib/graph.md +259 -0
- package/redscript-docs/docs/zh/stdlib/heap.md +185 -0
- package/redscript-docs/docs/zh/stdlib/interactions.md +160 -0
- package/redscript-docs/docs/zh/stdlib/inventory.md +94 -0
- package/redscript-docs/docs/zh/stdlib/linalg.md +543 -0
- package/redscript-docs/docs/zh/stdlib/list.md +561 -0
- package/redscript-docs/docs/zh/stdlib/map.md +132 -0
- package/redscript-docs/docs/zh/stdlib/math.md +193 -0
- package/redscript-docs/docs/zh/stdlib/math_hp.md +143 -0
- package/redscript-docs/docs/zh/stdlib/matrix.md +396 -0
- package/redscript-docs/docs/zh/stdlib/mobs.md +965 -0
- package/redscript-docs/docs/zh/stdlib/noise.md +244 -0
- package/redscript-docs/docs/zh/stdlib/ode.md +243 -0
- package/redscript-docs/docs/zh/stdlib/parabola.md +337 -0
- package/redscript-docs/docs/zh/stdlib/particles.md +307 -0
- package/redscript-docs/docs/zh/stdlib/pathfind.md +255 -0
- package/redscript-docs/docs/zh/stdlib/physics.md +493 -0
- package/redscript-docs/docs/zh/stdlib/player.md +78 -0
- package/redscript-docs/docs/zh/stdlib/quaternion.md +669 -0
- package/redscript-docs/docs/zh/stdlib/queue.md +124 -0
- package/redscript-docs/docs/zh/stdlib/random.md +222 -0
- package/redscript-docs/docs/zh/stdlib/result.md +147 -0
- package/redscript-docs/docs/zh/stdlib/scheduler.md +173 -0
- package/redscript-docs/docs/zh/stdlib/set_int.md +180 -0
- package/redscript-docs/docs/zh/stdlib/sets.md +107 -0
- package/redscript-docs/docs/zh/stdlib/signal.md +373 -0
- package/redscript-docs/docs/zh/stdlib/sort.md +104 -0
- package/redscript-docs/docs/zh/stdlib/spawn.md +142 -0
- package/redscript-docs/docs/zh/stdlib/state.md +134 -0
- package/redscript-docs/docs/zh/stdlib/strings.md +107 -0
- package/redscript-docs/docs/zh/stdlib/tags.md +3451 -0
- package/redscript-docs/docs/zh/stdlib/teams.md +150 -0
- package/redscript-docs/docs/zh/stdlib/timer.md +254 -0
- package/redscript-docs/docs/zh/stdlib/vec.md +158 -0
- package/redscript-docs/docs/zh/stdlib/world.md +289 -0
- package/src/__tests__/formatter-extra.test.ts +139 -0
- package/src/__tests__/global-vars.test.ts +171 -0
- package/src/__tests__/lint/new-rules.test.ts +437 -0
- package/src/__tests__/lsp-rename.test.ts +171 -0
- package/src/__tests__/mc-integration/say-fstring.test.ts +211 -0
- package/src/__tests__/mc-integration/stdlib-coverage-2.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-3.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-4.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-5.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-6.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-7.test.ts +1 -1
- package/src/__tests__/mc-integration/stdlib-coverage-8.test.ts +1 -1
- package/src/__tests__/mc-syntax.test.ts +3 -0
- package/src/__tests__/monomorphize-coverage.test.ts +220 -0
- package/src/__tests__/optimizer-cse.test.ts +250 -0
- package/src/__tests__/parser.test.ts +4 -13
- package/src/__tests__/repl-server-extra.test.ts +6 -6
- package/src/__tests__/repl-server.test.ts +5 -6
- package/src/__tests__/stdlib/queue.test.ts +6 -6
- package/src/lexer/index.ts +2 -1
- package/src/lint/index.ts +713 -5
- package/src/mc-test/client.ts +40 -0
- package/src/mir/lower.ts +111 -2
- package/src/optimizer/interprocedural.ts +40 -2
- package/src/parser/decl-parser.ts +349 -0
- package/src/parser/expr-parser.ts +838 -0
- package/src/parser/index.ts +17 -2558
- package/src/parser/stmt-parser.ts +585 -0
- package/src/parser/type-parser.ts +276 -0
- package/src/parser/utils.ts +173 -0
- package/src/stdlib/queue.mcrs +19 -6
package/src/lint/index.ts
CHANGED
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
* Run after HIR lowering, before MIR.
|
|
6
6
|
*
|
|
7
7
|
* Rules:
|
|
8
|
-
* unused-variable
|
|
9
|
-
* unused-import
|
|
10
|
-
* magic-number
|
|
11
|
-
* dead-branch
|
|
12
|
-
* function-too-long
|
|
8
|
+
* unused-variable — let x = 5 but x never read
|
|
9
|
+
* unused-import — import math::sin but sin never called
|
|
10
|
+
* magic-number — literal number > 1 used directly (0 and 1 ignored)
|
|
11
|
+
* dead-branch — if (const == const) always-true/false condition
|
|
12
|
+
* function-too-long — function body exceeds 50 lines
|
|
13
|
+
* no-dead-assignment — variable assigned but never read after the assignment
|
|
14
|
+
* prefer-match-exhaustive — Option match missing Some or None arm
|
|
15
|
+
* no-empty-catch — empty else branch in if_let_some (silent failure)
|
|
16
|
+
* naming-convention — variables must be camelCase; types must be PascalCase
|
|
17
|
+
* no-magic-numbers — any literal number other than 0 or 1 used in an expression
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
import type {
|
|
@@ -42,6 +47,8 @@ export interface LintOptions {
|
|
|
42
47
|
filePath?: string
|
|
43
48
|
/** Max function body lines before function-too-long fires (default: 50) */
|
|
44
49
|
maxFunctionLines?: number
|
|
50
|
+
/** Allowed literal number values for no-magic-numbers (default: [0, 1]) */
|
|
51
|
+
allowedNumbers?: number[]
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
// ---------------------------------------------------------------------------
|
|
@@ -61,6 +68,7 @@ export function lintSource(
|
|
|
61
68
|
const warnings: LintWarning[] = []
|
|
62
69
|
const file = options.filePath
|
|
63
70
|
const maxLines = options.maxFunctionLines ?? 50
|
|
71
|
+
const allowedNumbers = options.allowedNumbers ?? [0, 1]
|
|
64
72
|
|
|
65
73
|
// Rule: unused-import
|
|
66
74
|
warnings.push(...checkUnusedImports(imports, hir, file))
|
|
@@ -80,8 +88,26 @@ export function lintSource(
|
|
|
80
88
|
// Rule: function-too-long
|
|
81
89
|
const fnWarning = checkFunctionLength(fn, maxLines, file)
|
|
82
90
|
if (fnWarning) warnings.push(fnWarning)
|
|
91
|
+
|
|
92
|
+
// Rule: no-dead-assignment
|
|
93
|
+
warnings.push(...checkNoDeadAssignment(fn, file))
|
|
94
|
+
|
|
95
|
+
// Rule: prefer-match-exhaustive
|
|
96
|
+
warnings.push(...checkPreferMatchExhaustive(fn, file))
|
|
97
|
+
|
|
98
|
+
// Rule: no-empty-catch
|
|
99
|
+
warnings.push(...checkNoEmptyCatch(fn, file))
|
|
100
|
+
|
|
101
|
+
// Rule: naming-convention
|
|
102
|
+
warnings.push(...checkNamingConvention(fn, file))
|
|
103
|
+
|
|
104
|
+
// Rule: no-magic-numbers
|
|
105
|
+
warnings.push(...checkNoMagicNumbers(fn, allowedNumbers, file))
|
|
83
106
|
}
|
|
84
107
|
|
|
108
|
+
// Rule: naming-convention — type names in structs/enums
|
|
109
|
+
warnings.push(...checkNamingConventionModule(hir, file))
|
|
110
|
+
|
|
85
111
|
return warnings
|
|
86
112
|
}
|
|
87
113
|
|
|
@@ -920,3 +946,685 @@ function countStmts(block: HIRBlock): number {
|
|
|
920
946
|
}
|
|
921
947
|
return count
|
|
922
948
|
}
|
|
949
|
+
|
|
950
|
+
// ---------------------------------------------------------------------------
|
|
951
|
+
// Rule: no-dead-assignment
|
|
952
|
+
// ---------------------------------------------------------------------------
|
|
953
|
+
//
|
|
954
|
+
// Detects variables that are assigned (via `assign` expr) but whose value is
|
|
955
|
+
// never subsequently read. This is stricter than unused-variable: the variable
|
|
956
|
+
// IS read at some point, but a particular write is "dead" because it is
|
|
957
|
+
// overwritten before being read.
|
|
958
|
+
//
|
|
959
|
+
// Implementation: single-pass, scope-insensitive. We track every assignment
|
|
960
|
+
// target and warn if the same name is assigned twice with no intervening read.
|
|
961
|
+
|
|
962
|
+
function checkNoDeadAssignment(fn: HIRFunction, file?: string): LintWarning[] {
|
|
963
|
+
const warnings: LintWarning[] = []
|
|
964
|
+
// Map from name → span of the last unread assignment
|
|
965
|
+
const pendingWrite = new Map<string, { span?: Span }>()
|
|
966
|
+
noDeadAssignBlock(fn.body, pendingWrite, warnings, file)
|
|
967
|
+
return warnings
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function noDeadAssignBlock(
|
|
971
|
+
block: HIRBlock,
|
|
972
|
+
pending: Map<string, { span?: Span }>,
|
|
973
|
+
out: LintWarning[],
|
|
974
|
+
file: string | undefined,
|
|
975
|
+
): void {
|
|
976
|
+
for (const stmt of block) {
|
|
977
|
+
noDeadAssignStmt(stmt, pending, out, file)
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function noDeadAssignStmt(
|
|
982
|
+
stmt: HIRStmt,
|
|
983
|
+
pending: Map<string, { span?: Span }>,
|
|
984
|
+
out: LintWarning[],
|
|
985
|
+
file: string | undefined,
|
|
986
|
+
): void {
|
|
987
|
+
switch (stmt.kind) {
|
|
988
|
+
case 'let':
|
|
989
|
+
// Initial let binding — register as pending write
|
|
990
|
+
noDeadAssignExprReads(stmt.init, pending, out, file)
|
|
991
|
+
pending.set(stmt.name, { span: stmt.span })
|
|
992
|
+
break
|
|
993
|
+
case 'let_destruct':
|
|
994
|
+
noDeadAssignExprReads(stmt.init, pending, out, file)
|
|
995
|
+
for (const name of stmt.names) pending.set(name, { span: stmt.span })
|
|
996
|
+
break
|
|
997
|
+
case 'const_decl':
|
|
998
|
+
noDeadAssignExprReads(stmt.value, pending, out, file)
|
|
999
|
+
break
|
|
1000
|
+
case 'expr':
|
|
1001
|
+
noDeadAssignExprReads(stmt.expr, pending, out, file)
|
|
1002
|
+
break
|
|
1003
|
+
case 'return':
|
|
1004
|
+
if (stmt.value) noDeadAssignExprReads(stmt.value, pending, out, file)
|
|
1005
|
+
break
|
|
1006
|
+
case 'if':
|
|
1007
|
+
noDeadAssignExprReads(stmt.cond, pending, out, file)
|
|
1008
|
+
noDeadAssignBlock(stmt.then, pending, out, file)
|
|
1009
|
+
if (stmt.else_) noDeadAssignBlock(stmt.else_, pending, out, file)
|
|
1010
|
+
break
|
|
1011
|
+
case 'while':
|
|
1012
|
+
noDeadAssignExprReads(stmt.cond, pending, out, file)
|
|
1013
|
+
noDeadAssignBlock(stmt.body, pending, out, file)
|
|
1014
|
+
if (stmt.step) noDeadAssignBlock(stmt.step, pending, out, file)
|
|
1015
|
+
break
|
|
1016
|
+
case 'foreach':
|
|
1017
|
+
noDeadAssignExprReads(stmt.iterable, pending, out, file)
|
|
1018
|
+
noDeadAssignBlock(stmt.body, pending, out, file)
|
|
1019
|
+
break
|
|
1020
|
+
case 'match':
|
|
1021
|
+
noDeadAssignExprReads(stmt.expr, pending, out, file)
|
|
1022
|
+
for (const arm of stmt.arms) noDeadAssignBlock(arm.body, pending, out, file)
|
|
1023
|
+
break
|
|
1024
|
+
case 'execute':
|
|
1025
|
+
noDeadAssignBlock(stmt.body, pending, out, file)
|
|
1026
|
+
break
|
|
1027
|
+
case 'if_let_some':
|
|
1028
|
+
noDeadAssignExprReads(stmt.init, pending, out, file)
|
|
1029
|
+
noDeadAssignBlock(stmt.then, pending, out, file)
|
|
1030
|
+
if (stmt.else_) noDeadAssignBlock(stmt.else_, pending, out, file)
|
|
1031
|
+
break
|
|
1032
|
+
case 'while_let_some':
|
|
1033
|
+
noDeadAssignExprReads(stmt.init, pending, out, file)
|
|
1034
|
+
noDeadAssignBlock(stmt.body, pending, out, file)
|
|
1035
|
+
break
|
|
1036
|
+
case 'labeled_loop':
|
|
1037
|
+
noDeadAssignStmt(stmt.body, pending, out, file)
|
|
1038
|
+
break
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** Processes an expression: reads clear pending writes; assign exprs register new pending writes. */
|
|
1043
|
+
function noDeadAssignExprReads(
|
|
1044
|
+
expr: HIRExpr | undefined,
|
|
1045
|
+
pending: Map<string, { span?: Span }>,
|
|
1046
|
+
out: LintWarning[],
|
|
1047
|
+
file: string | undefined,
|
|
1048
|
+
): void {
|
|
1049
|
+
if (!expr) return
|
|
1050
|
+
switch (expr.kind) {
|
|
1051
|
+
case 'ident':
|
|
1052
|
+
// Reading a variable clears its pending write
|
|
1053
|
+
pending.delete(expr.name)
|
|
1054
|
+
break
|
|
1055
|
+
case 'assign': {
|
|
1056
|
+
// RHS is read first
|
|
1057
|
+
noDeadAssignExprReads(expr.value, pending, out, file)
|
|
1058
|
+
// Then the target is written — if there's already a pending unread write, warn
|
|
1059
|
+
if (pending.has(expr.target)) {
|
|
1060
|
+
const prev = pending.get(expr.target)!
|
|
1061
|
+
const warn: LintWarning = {
|
|
1062
|
+
rule: 'no-dead-assignment',
|
|
1063
|
+
message: `Assignment to "${expr.target}" is never read before being overwritten`,
|
|
1064
|
+
file,
|
|
1065
|
+
}
|
|
1066
|
+
if (prev.span) { warn.line = prev.span.line; warn.col = prev.span.col }
|
|
1067
|
+
out.push(warn)
|
|
1068
|
+
}
|
|
1069
|
+
pending.set(expr.target, { span: expr.span })
|
|
1070
|
+
break
|
|
1071
|
+
}
|
|
1072
|
+
case 'binary':
|
|
1073
|
+
noDeadAssignExprReads(expr.left, pending, out, file)
|
|
1074
|
+
noDeadAssignExprReads(expr.right, pending, out, file)
|
|
1075
|
+
break
|
|
1076
|
+
case 'unary':
|
|
1077
|
+
noDeadAssignExprReads(expr.operand, pending, out, file)
|
|
1078
|
+
break
|
|
1079
|
+
case 'call':
|
|
1080
|
+
for (const arg of expr.args) noDeadAssignExprReads(arg, pending, out, file)
|
|
1081
|
+
break
|
|
1082
|
+
case 'invoke':
|
|
1083
|
+
noDeadAssignExprReads(expr.callee, pending, out, file)
|
|
1084
|
+
for (const arg of expr.args) noDeadAssignExprReads(arg, pending, out, file)
|
|
1085
|
+
break
|
|
1086
|
+
case 'static_call':
|
|
1087
|
+
for (const arg of expr.args) noDeadAssignExprReads(arg, pending, out, file)
|
|
1088
|
+
break
|
|
1089
|
+
case 'member':
|
|
1090
|
+
noDeadAssignExprReads(expr.obj, pending, out, file)
|
|
1091
|
+
break
|
|
1092
|
+
case 'member_assign':
|
|
1093
|
+
noDeadAssignExprReads(expr.obj, pending, out, file)
|
|
1094
|
+
noDeadAssignExprReads(expr.value, pending, out, file)
|
|
1095
|
+
break
|
|
1096
|
+
case 'index':
|
|
1097
|
+
noDeadAssignExprReads(expr.obj, pending, out, file)
|
|
1098
|
+
noDeadAssignExprReads(expr.index, pending, out, file)
|
|
1099
|
+
break
|
|
1100
|
+
case 'index_assign':
|
|
1101
|
+
noDeadAssignExprReads(expr.obj, pending, out, file)
|
|
1102
|
+
noDeadAssignExprReads(expr.index, pending, out, file)
|
|
1103
|
+
noDeadAssignExprReads(expr.value, pending, out, file)
|
|
1104
|
+
break
|
|
1105
|
+
case 'some_lit':
|
|
1106
|
+
noDeadAssignExprReads(expr.value, pending, out, file)
|
|
1107
|
+
break
|
|
1108
|
+
case 'unwrap_or':
|
|
1109
|
+
noDeadAssignExprReads(expr.opt, pending, out, file)
|
|
1110
|
+
noDeadAssignExprReads(expr.default_, pending, out, file)
|
|
1111
|
+
break
|
|
1112
|
+
case 'type_cast':
|
|
1113
|
+
noDeadAssignExprReads(expr.expr, pending, out, file)
|
|
1114
|
+
break
|
|
1115
|
+
case 'array_lit':
|
|
1116
|
+
case 'tuple_lit':
|
|
1117
|
+
for (const e of expr.elements) noDeadAssignExprReads(e, pending, out, file)
|
|
1118
|
+
break
|
|
1119
|
+
case 'struct_lit':
|
|
1120
|
+
for (const f of expr.fields) noDeadAssignExprReads(f.value, pending, out, file)
|
|
1121
|
+
break
|
|
1122
|
+
case 'str_interp':
|
|
1123
|
+
for (const p of expr.parts) {
|
|
1124
|
+
if (typeof p !== 'string') noDeadAssignExprReads(p, pending, out, file)
|
|
1125
|
+
}
|
|
1126
|
+
break
|
|
1127
|
+
case 'f_string':
|
|
1128
|
+
for (const p of expr.parts) {
|
|
1129
|
+
if (p.kind === 'expr') noDeadAssignExprReads(p.expr, pending, out, file)
|
|
1130
|
+
}
|
|
1131
|
+
break
|
|
1132
|
+
case 'lambda':
|
|
1133
|
+
if (Array.isArray(expr.body)) {
|
|
1134
|
+
noDeadAssignBlock(expr.body as HIRBlock, pending, out, file)
|
|
1135
|
+
} else {
|
|
1136
|
+
noDeadAssignExprReads(expr.body as HIRExpr, pending, out, file)
|
|
1137
|
+
}
|
|
1138
|
+
break
|
|
1139
|
+
case 'enum_construct':
|
|
1140
|
+
for (const f of expr.args) noDeadAssignExprReads(f.value, pending, out, file)
|
|
1141
|
+
break
|
|
1142
|
+
default:
|
|
1143
|
+
break
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// ---------------------------------------------------------------------------
|
|
1148
|
+
// Rule: prefer-match-exhaustive
|
|
1149
|
+
// ---------------------------------------------------------------------------
|
|
1150
|
+
//
|
|
1151
|
+
// When a match statement uses Option patterns (PatSome / PatNone), it should
|
|
1152
|
+
// cover both arms. Missing PatNone means the None case falls through silently;
|
|
1153
|
+
// missing PatSome means Some values are unhandled.
|
|
1154
|
+
|
|
1155
|
+
function checkPreferMatchExhaustive(fn: HIRFunction, file?: string): LintWarning[] {
|
|
1156
|
+
const warnings: LintWarning[] = []
|
|
1157
|
+
checkPreferMatchExhaustiveBlock(fn.body, warnings, file)
|
|
1158
|
+
return warnings
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function checkPreferMatchExhaustiveBlock(block: HIRBlock, out: LintWarning[], file: string | undefined): void {
|
|
1162
|
+
for (const stmt of block) {
|
|
1163
|
+
checkPreferMatchExhaustiveStmt(stmt, out, file)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function checkPreferMatchExhaustiveStmt(stmt: HIRStmt, out: LintWarning[], file: string | undefined): void {
|
|
1168
|
+
switch (stmt.kind) {
|
|
1169
|
+
case 'match': {
|
|
1170
|
+
const patKinds = new Set(stmt.arms.map(a => a.pattern.kind))
|
|
1171
|
+
const hasOptionPat = patKinds.has('PatSome') || patKinds.has('PatNone')
|
|
1172
|
+
const hasWild = patKinds.has('PatWild')
|
|
1173
|
+
if (hasOptionPat && !hasWild) {
|
|
1174
|
+
const hasSome = patKinds.has('PatSome')
|
|
1175
|
+
const hasNone = patKinds.has('PatNone')
|
|
1176
|
+
if (!hasSome) {
|
|
1177
|
+
const warn: LintWarning = {
|
|
1178
|
+
rule: 'prefer-match-exhaustive',
|
|
1179
|
+
message: `match on Option is missing a Some(_) arm`,
|
|
1180
|
+
file,
|
|
1181
|
+
}
|
|
1182
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1183
|
+
out.push(warn)
|
|
1184
|
+
}
|
|
1185
|
+
if (!hasNone) {
|
|
1186
|
+
const warn: LintWarning = {
|
|
1187
|
+
rule: 'prefer-match-exhaustive',
|
|
1188
|
+
message: `match on Option is missing a None arm`,
|
|
1189
|
+
file,
|
|
1190
|
+
}
|
|
1191
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1192
|
+
out.push(warn)
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// Recurse into arm bodies
|
|
1196
|
+
for (const arm of stmt.arms) checkPreferMatchExhaustiveBlock(arm.body, out, file)
|
|
1197
|
+
break
|
|
1198
|
+
}
|
|
1199
|
+
case 'if':
|
|
1200
|
+
checkPreferMatchExhaustiveBlock(stmt.then, out, file)
|
|
1201
|
+
if (stmt.else_) checkPreferMatchExhaustiveBlock(stmt.else_, out, file)
|
|
1202
|
+
break
|
|
1203
|
+
case 'while':
|
|
1204
|
+
checkPreferMatchExhaustiveBlock(stmt.body, out, file)
|
|
1205
|
+
if (stmt.step) checkPreferMatchExhaustiveBlock(stmt.step, out, file)
|
|
1206
|
+
break
|
|
1207
|
+
case 'foreach':
|
|
1208
|
+
checkPreferMatchExhaustiveBlock(stmt.body, out, file)
|
|
1209
|
+
break
|
|
1210
|
+
case 'execute':
|
|
1211
|
+
checkPreferMatchExhaustiveBlock(stmt.body, out, file)
|
|
1212
|
+
break
|
|
1213
|
+
case 'if_let_some':
|
|
1214
|
+
checkPreferMatchExhaustiveBlock(stmt.then, out, file)
|
|
1215
|
+
if (stmt.else_) checkPreferMatchExhaustiveBlock(stmt.else_, out, file)
|
|
1216
|
+
break
|
|
1217
|
+
case 'while_let_some':
|
|
1218
|
+
checkPreferMatchExhaustiveBlock(stmt.body, out, file)
|
|
1219
|
+
break
|
|
1220
|
+
case 'labeled_loop':
|
|
1221
|
+
checkPreferMatchExhaustiveStmt(stmt.body, out, file)
|
|
1222
|
+
break
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// ---------------------------------------------------------------------------
|
|
1227
|
+
// Rule: no-empty-catch
|
|
1228
|
+
// ---------------------------------------------------------------------------
|
|
1229
|
+
//
|
|
1230
|
+
// RedScript has no try/catch. The equivalent pattern is `if_let_some` with an
|
|
1231
|
+
// empty else_ block (silently ignoring the None case) or a match arm whose
|
|
1232
|
+
// body is empty. Both patterns silently swallow a failure — warn about them.
|
|
1233
|
+
|
|
1234
|
+
function checkNoEmptyCatch(fn: HIRFunction, file?: string): LintWarning[] {
|
|
1235
|
+
const warnings: LintWarning[] = []
|
|
1236
|
+
checkNoEmptyCatchBlock(fn.body, warnings, file)
|
|
1237
|
+
return warnings
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function checkNoEmptyCatchBlock(block: HIRBlock, out: LintWarning[], file: string | undefined): void {
|
|
1241
|
+
for (const stmt of block) {
|
|
1242
|
+
checkNoEmptyCatchStmt(stmt, out, file)
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function checkNoEmptyCatchStmt(stmt: HIRStmt, out: LintWarning[], file: string | undefined): void {
|
|
1247
|
+
switch (stmt.kind) {
|
|
1248
|
+
case 'if_let_some':
|
|
1249
|
+
if (stmt.else_ && stmt.else_.length === 0) {
|
|
1250
|
+
const warn: LintWarning = {
|
|
1251
|
+
rule: 'no-empty-catch',
|
|
1252
|
+
message: `Empty else block in if let Some — None case is silently ignored`,
|
|
1253
|
+
file,
|
|
1254
|
+
}
|
|
1255
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1256
|
+
out.push(warn)
|
|
1257
|
+
}
|
|
1258
|
+
checkNoEmptyCatchBlock(stmt.then, out, file)
|
|
1259
|
+
if (stmt.else_) checkNoEmptyCatchBlock(stmt.else_, out, file)
|
|
1260
|
+
break
|
|
1261
|
+
case 'match':
|
|
1262
|
+
for (const arm of stmt.arms) {
|
|
1263
|
+
if (arm.body.length === 0) {
|
|
1264
|
+
const warn: LintWarning = {
|
|
1265
|
+
rule: 'no-empty-catch',
|
|
1266
|
+
message: `Empty match arm body — consider handling this case explicitly`,
|
|
1267
|
+
file,
|
|
1268
|
+
}
|
|
1269
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1270
|
+
out.push(warn)
|
|
1271
|
+
}
|
|
1272
|
+
checkNoEmptyCatchBlock(arm.body, out, file)
|
|
1273
|
+
}
|
|
1274
|
+
break
|
|
1275
|
+
case 'if':
|
|
1276
|
+
checkNoEmptyCatchBlock(stmt.then, out, file)
|
|
1277
|
+
if (stmt.else_) checkNoEmptyCatchBlock(stmt.else_, out, file)
|
|
1278
|
+
break
|
|
1279
|
+
case 'while':
|
|
1280
|
+
checkNoEmptyCatchBlock(stmt.body, out, file)
|
|
1281
|
+
if (stmt.step) checkNoEmptyCatchBlock(stmt.step, out, file)
|
|
1282
|
+
break
|
|
1283
|
+
case 'foreach':
|
|
1284
|
+
checkNoEmptyCatchBlock(stmt.body, out, file)
|
|
1285
|
+
break
|
|
1286
|
+
case 'execute':
|
|
1287
|
+
checkNoEmptyCatchBlock(stmt.body, out, file)
|
|
1288
|
+
break
|
|
1289
|
+
case 'while_let_some':
|
|
1290
|
+
checkNoEmptyCatchBlock(stmt.body, out, file)
|
|
1291
|
+
break
|
|
1292
|
+
case 'labeled_loop':
|
|
1293
|
+
checkNoEmptyCatchStmt(stmt.body, out, file)
|
|
1294
|
+
break
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// ---------------------------------------------------------------------------
|
|
1299
|
+
// Rule: naming-convention
|
|
1300
|
+
// ---------------------------------------------------------------------------
|
|
1301
|
+
//
|
|
1302
|
+
// Variables (let bindings, foreach bindings) must use camelCase.
|
|
1303
|
+
// Type names (structs, enums) must use PascalCase.
|
|
1304
|
+
//
|
|
1305
|
+
// camelCase: starts with lowercase letter, no underscores (except leading _)
|
|
1306
|
+
// PascalCase: starts with uppercase letter
|
|
1307
|
+
|
|
1308
|
+
function isCamelCase(name: string): boolean {
|
|
1309
|
+
// Allow leading underscore (private convention), then must start lowercase
|
|
1310
|
+
const stripped = name.startsWith('_') ? name.slice(1) : name
|
|
1311
|
+
if (stripped.length === 0) return true
|
|
1312
|
+
// Must start with lowercase, no underscores after first char
|
|
1313
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(stripped)
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function isPascalCase(name: string): boolean {
|
|
1317
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name)
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function checkNamingConvention(fn: HIRFunction, file?: string): LintWarning[] {
|
|
1321
|
+
const warnings: LintWarning[] = []
|
|
1322
|
+
checkNamingConventionBlock(fn.body, warnings, file)
|
|
1323
|
+
return warnings
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function checkNamingConventionBlock(block: HIRBlock, out: LintWarning[], file: string | undefined): void {
|
|
1327
|
+
for (const stmt of block) {
|
|
1328
|
+
checkNamingConventionStmt(stmt, out, file)
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function checkNamingConventionStmt(stmt: HIRStmt, out: LintWarning[], file: string | undefined): void {
|
|
1333
|
+
switch (stmt.kind) {
|
|
1334
|
+
case 'let':
|
|
1335
|
+
if (!isCamelCase(stmt.name)) {
|
|
1336
|
+
const warn: LintWarning = {
|
|
1337
|
+
rule: 'naming-convention',
|
|
1338
|
+
message: `Variable "${stmt.name}" should use camelCase`,
|
|
1339
|
+
file,
|
|
1340
|
+
}
|
|
1341
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1342
|
+
out.push(warn)
|
|
1343
|
+
}
|
|
1344
|
+
break
|
|
1345
|
+
case 'let_destruct':
|
|
1346
|
+
for (const name of stmt.names) {
|
|
1347
|
+
if (!isCamelCase(name)) {
|
|
1348
|
+
const warn: LintWarning = {
|
|
1349
|
+
rule: 'naming-convention',
|
|
1350
|
+
message: `Variable "${name}" should use camelCase`,
|
|
1351
|
+
file,
|
|
1352
|
+
}
|
|
1353
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1354
|
+
out.push(warn)
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
break
|
|
1358
|
+
case 'foreach':
|
|
1359
|
+
if (!isCamelCase(stmt.binding)) {
|
|
1360
|
+
const warn: LintWarning = {
|
|
1361
|
+
rule: 'naming-convention',
|
|
1362
|
+
message: `Loop variable "${stmt.binding}" should use camelCase`,
|
|
1363
|
+
file,
|
|
1364
|
+
}
|
|
1365
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1366
|
+
out.push(warn)
|
|
1367
|
+
}
|
|
1368
|
+
checkNamingConventionBlock(stmt.body, out, file)
|
|
1369
|
+
break
|
|
1370
|
+
case 'if':
|
|
1371
|
+
checkNamingConventionBlock(stmt.then, out, file)
|
|
1372
|
+
if (stmt.else_) checkNamingConventionBlock(stmt.else_, out, file)
|
|
1373
|
+
break
|
|
1374
|
+
case 'while':
|
|
1375
|
+
checkNamingConventionBlock(stmt.body, out, file)
|
|
1376
|
+
if (stmt.step) checkNamingConventionBlock(stmt.step, out, file)
|
|
1377
|
+
break
|
|
1378
|
+
case 'match':
|
|
1379
|
+
for (const arm of stmt.arms) checkNamingConventionBlock(arm.body, out, file)
|
|
1380
|
+
break
|
|
1381
|
+
case 'execute':
|
|
1382
|
+
checkNamingConventionBlock(stmt.body, out, file)
|
|
1383
|
+
break
|
|
1384
|
+
case 'if_let_some':
|
|
1385
|
+
if (!isCamelCase(stmt.binding)) {
|
|
1386
|
+
const warn: LintWarning = {
|
|
1387
|
+
rule: 'naming-convention',
|
|
1388
|
+
message: `Binding "${stmt.binding}" should use camelCase`,
|
|
1389
|
+
file,
|
|
1390
|
+
}
|
|
1391
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1392
|
+
out.push(warn)
|
|
1393
|
+
}
|
|
1394
|
+
checkNamingConventionBlock(stmt.then, out, file)
|
|
1395
|
+
if (stmt.else_) checkNamingConventionBlock(stmt.else_, out, file)
|
|
1396
|
+
break
|
|
1397
|
+
case 'while_let_some':
|
|
1398
|
+
if (!isCamelCase(stmt.binding)) {
|
|
1399
|
+
const warn: LintWarning = {
|
|
1400
|
+
rule: 'naming-convention',
|
|
1401
|
+
message: `Binding "${stmt.binding}" should use camelCase`,
|
|
1402
|
+
file,
|
|
1403
|
+
}
|
|
1404
|
+
if (stmt.span) { warn.line = stmt.span.line; warn.col = stmt.span.col }
|
|
1405
|
+
out.push(warn)
|
|
1406
|
+
}
|
|
1407
|
+
checkNamingConventionBlock(stmt.body, out, file)
|
|
1408
|
+
break
|
|
1409
|
+
case 'labeled_loop':
|
|
1410
|
+
checkNamingConventionStmt(stmt.body, out, file)
|
|
1411
|
+
break
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/** Check type names (structs and enums) at the module level. */
|
|
1416
|
+
function checkNamingConventionModule(hir: HIRModule, file?: string): LintWarning[] {
|
|
1417
|
+
const warnings: LintWarning[] = []
|
|
1418
|
+
for (const s of hir.structs) {
|
|
1419
|
+
if (!isPascalCase(s.name)) {
|
|
1420
|
+
const warn: LintWarning = {
|
|
1421
|
+
rule: 'naming-convention',
|
|
1422
|
+
message: `Struct "${s.name}" should use PascalCase`,
|
|
1423
|
+
file,
|
|
1424
|
+
}
|
|
1425
|
+
if (s.span) { warn.line = s.span.line; warn.col = s.span.col }
|
|
1426
|
+
warnings.push(warn)
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
for (const e of hir.enums) {
|
|
1430
|
+
if (!isPascalCase(e.name)) {
|
|
1431
|
+
const warn: LintWarning = {
|
|
1432
|
+
rule: 'naming-convention',
|
|
1433
|
+
message: `Enum "${e.name}" should use PascalCase`,
|
|
1434
|
+
file,
|
|
1435
|
+
}
|
|
1436
|
+
if (e.span) { warn.line = e.span.line; warn.col = e.span.col }
|
|
1437
|
+
warnings.push(warn)
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return warnings
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// ---------------------------------------------------------------------------
|
|
1444
|
+
// Rule: no-magic-numbers
|
|
1445
|
+
// ---------------------------------------------------------------------------
|
|
1446
|
+
//
|
|
1447
|
+
// Flags any numeric literal that is not in the allowedNumbers list (default
|
|
1448
|
+
// [0, 1]) when used outside of a const declaration. Unlike the older
|
|
1449
|
+
// magic-number rule (threshold > 1), this rule checks against an explicit
|
|
1450
|
+
// allow-list, making it configurable for different projects.
|
|
1451
|
+
|
|
1452
|
+
function checkNoMagicNumbers(fn: HIRFunction, allowed: number[], file?: string): LintWarning[] {
|
|
1453
|
+
const warnings: LintWarning[] = []
|
|
1454
|
+
const allowedSet = new Set(allowed)
|
|
1455
|
+
checkNoMagicNumbersBlock(fn.body, warnings, file, allowedSet, /*inConst=*/false)
|
|
1456
|
+
return warnings
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function checkNoMagicNumbersBlock(
|
|
1460
|
+
block: HIRBlock,
|
|
1461
|
+
out: LintWarning[],
|
|
1462
|
+
file: string | undefined,
|
|
1463
|
+
allowed: Set<number>,
|
|
1464
|
+
inConst: boolean,
|
|
1465
|
+
): void {
|
|
1466
|
+
for (const stmt of block) {
|
|
1467
|
+
checkNoMagicNumbersStmt(stmt, out, file, allowed, inConst)
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function checkNoMagicNumbersStmt(
|
|
1472
|
+
stmt: HIRStmt,
|
|
1473
|
+
out: LintWarning[],
|
|
1474
|
+
file: string | undefined,
|
|
1475
|
+
allowed: Set<number>,
|
|
1476
|
+
inConst: boolean,
|
|
1477
|
+
): void {
|
|
1478
|
+
switch (stmt.kind) {
|
|
1479
|
+
case 'const_decl':
|
|
1480
|
+
// Numbers in const declarations are the intended fix — skip
|
|
1481
|
+
break
|
|
1482
|
+
case 'let':
|
|
1483
|
+
checkNoMagicNumbersExpr(stmt.init, out, file, allowed)
|
|
1484
|
+
break
|
|
1485
|
+
case 'let_destruct':
|
|
1486
|
+
checkNoMagicNumbersExpr(stmt.init, out, file, allowed)
|
|
1487
|
+
break
|
|
1488
|
+
case 'expr':
|
|
1489
|
+
checkNoMagicNumbersExpr(stmt.expr, out, file, allowed)
|
|
1490
|
+
break
|
|
1491
|
+
case 'return':
|
|
1492
|
+
if (stmt.value) checkNoMagicNumbersExpr(stmt.value, out, file, allowed)
|
|
1493
|
+
break
|
|
1494
|
+
case 'if':
|
|
1495
|
+
checkNoMagicNumbersExpr(stmt.cond, out, file, allowed)
|
|
1496
|
+
checkNoMagicNumbersBlock(stmt.then, out, file, allowed, false)
|
|
1497
|
+
if (stmt.else_) checkNoMagicNumbersBlock(stmt.else_, out, file, allowed, false)
|
|
1498
|
+
break
|
|
1499
|
+
case 'while':
|
|
1500
|
+
checkNoMagicNumbersExpr(stmt.cond, out, file, allowed)
|
|
1501
|
+
checkNoMagicNumbersBlock(stmt.body, out, file, allowed, false)
|
|
1502
|
+
if (stmt.step) checkNoMagicNumbersBlock(stmt.step, out, file, allowed, false)
|
|
1503
|
+
break
|
|
1504
|
+
case 'foreach':
|
|
1505
|
+
checkNoMagicNumbersExpr(stmt.iterable, out, file, allowed)
|
|
1506
|
+
checkNoMagicNumbersBlock(stmt.body, out, file, allowed, false)
|
|
1507
|
+
break
|
|
1508
|
+
case 'match':
|
|
1509
|
+
checkNoMagicNumbersExpr(stmt.expr, out, file, allowed)
|
|
1510
|
+
for (const arm of stmt.arms) checkNoMagicNumbersBlock(arm.body, out, file, allowed, false)
|
|
1511
|
+
break
|
|
1512
|
+
case 'execute':
|
|
1513
|
+
checkNoMagicNumbersBlock(stmt.body, out, file, allowed, false)
|
|
1514
|
+
break
|
|
1515
|
+
case 'if_let_some':
|
|
1516
|
+
checkNoMagicNumbersExpr(stmt.init, out, file, allowed)
|
|
1517
|
+
checkNoMagicNumbersBlock(stmt.then, out, file, allowed, false)
|
|
1518
|
+
if (stmt.else_) checkNoMagicNumbersBlock(stmt.else_, out, file, allowed, false)
|
|
1519
|
+
break
|
|
1520
|
+
case 'while_let_some':
|
|
1521
|
+
checkNoMagicNumbersExpr(stmt.init, out, file, allowed)
|
|
1522
|
+
checkNoMagicNumbersBlock(stmt.body, out, file, allowed, false)
|
|
1523
|
+
break
|
|
1524
|
+
case 'labeled_loop':
|
|
1525
|
+
checkNoMagicNumbersStmt(stmt.body, out, file, allowed, inConst)
|
|
1526
|
+
break
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function checkNoMagicNumbersExpr(
|
|
1531
|
+
expr: HIRExpr | undefined,
|
|
1532
|
+
out: LintWarning[],
|
|
1533
|
+
file: string | undefined,
|
|
1534
|
+
allowed: Set<number>,
|
|
1535
|
+
): void {
|
|
1536
|
+
if (!expr) return
|
|
1537
|
+
switch (expr.kind) {
|
|
1538
|
+
case 'int_lit':
|
|
1539
|
+
case 'float_lit':
|
|
1540
|
+
case 'byte_lit':
|
|
1541
|
+
case 'short_lit':
|
|
1542
|
+
case 'long_lit':
|
|
1543
|
+
case 'double_lit':
|
|
1544
|
+
if (!allowed.has(expr.value)) {
|
|
1545
|
+
const warn: LintWarning = {
|
|
1546
|
+
rule: 'no-magic-numbers',
|
|
1547
|
+
message: `Magic number ${expr.value} — extract to a named const`,
|
|
1548
|
+
file,
|
|
1549
|
+
}
|
|
1550
|
+
if (expr.span) { warn.line = expr.span.line; warn.col = expr.span.col }
|
|
1551
|
+
out.push(warn)
|
|
1552
|
+
}
|
|
1553
|
+
break
|
|
1554
|
+
case 'binary':
|
|
1555
|
+
checkNoMagicNumbersExpr(expr.left, out, file, allowed)
|
|
1556
|
+
checkNoMagicNumbersExpr(expr.right, out, file, allowed)
|
|
1557
|
+
break
|
|
1558
|
+
case 'unary':
|
|
1559
|
+
checkNoMagicNumbersExpr(expr.operand, out, file, allowed)
|
|
1560
|
+
break
|
|
1561
|
+
case 'call':
|
|
1562
|
+
for (const arg of expr.args) checkNoMagicNumbersExpr(arg, out, file, allowed)
|
|
1563
|
+
break
|
|
1564
|
+
case 'invoke':
|
|
1565
|
+
checkNoMagicNumbersExpr(expr.callee, out, file, allowed)
|
|
1566
|
+
for (const arg of expr.args) checkNoMagicNumbersExpr(arg, out, file, allowed)
|
|
1567
|
+
break
|
|
1568
|
+
case 'static_call':
|
|
1569
|
+
for (const arg of expr.args) checkNoMagicNumbersExpr(arg, out, file, allowed)
|
|
1570
|
+
break
|
|
1571
|
+
case 'member':
|
|
1572
|
+
checkNoMagicNumbersExpr(expr.obj, out, file, allowed)
|
|
1573
|
+
break
|
|
1574
|
+
case 'member_assign':
|
|
1575
|
+
checkNoMagicNumbersExpr(expr.obj, out, file, allowed)
|
|
1576
|
+
checkNoMagicNumbersExpr(expr.value, out, file, allowed)
|
|
1577
|
+
break
|
|
1578
|
+
case 'index':
|
|
1579
|
+
checkNoMagicNumbersExpr(expr.obj, out, file, allowed)
|
|
1580
|
+
checkNoMagicNumbersExpr(expr.index, out, file, allowed)
|
|
1581
|
+
break
|
|
1582
|
+
case 'index_assign':
|
|
1583
|
+
checkNoMagicNumbersExpr(expr.obj, out, file, allowed)
|
|
1584
|
+
checkNoMagicNumbersExpr(expr.index, out, file, allowed)
|
|
1585
|
+
checkNoMagicNumbersExpr(expr.value, out, file, allowed)
|
|
1586
|
+
break
|
|
1587
|
+
case 'assign':
|
|
1588
|
+
checkNoMagicNumbersExpr(expr.value, out, file, allowed)
|
|
1589
|
+
break
|
|
1590
|
+
case 'some_lit':
|
|
1591
|
+
checkNoMagicNumbersExpr(expr.value, out, file, allowed)
|
|
1592
|
+
break
|
|
1593
|
+
case 'unwrap_or':
|
|
1594
|
+
checkNoMagicNumbersExpr(expr.opt, out, file, allowed)
|
|
1595
|
+
checkNoMagicNumbersExpr(expr.default_, out, file, allowed)
|
|
1596
|
+
break
|
|
1597
|
+
case 'type_cast':
|
|
1598
|
+
checkNoMagicNumbersExpr(expr.expr, out, file, allowed)
|
|
1599
|
+
break
|
|
1600
|
+
case 'array_lit':
|
|
1601
|
+
case 'tuple_lit':
|
|
1602
|
+
for (const e of expr.elements) checkNoMagicNumbersExpr(e, out, file, allowed)
|
|
1603
|
+
break
|
|
1604
|
+
case 'struct_lit':
|
|
1605
|
+
for (const f of expr.fields) checkNoMagicNumbersExpr(f.value, out, file, allowed)
|
|
1606
|
+
break
|
|
1607
|
+
case 'str_interp':
|
|
1608
|
+
for (const p of expr.parts) {
|
|
1609
|
+
if (typeof p !== 'string') checkNoMagicNumbersExpr(p, out, file, allowed)
|
|
1610
|
+
}
|
|
1611
|
+
break
|
|
1612
|
+
case 'f_string':
|
|
1613
|
+
for (const p of expr.parts) {
|
|
1614
|
+
if (p.kind === 'expr') checkNoMagicNumbersExpr(p.expr, out, file, allowed)
|
|
1615
|
+
}
|
|
1616
|
+
break
|
|
1617
|
+
case 'lambda':
|
|
1618
|
+
if (Array.isArray(expr.body)) {
|
|
1619
|
+
checkNoMagicNumbersBlock(expr.body as HIRBlock, out, file, allowed, false)
|
|
1620
|
+
} else {
|
|
1621
|
+
checkNoMagicNumbersExpr(expr.body as HIRExpr, out, file, allowed)
|
|
1622
|
+
}
|
|
1623
|
+
break
|
|
1624
|
+
case 'enum_construct':
|
|
1625
|
+
for (const f of expr.args) checkNoMagicNumbersExpr(f.value, out, file, allowed)
|
|
1626
|
+
break
|
|
1627
|
+
default:
|
|
1628
|
+
break
|
|
1629
|
+
}
|
|
1630
|
+
}
|