@triflare/fx 1.0.3 → 2.1.0-beta.1
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 +59 -0
- package/bin/fx.js +4 -1162
- package/package.json +8 -4
- package/product.yml +1 -1
- package/src/brand.js +9 -3
- package/src/catalog.js +103 -0
- package/src/cli/commands/build.js +98 -0
- package/src/cli/commands/catalog.js +42 -0
- package/src/cli/commands/check.js +44 -0
- package/src/cli/commands/completion.js +54 -0
- package/src/cli/commands/decompile.js +266 -0
- package/src/cli/commands/dev.js +179 -0
- package/src/cli/commands/extension.js +110 -0
- package/src/cli/commands/feature.js +114 -0
- package/src/cli/commands/fmt.js +173 -0
- package/src/cli/commands/index.js +45 -0
- package/src/cli/commands/init.js +169 -0
- package/src/cli/commands/lint.js +270 -0
- package/src/cli/commands/package.js +137 -0
- package/src/cli/commands/sprite/add.js +85 -0
- package/src/cli/commands/sprite/build.js +62 -0
- package/src/cli/commands/sprite/helpers.js +66 -0
- package/src/cli/commands/sprite/index.js +29 -0
- package/src/cli/commands/sprite/list.js +31 -0
- package/src/cli/commands/sprite/remove.js +51 -0
- package/src/cli/commands/vendor/add.js +98 -0
- package/src/cli/commands/vendor/helpers.js +50 -0
- package/src/cli/commands/vendor/index.js +39 -0
- package/src/cli/commands/vendor/install.js +64 -0
- package/src/cli/commands/vendor/list.js +45 -0
- package/src/cli/commands/vendor/migrate.js +47 -0
- package/src/cli/commands/vendor/outdated.js +51 -0
- package/src/cli/commands/vendor/remove.js +34 -0
- package/src/cli/commands/vendor/update.js +49 -0
- package/src/cli/commands/vendor/verify.js +44 -0
- package/src/cli/completion/bash.js +10 -0
- package/src/cli/completion/fish.js +9 -0
- package/src/cli/completion/index.js +97 -0
- package/src/cli/completion/zsh.js +11 -0
- package/src/cli/context.js +270 -0
- package/src/cli/define-command.js +147 -0
- package/src/cli/errors.js +111 -0
- package/src/cli/format.js +31 -0
- package/src/cli/prompts.js +183 -0
- package/src/cli/reporter.js +203 -0
- package/src/cli/run.js +248 -0
- package/src/cli/templates/index.js +112 -0
- package/src/cli/theme.js +84 -0
- package/src/compiler.js +133 -0
- package/src/config-edit.js +399 -1
- package/src/config.js +174 -4
- package/src/decompiler.js +45 -0
- package/src/extensions.js +72 -19
- package/src/features.js +339 -0
- package/src/formatter.js +499 -0
- package/src/index.js +31 -0
- package/src/lexer.js +45 -4
- package/src/lint/analyze.js +1414 -0
- package/src/lint/config-file.js +97 -0
- package/src/lint/config.js +174 -0
- package/src/lint/rules.js +461 -0
- package/src/linter.js +220 -0
- package/src/merge.js +109 -0
- package/src/parser.js +53 -13
- package/src/project.js +7 -87
- package/src/vendor.js +1323 -0
- package/vendor/pm-packager-dist/.fx-build.json +1 -1
- package/vendor/tempvars/fx-tempvars.js +321 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triflare Fx linter rule metadata and presets.
|
|
3
|
+
*
|
|
4
|
+
* Rule IDs and categories are stable: tools, suppressions and `fx.yml` refer to
|
|
5
|
+
* a rule by ID, and the category only groups diagnostics for presentation. A
|
|
6
|
+
* rule that is not part of a preset is off unless `fx.lint.rules` (or
|
|
7
|
+
* `--rule`) enables it explicitly, so the initial implementation can ship a
|
|
8
|
+
* subset of the full ruleset without renumbering anything.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Category labels, keyed by the prefix used in rule metadata. */
|
|
12
|
+
export const CATEGORIES = {
|
|
13
|
+
syntax: "Syntax",
|
|
14
|
+
names: "Names and scope",
|
|
15
|
+
control: "Control flow",
|
|
16
|
+
expressions: "Expressions and runtime behavior",
|
|
17
|
+
project: "Assets, extensions, and project structure",
|
|
18
|
+
maintainability: "Maintainability",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Severity a rule reports when it is enabled without an override. */
|
|
22
|
+
export const SEVERITIES = ["error", "warning", "off"];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} Rule
|
|
26
|
+
* @property {string} id stable rule ID, such as `FX201`
|
|
27
|
+
* @property {string} category key of `CATEGORIES`
|
|
28
|
+
* @property {'error'|'warning'} severity default severity
|
|
29
|
+
* @property {string} summary one-line description
|
|
30
|
+
* @property {string} explanation why the rule exists and what to do about it
|
|
31
|
+
* @property {boolean} [fixable] whether `--fix` can repair it
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** @type {Record<string, Rule>} */
|
|
35
|
+
export const RULES = {
|
|
36
|
+
// ---------------------------------------------------------------- syntax
|
|
37
|
+
FX000: {
|
|
38
|
+
id: "FX000",
|
|
39
|
+
category: "syntax",
|
|
40
|
+
severity: "error",
|
|
41
|
+
summary: "Parse or project error",
|
|
42
|
+
explanation:
|
|
43
|
+
"The source could not be parsed or the project could not be assembled, " +
|
|
44
|
+
"so lint rules were not applied to it. Fix the reported syntax error " +
|
|
45
|
+
"first; `fx check` reports the same problems.",
|
|
46
|
+
},
|
|
47
|
+
FX001: {
|
|
48
|
+
id: "FX001",
|
|
49
|
+
category: "syntax",
|
|
50
|
+
severity: "error",
|
|
51
|
+
summary: "Invalid lint suppression",
|
|
52
|
+
explanation:
|
|
53
|
+
"An `fx-lint` comment names a rule that does not exist, so the " +
|
|
54
|
+
"suppression silently does nothing. Use a stable rule ID or remove the " +
|
|
55
|
+
"comment.",
|
|
56
|
+
},
|
|
57
|
+
FX002: {
|
|
58
|
+
id: "FX002",
|
|
59
|
+
category: "syntax",
|
|
60
|
+
severity: "warning",
|
|
61
|
+
summary: "Unused lint suppression",
|
|
62
|
+
explanation:
|
|
63
|
+
"An `fx-lint` comment matches no diagnostic. Remove it so the reason for " +
|
|
64
|
+
"the suppression does not outlive the problem.",
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// ------------------------------------------------------- names and scope
|
|
68
|
+
FX201: {
|
|
69
|
+
id: "FX201",
|
|
70
|
+
category: "names",
|
|
71
|
+
severity: "error",
|
|
72
|
+
summary: "Undefined variable",
|
|
73
|
+
explanation:
|
|
74
|
+
"A name is read as a variable but no `let`, `list` or function parameter " +
|
|
75
|
+
"declares it, so the project fails to compile.",
|
|
76
|
+
},
|
|
77
|
+
FX202: {
|
|
78
|
+
id: "FX202",
|
|
79
|
+
category: "names",
|
|
80
|
+
severity: "error",
|
|
81
|
+
summary: "Undefined list",
|
|
82
|
+
explanation:
|
|
83
|
+
"A list method is called on a name that is not a declared list. Declare " +
|
|
84
|
+
"it with `list <name> = [];` or check the spelling.",
|
|
85
|
+
},
|
|
86
|
+
FX203: {
|
|
87
|
+
id: "FX203",
|
|
88
|
+
category: "names",
|
|
89
|
+
severity: "error",
|
|
90
|
+
summary: "Read before initialization",
|
|
91
|
+
explanation:
|
|
92
|
+
"A declaration's initializer reads a variable or list declared later in " +
|
|
93
|
+
"the same target. Initializers run in declaration order, so the later " +
|
|
94
|
+
"declaration still holds its default value here.",
|
|
95
|
+
},
|
|
96
|
+
FX204: {
|
|
97
|
+
id: "FX204",
|
|
98
|
+
category: "names",
|
|
99
|
+
severity: "error",
|
|
100
|
+
summary: "Assignment creates an implicit variable",
|
|
101
|
+
explanation:
|
|
102
|
+
"Assigning to an undeclared name fails to compile. Declare it with " +
|
|
103
|
+
"`let <name> = <value>;` first; `--fix` can insert the declaration.",
|
|
104
|
+
fixable: true,
|
|
105
|
+
},
|
|
106
|
+
FX205: {
|
|
107
|
+
id: "FX205",
|
|
108
|
+
category: "names",
|
|
109
|
+
severity: "warning",
|
|
110
|
+
summary: "Local variable shadows a parameter",
|
|
111
|
+
explanation:
|
|
112
|
+
"A target-level `let` or `list` has the same name as a function " +
|
|
113
|
+
"parameter, so inside that function the parameter hides the local. Rename " +
|
|
114
|
+
"one of them to keep both reachable.",
|
|
115
|
+
},
|
|
116
|
+
FX206: {
|
|
117
|
+
id: "FX206",
|
|
118
|
+
category: "names",
|
|
119
|
+
severity: "warning",
|
|
120
|
+
summary: "Local variable shadows a project variable",
|
|
121
|
+
explanation:
|
|
122
|
+
"A sprite declares a variable or list with the same name as a stage " +
|
|
123
|
+
"variable. The sprite copy hides the global one, which is easy to miss.",
|
|
124
|
+
},
|
|
125
|
+
FX207: {
|
|
126
|
+
id: "FX207",
|
|
127
|
+
category: "names",
|
|
128
|
+
severity: "warning",
|
|
129
|
+
summary: "Unused function parameter",
|
|
130
|
+
explanation:
|
|
131
|
+
"A parameter is never read in the function body. Remove it, or use it, " +
|
|
132
|
+
"so call sites and the parameter list stay honest.",
|
|
133
|
+
},
|
|
134
|
+
FX208: {
|
|
135
|
+
id: "FX208",
|
|
136
|
+
category: "names",
|
|
137
|
+
severity: "warning",
|
|
138
|
+
summary: "Unused variable or list",
|
|
139
|
+
explanation:
|
|
140
|
+
"A variable or list is declared but never read or written. Remove the " +
|
|
141
|
+
"declaration, or use it.",
|
|
142
|
+
},
|
|
143
|
+
FX209: {
|
|
144
|
+
id: "FX209",
|
|
145
|
+
category: "names",
|
|
146
|
+
severity: "warning",
|
|
147
|
+
summary: "Unused function",
|
|
148
|
+
explanation:
|
|
149
|
+
"A function is declared but never called. Remove it, or call it from a " +
|
|
150
|
+
"hat.",
|
|
151
|
+
},
|
|
152
|
+
FX211: {
|
|
153
|
+
id: "FX211",
|
|
154
|
+
category: "names",
|
|
155
|
+
severity: "warning",
|
|
156
|
+
summary: "Declarations differ only by case",
|
|
157
|
+
explanation:
|
|
158
|
+
"Two declarations in the same target have names that differ only by " +
|
|
159
|
+
"letter case. PenguinMod treats names loosely, so one shadows the other " +
|
|
160
|
+
"in practice. Rename one of them.",
|
|
161
|
+
},
|
|
162
|
+
FX212: {
|
|
163
|
+
id: "FX212",
|
|
164
|
+
category: "names",
|
|
165
|
+
severity: "error",
|
|
166
|
+
summary: "Cloud variable declared outside the stage",
|
|
167
|
+
explanation:
|
|
168
|
+
"`cloud let` is only valid at the top level or inside `stage`; the " +
|
|
169
|
+
"project fails to compile when a sprite declares one.",
|
|
170
|
+
},
|
|
171
|
+
FX214: {
|
|
172
|
+
id: "FX214",
|
|
173
|
+
category: "names",
|
|
174
|
+
severity: "error",
|
|
175
|
+
summary: "Reserved temporary-variable name",
|
|
176
|
+
explanation:
|
|
177
|
+
"Names starting with `$` or `$$` are read as thread/runtime temporary " +
|
|
178
|
+
"variables, so they cannot be declared with `let`, `list` or a function " +
|
|
179
|
+
"parameter. Rename the declaration, or use `thread let` / `runtime let`.",
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// ----------------------------------------------------------- control flow
|
|
183
|
+
FX101: {
|
|
184
|
+
id: "FX101",
|
|
185
|
+
category: "control",
|
|
186
|
+
severity: "error",
|
|
187
|
+
summary: "Unreachable statement",
|
|
188
|
+
explanation:
|
|
189
|
+
"Nothing can connect below `return`, `control.stop(...)` or " +
|
|
190
|
+
"`control.deleteThisClone()`, so the following statement is dropped.",
|
|
191
|
+
},
|
|
192
|
+
FX301: {
|
|
193
|
+
id: "FX301",
|
|
194
|
+
category: "control",
|
|
195
|
+
severity: "warning",
|
|
196
|
+
summary: "Infinite loop with no yielding block",
|
|
197
|
+
explanation:
|
|
198
|
+
"A `forever` loop or a `while` loop whose condition is always true has a " +
|
|
199
|
+
"body with nothing that yields: no wait, no loop, no user function call. " +
|
|
200
|
+
"It spins in place instead of letting other scripts run.",
|
|
201
|
+
},
|
|
202
|
+
FX302: {
|
|
203
|
+
id: "FX302",
|
|
204
|
+
category: "control",
|
|
205
|
+
severity: "warning",
|
|
206
|
+
summary: "Statically constant loop condition",
|
|
207
|
+
explanation:
|
|
208
|
+
"The condition of a `while` loop is a constant, so the loop either never " +
|
|
209
|
+
"runs or never stops. Use `forever` for the intentional case.",
|
|
210
|
+
},
|
|
211
|
+
FX304: {
|
|
212
|
+
id: "FX304",
|
|
213
|
+
category: "control",
|
|
214
|
+
severity: "warning",
|
|
215
|
+
summary: "Constant false or zero loop",
|
|
216
|
+
explanation:
|
|
217
|
+
"A `repeat` count is a constant that is zero or negative, so the body " +
|
|
218
|
+
"never runs. This is almost always a leftover or a mistake.",
|
|
219
|
+
},
|
|
220
|
+
FX307: {
|
|
221
|
+
id: "FX307",
|
|
222
|
+
category: "control",
|
|
223
|
+
severity: "error",
|
|
224
|
+
summary: "Return outside a function",
|
|
225
|
+
explanation:
|
|
226
|
+
"`return` is only valid inside a `function`, `returning function` or " +
|
|
227
|
+
"`warp function`.",
|
|
228
|
+
},
|
|
229
|
+
FX308: {
|
|
230
|
+
id: "FX308",
|
|
231
|
+
category: "control",
|
|
232
|
+
severity: "error",
|
|
233
|
+
summary: "Returning function has a path with no return",
|
|
234
|
+
explanation:
|
|
235
|
+
"A `returning function` can finish without a value, so callers receive " +
|
|
236
|
+
"an empty result. End every branch with a `return`.",
|
|
237
|
+
},
|
|
238
|
+
FX309: {
|
|
239
|
+
id: "FX309",
|
|
240
|
+
category: "control",
|
|
241
|
+
severity: "error",
|
|
242
|
+
summary: "Value returned from a non-returning function",
|
|
243
|
+
explanation:
|
|
244
|
+
"Only a `returning function` may return a value; a plain or `warp` " +
|
|
245
|
+
"function must use a bare `return;`.",
|
|
246
|
+
},
|
|
247
|
+
FX506: {
|
|
248
|
+
id: "FX506",
|
|
249
|
+
category: "control",
|
|
250
|
+
severity: "warning",
|
|
251
|
+
summary: "Warp function contains a yielding operation",
|
|
252
|
+
explanation:
|
|
253
|
+
"Blocks such as `control.wait`, `looks.sayForSecs` and " +
|
|
254
|
+
"`event.broadcastAndWait` cannot yield inside a `warp function`, so the " +
|
|
255
|
+
"whole call still runs in one frame. Remove the yielding block, or drop " +
|
|
256
|
+
"`warp`.",
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// ------------------------------------------ expressions and runtime behavior
|
|
260
|
+
FX403: {
|
|
261
|
+
id: "FX403",
|
|
262
|
+
category: "expressions",
|
|
263
|
+
severity: "warning",
|
|
264
|
+
summary: "Condition is always truthy or falsy",
|
|
265
|
+
explanation:
|
|
266
|
+
"An `if` condition is a literal, so one branch is dead code. Check for a " +
|
|
267
|
+
"comparison that was meant to be there.",
|
|
268
|
+
},
|
|
269
|
+
FX404: {
|
|
270
|
+
id: "FX404",
|
|
271
|
+
category: "expressions",
|
|
272
|
+
severity: "error",
|
|
273
|
+
summary: "Division or modulo by statically known zero",
|
|
274
|
+
explanation:
|
|
275
|
+
"Dividing by a literal zero makes PenguinMod report `Infinity` or " +
|
|
276
|
+
"`NaN`, which quietly poisons every later calculation.",
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// ---------------------------------- assets, extensions, and project structure
|
|
280
|
+
FX701: {
|
|
281
|
+
id: "FX701",
|
|
282
|
+
category: "project",
|
|
283
|
+
severity: "error",
|
|
284
|
+
summary: "Missing asset",
|
|
285
|
+
explanation:
|
|
286
|
+
"A costume or sound path does not exist relative to the file that " +
|
|
287
|
+
"declares it, so the build cannot register the asset.",
|
|
288
|
+
},
|
|
289
|
+
FX702: {
|
|
290
|
+
id: "FX702",
|
|
291
|
+
category: "project",
|
|
292
|
+
severity: "warning",
|
|
293
|
+
summary: "Unsupported or inefficient asset format",
|
|
294
|
+
explanation:
|
|
295
|
+
"PenguinMod loads costumes as SVG, PNG or JPG and sounds as WAV or MP3. " +
|
|
296
|
+
"Convert other formats before building.",
|
|
297
|
+
},
|
|
298
|
+
FX703: {
|
|
299
|
+
id: "FX703",
|
|
300
|
+
category: "project",
|
|
301
|
+
severity: "warning",
|
|
302
|
+
summary: "Unused asset",
|
|
303
|
+
explanation:
|
|
304
|
+
"A file under `assets/` is never referenced by a costume or sound. " +
|
|
305
|
+
"Remove it to keep the repository small.",
|
|
306
|
+
},
|
|
307
|
+
FX706: {
|
|
308
|
+
id: "FX706",
|
|
309
|
+
category: "project",
|
|
310
|
+
severity: "warning",
|
|
311
|
+
summary: "Mutable or unpinned extension source",
|
|
312
|
+
explanation:
|
|
313
|
+
"A remote extension is not pinned to a verified hash, so a later build " +
|
|
314
|
+
"can silently load different code. Run `fx vendor install` to lock it.",
|
|
315
|
+
},
|
|
316
|
+
FX707: {
|
|
317
|
+
id: "FX707",
|
|
318
|
+
category: "project",
|
|
319
|
+
severity: "warning",
|
|
320
|
+
summary: "Stale vendored extension hash",
|
|
321
|
+
explanation:
|
|
322
|
+
"The vendored file differs from `fx.lock`. Run `fx vendor verify` for " +
|
|
323
|
+
"details and `fx vendor install` to refresh the lock.",
|
|
324
|
+
},
|
|
325
|
+
FX708: {
|
|
326
|
+
id: "FX708",
|
|
327
|
+
category: "project",
|
|
328
|
+
severity: "error",
|
|
329
|
+
summary: "Unavailable extension",
|
|
330
|
+
explanation:
|
|
331
|
+
"A declared extension has no vendored file or no lock entry, so builds " +
|
|
332
|
+
"that embed it fail. Run `fx vendor install`.",
|
|
333
|
+
},
|
|
334
|
+
FX709: {
|
|
335
|
+
id: "FX709",
|
|
336
|
+
category: "project",
|
|
337
|
+
severity: "error",
|
|
338
|
+
summary: "Unknown block or extension block",
|
|
339
|
+
explanation:
|
|
340
|
+
"A call does not name a catalog block, a list method or a declared " +
|
|
341
|
+
"function. Check the block name and that its extension is declared in " +
|
|
342
|
+
"`fx.yml`.",
|
|
343
|
+
},
|
|
344
|
+
FX803: {
|
|
345
|
+
id: "FX803",
|
|
346
|
+
category: "project",
|
|
347
|
+
severity: "error",
|
|
348
|
+
summary: "Partial sprite used without enabling partial sprites",
|
|
349
|
+
explanation:
|
|
350
|
+
"`partial sprite` fragments only merge when `fx.partialSprites: true` is " +
|
|
351
|
+
"set in `fx.yml`.",
|
|
352
|
+
},
|
|
353
|
+
FX804: {
|
|
354
|
+
id: "FX804",
|
|
355
|
+
category: "project",
|
|
356
|
+
severity: "warning",
|
|
357
|
+
summary: "Partial sprite has no base declaration",
|
|
358
|
+
explanation:
|
|
359
|
+
'Every sprite needs one plain `sprite "Name" { }` declaration to define ' +
|
|
360
|
+
"its costumes, sounds and layer. Partial fragments alone still build, " +
|
|
361
|
+
"but the sprite has no base properties.",
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
// --------------------------------------------------------- maintainability
|
|
365
|
+
FX901: {
|
|
366
|
+
id: "FX901",
|
|
367
|
+
category: "maintainability",
|
|
368
|
+
severity: "warning",
|
|
369
|
+
summary: "Excessive nesting",
|
|
370
|
+
explanation:
|
|
371
|
+
"Control blocks are nested more than four levels deep. Extract part of " +
|
|
372
|
+
"the body into a function so the shape stays readable.",
|
|
373
|
+
},
|
|
374
|
+
FX902: {
|
|
375
|
+
id: "FX902",
|
|
376
|
+
category: "maintainability",
|
|
377
|
+
severity: "warning",
|
|
378
|
+
summary: "Excessive script length",
|
|
379
|
+
explanation:
|
|
380
|
+
"A single hat or function holds more than 60 statements. Split it into " +
|
|
381
|
+
"smaller functions that each do one thing.",
|
|
382
|
+
},
|
|
383
|
+
FX908: {
|
|
384
|
+
id: "FX908",
|
|
385
|
+
category: "maintainability",
|
|
386
|
+
severity: "warning",
|
|
387
|
+
summary: "Missing documentation for public functions",
|
|
388
|
+
explanation:
|
|
389
|
+
"A function has no preceding `//` comment describing it. Add one line " +
|
|
390
|
+
"that says what the function does.",
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/** Look up a rule by ID, case-insensitively. */
|
|
395
|
+
export function ruleById(id) {
|
|
396
|
+
const key = String(id).trim().toUpperCase();
|
|
397
|
+
return RULES[key] ?? null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const MINIMAL = [
|
|
401
|
+
"FX101",
|
|
402
|
+
"FX201",
|
|
403
|
+
"FX202",
|
|
404
|
+
"FX204",
|
|
405
|
+
"FX212",
|
|
406
|
+
"FX214",
|
|
407
|
+
"FX307",
|
|
408
|
+
"FX404",
|
|
409
|
+
"FX803",
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
const RECOMMENDED = [
|
|
413
|
+
...MINIMAL,
|
|
414
|
+
"FX203",
|
|
415
|
+
"FX205",
|
|
416
|
+
"FX206",
|
|
417
|
+
"FX211",
|
|
418
|
+
"FX301",
|
|
419
|
+
"FX302",
|
|
420
|
+
"FX304",
|
|
421
|
+
"FX308",
|
|
422
|
+
"FX309",
|
|
423
|
+
"FX506",
|
|
424
|
+
"FX701",
|
|
425
|
+
"FX702",
|
|
426
|
+
"FX703",
|
|
427
|
+
"FX706",
|
|
428
|
+
"FX707",
|
|
429
|
+
"FX708",
|
|
430
|
+
"FX709",
|
|
431
|
+
"FX804",
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
const STRICT = [
|
|
435
|
+
...RECOMMENDED,
|
|
436
|
+
"FX207",
|
|
437
|
+
"FX208",
|
|
438
|
+
"FX209",
|
|
439
|
+
"FX403",
|
|
440
|
+
"FX901",
|
|
441
|
+
"FX902",
|
|
442
|
+
"FX908",
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Preset name -> enabled rule IDs. `all` deliberately lists every known rule
|
|
447
|
+
* (including experimental ones added later), so a new rule only has to be
|
|
448
|
+
* registered above to join it.
|
|
449
|
+
*/
|
|
450
|
+
/** Rules that describe lint machinery itself; always on, never configurable. */
|
|
451
|
+
export const META_RULE_IDS = ["FX000", "FX001", "FX002"];
|
|
452
|
+
|
|
453
|
+
export const PRESETS = {
|
|
454
|
+
minimal: MINIMAL,
|
|
455
|
+
recommended: RECOMMENDED,
|
|
456
|
+
strict: STRICT,
|
|
457
|
+
all: Object.keys(RULES).filter((id) => !META_RULE_IDS.includes(id)),
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/** Preset names in escalation order. */
|
|
461
|
+
export const PRESET_NAMES = ["minimal", "recommended", "strict", "all"];
|
package/src/linter.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triflare Fx linter (Node entry point).
|
|
3
|
+
*
|
|
4
|
+
* The rules that only need source text live in `lint/analyze.js` so browser
|
|
5
|
+
* tooling can reuse them. This module adds the checks that need the
|
|
6
|
+
* filesystem: asset files, `assets/` usage and vendored extension hashes.
|
|
7
|
+
* Both sets of diagnostics share severity handling, suppressions and ordering.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { parse } from "./parser.js";
|
|
13
|
+
import { LOCK_FILENAME, readLock, verifyVendor } from "./vendor.js";
|
|
14
|
+
import {
|
|
15
|
+
META_RULES,
|
|
16
|
+
applyFixes,
|
|
17
|
+
lintSources as lintAnalyze,
|
|
18
|
+
sortDiagnostics,
|
|
19
|
+
} from "./lint/analyze.js";
|
|
20
|
+
|
|
21
|
+
export { META_RULES, applyFixes, sortDiagnostics };
|
|
22
|
+
|
|
23
|
+
/** Costume and sound formats PenguinMod can load. */
|
|
24
|
+
const COSTUME_FORMATS = new Set(["svg", "png", "jpg"]);
|
|
25
|
+
const SOUND_FORMATS = new Set(["wav", "mp3"]);
|
|
26
|
+
|
|
27
|
+
/** Walk every body list in a parsed program (stage, sprites, top items). */
|
|
28
|
+
function eachBody(ast, visit) {
|
|
29
|
+
if (!ast) return;
|
|
30
|
+
visit(ast.topItems ?? []);
|
|
31
|
+
if (ast.stage) visit(ast.stage.body ?? []);
|
|
32
|
+
for (const sprite of ast.sprites ?? []) visit(sprite.body ?? []);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Raw diagnostics for a project rule problem. */
|
|
36
|
+
function problem(ruleId, file, message, length = 1) {
|
|
37
|
+
return { ruleId, file, line: 1, column: 1, message, length };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** FX701/FX702/FX703: asset references on disk. */
|
|
41
|
+
function assetDiagnostics({ sources = [], configPath = null, rootDir = null }) {
|
|
42
|
+
const diagnostics = [];
|
|
43
|
+
const referenced = new Set();
|
|
44
|
+
|
|
45
|
+
for (const { file, source } of sources) {
|
|
46
|
+
let ast;
|
|
47
|
+
try {
|
|
48
|
+
ast = parse(source, file);
|
|
49
|
+
} catch {
|
|
50
|
+
continue; // FX000 already covers the parse error.
|
|
51
|
+
}
|
|
52
|
+
eachBody(ast, (body) => {
|
|
53
|
+
for (const item of body) {
|
|
54
|
+
if (item.type !== "PropertyDecl") continue;
|
|
55
|
+
if (item.key !== "costumes" && item.key !== "sounds") continue;
|
|
56
|
+
const formats =
|
|
57
|
+
item.key === "costumes" ? COSTUME_FORMATS : SOUND_FORMATS;
|
|
58
|
+
for (const entry of item.value) {
|
|
59
|
+
const holder = entry.loc?.file ?? file;
|
|
60
|
+
const absolute = path.resolve(path.dirname(holder), entry.value);
|
|
61
|
+
referenced.add(absolute);
|
|
62
|
+
if (!fs.existsSync(absolute)) {
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
ruleId: "FX701",
|
|
65
|
+
file: entry.loc?.file ?? file,
|
|
66
|
+
line: entry.loc?.line ?? 1,
|
|
67
|
+
column: entry.loc?.column ?? 1,
|
|
68
|
+
length: String(entry.value).length,
|
|
69
|
+
message: `Asset ${JSON.stringify(entry.value)} does not exist.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const extension = path.extname(entry.value).slice(1).toLowerCase();
|
|
73
|
+
if (extension && !formats.has(extension)) {
|
|
74
|
+
diagnostics.push({
|
|
75
|
+
ruleId: "FX702",
|
|
76
|
+
file: entry.loc?.file ?? file,
|
|
77
|
+
line: entry.loc?.line ?? 1,
|
|
78
|
+
column: entry.loc?.column ?? 1,
|
|
79
|
+
length: String(entry.value).length,
|
|
80
|
+
message: `Asset ${JSON.stringify(entry.value)} uses .${extension}; use ${[...formats].map((format) => format.toUpperCase()).join(", ")}.`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The walk and the referenced set both use absolute paths, so resolve the
|
|
89
|
+
// project directory even when the caller passed it relative to the cwd.
|
|
90
|
+
const assetsDir = rootDir ? path.resolve(rootDir, "assets") : null;
|
|
91
|
+
if (
|
|
92
|
+
assetsDir &&
|
|
93
|
+
fs.existsSync(assetsDir) &&
|
|
94
|
+
fs.statSync(assetsDir).isDirectory()
|
|
95
|
+
) {
|
|
96
|
+
const walk = (dir) => {
|
|
97
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
98
|
+
if (entry.name.startsWith(".")) continue;
|
|
99
|
+
const full = path.join(dir, entry.name);
|
|
100
|
+
if (entry.isDirectory()) walk(full);
|
|
101
|
+
else if (entry.isFile() && !referenced.has(full)) {
|
|
102
|
+
diagnostics.push({
|
|
103
|
+
ruleId: "FX703",
|
|
104
|
+
file: full,
|
|
105
|
+
line: 1,
|
|
106
|
+
column: 1,
|
|
107
|
+
message: `Asset ${JSON.stringify(path.relative(assetsDir, full))} is never used by a costume or sound.`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
walk(assetsDir);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// `configPath` is unused for assets but keeps the call site symmetric.
|
|
116
|
+
void configPath;
|
|
117
|
+
return diagnostics;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** FX706/FX707/FX708: extension sources and `fx.lock` verification. */
|
|
121
|
+
function extensionDiagnostics({
|
|
122
|
+
config = null,
|
|
123
|
+
configPath = null,
|
|
124
|
+
configDir = null,
|
|
125
|
+
}) {
|
|
126
|
+
const vendor = config?.vendor ?? [];
|
|
127
|
+
const legacy = config?.project?.extensions ?? [];
|
|
128
|
+
if (vendor.length === 0 && legacy.length === 0) return [];
|
|
129
|
+
|
|
130
|
+
const diagnostics = [];
|
|
131
|
+
const file = configPath ?? null;
|
|
132
|
+
const lock = (() => {
|
|
133
|
+
try {
|
|
134
|
+
return readLock(configDir);
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
|
|
140
|
+
for (const entry of vendor) {
|
|
141
|
+
if (!entry.remote) continue;
|
|
142
|
+
const locked = lock?.extensions?.[entry.name]?.sha256;
|
|
143
|
+
if (!locked || entry.volatile) {
|
|
144
|
+
diagnostics.push(
|
|
145
|
+
problem(
|
|
146
|
+
"FX706",
|
|
147
|
+
file,
|
|
148
|
+
`Extension ${JSON.stringify(entry.name)} loads mutable source ${JSON.stringify(entry.source)}; pin it with \`fx vendor install\`.`,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const source of legacy) {
|
|
154
|
+
if (/^https?:\/\//i.test(String(source))) {
|
|
155
|
+
diagnostics.push(
|
|
156
|
+
problem(
|
|
157
|
+
"FX706",
|
|
158
|
+
file,
|
|
159
|
+
`project.extensions loads mutable source ${JSON.stringify(String(source))}; move it under \`vendor:\` and pin it.`,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!configDir || !config) return diagnostics;
|
|
166
|
+
let report;
|
|
167
|
+
try {
|
|
168
|
+
report = verifyVendor({ configDir, config });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
diagnostics.push(problem("FX708", file, error.message));
|
|
171
|
+
return diagnostics;
|
|
172
|
+
}
|
|
173
|
+
for (const record of report.entries ?? []) {
|
|
174
|
+
if (record.status === "missing" || record.status === "unlocked") {
|
|
175
|
+
diagnostics.push(
|
|
176
|
+
problem(
|
|
177
|
+
"FX708",
|
|
178
|
+
file,
|
|
179
|
+
`Extension ${JSON.stringify(record.name)} is unavailable in ${record.file ?? "the vendor directory"}; run \`fx vendor install\`.`,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
} else if (
|
|
183
|
+
record.status === "mismatch" ||
|
|
184
|
+
record.status === "source-changed"
|
|
185
|
+
) {
|
|
186
|
+
diagnostics.push(
|
|
187
|
+
problem(
|
|
188
|
+
"FX707",
|
|
189
|
+
file,
|
|
190
|
+
`Extension ${JSON.stringify(record.name)} does not match \`${LOCK_FILENAME}\`; run \`fx vendor verify\` for details.`,
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return diagnostics;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Lint sources plus the surrounding project.
|
|
200
|
+
*
|
|
201
|
+
* @param {{
|
|
202
|
+
* sources: Array<{file: string, source: string}>,
|
|
203
|
+
* config?: object|null,
|
|
204
|
+
* configPath?: string|null,
|
|
205
|
+
* configDir?: string|null,
|
|
206
|
+
* rootDir?: string|null,
|
|
207
|
+
* extensionCatalog?: Record<string, object>,
|
|
208
|
+
* declaredExtensions?: string[],
|
|
209
|
+
* lintConfig?: object,
|
|
210
|
+
* strict?: boolean,
|
|
211
|
+
* overrides?: Record<string, string>,
|
|
212
|
+
* }} options
|
|
213
|
+
*/
|
|
214
|
+
export function lintSources(options = {}) {
|
|
215
|
+
const extraDiagnostics = [
|
|
216
|
+
...assetDiagnostics(options),
|
|
217
|
+
...extensionDiagnostics(options),
|
|
218
|
+
];
|
|
219
|
+
return lintAnalyze({ ...options, extraDiagnostics });
|
|
220
|
+
}
|