@gesslar/sassy 0.20.0 → 0.21.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/package.json +12 -8
- package/src/BuildCommand.js +8 -9
- package/src/Cache.js +1 -0
- package/src/Colour.js +8 -2
- package/src/Command.js +92 -32
- package/src/Compiler.js +62 -36
- package/src/Data.js +24 -14
- package/src/Evaluator.js +8 -4
- package/src/File.js +14 -2
- package/src/LintCommand.js +381 -103
- package/src/ResolveCommand.js +44 -27
- package/src/Sass.js +2 -1
- package/src/Session.js +211 -42
- package/src/Term.js +7 -7
- package/src/Theme.js +378 -53
- package/src/ThemePool.js +1 -1
- package/src/ThemeToken.js +1 -0
- package/src/Type.js +11 -10
- package/src/Util.js +2 -2
- package/src/Valid.js +1 -1
- package/src/cli.js +27 -15
package/src/LintCommand.js
CHANGED
|
@@ -26,7 +26,6 @@ import Term from "./Term.js"
|
|
|
26
26
|
import Theme from "./Theme.js"
|
|
27
27
|
import ThemePool from "./ThemePool.js"
|
|
28
28
|
|
|
29
|
-
|
|
30
29
|
// oops, need to have @gesslar/colours support this, too!
|
|
31
30
|
// ansiColors.enabled = colorSupport.hasBasic
|
|
32
31
|
|
|
@@ -45,13 +44,13 @@ export default class LintCommand extends Command {
|
|
|
45
44
|
constructor(base) {
|
|
46
45
|
super(base)
|
|
47
46
|
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
47
|
+
this.setCliCommand("lint <file>")
|
|
48
|
+
this.setCliOptions({
|
|
50
49
|
// Future options could include:
|
|
51
50
|
// "fix": ["-f, --fix", "automatically fix issues where possible"],
|
|
52
51
|
// "strict": ["--strict", "treat warnings as errors"],
|
|
53
52
|
// "format": ["--format <type>", "output format (text, json)", "text"],
|
|
54
|
-
}
|
|
53
|
+
})
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
/**
|
|
@@ -63,71 +62,185 @@ export default class LintCommand extends Command {
|
|
|
63
62
|
* @returns {Promise<void>} Resolves when linting is complete
|
|
64
63
|
*/
|
|
65
64
|
async execute(inputArg, options = {}) {
|
|
66
|
-
const
|
|
65
|
+
const cwd = this.getCwd()
|
|
67
66
|
const fileObject = await this.resolveThemeFileName(inputArg, cwd)
|
|
68
67
|
const theme = new Theme(fileObject, cwd, options)
|
|
69
68
|
|
|
70
|
-
theme.
|
|
71
|
-
|
|
69
|
+
theme.setCache(this.getCache())
|
|
72
70
|
await theme.load()
|
|
73
71
|
await theme.build()
|
|
74
72
|
|
|
75
|
-
const issues = await this
|
|
73
|
+
const issues = await this.#lintTheme(theme)
|
|
76
74
|
|
|
77
|
-
this
|
|
75
|
+
this.#reportIssues(issues)
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
79
|
+
* Public method to lint a theme and return structured results for external
|
|
80
|
+
* consumption.
|
|
81
|
+
*
|
|
82
|
+
* Returns categorized lint results for tokenColors, semanticTokenColors, and colors.
|
|
83
83
|
*
|
|
84
84
|
* @param {Theme} theme - The compiled theme object
|
|
85
|
-
* @returns {Promise<
|
|
85
|
+
* @returns {Promise<object>} Object containing categorized lint results
|
|
86
86
|
*/
|
|
87
|
-
async
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
async lint(theme) {
|
|
88
|
+
const results = {
|
|
89
|
+
tokenColors: [],
|
|
90
|
+
semanticTokenColors: [],
|
|
91
|
+
colors: [],
|
|
92
|
+
variables: []
|
|
93
|
+
}
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
const pool = theme.getPool()
|
|
96
|
+
|
|
97
|
+
// Always perform structural linting (works with or without pool)
|
|
98
|
+
if(theme.getOutput()?.tokenColors)
|
|
99
|
+
results.tokenColors.push(
|
|
100
|
+
...this.#lintTokenColorsStructure(theme.getOutput().tokenColors)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Only perform variable-dependent linting if pool exists
|
|
104
|
+
if(pool) {
|
|
105
|
+
// Get source data for variable analysis
|
|
106
|
+
const colors = await this.getColors(theme)
|
|
107
|
+
const tokenColors = await this.#getTokenColors(theme)
|
|
108
|
+
const semanticTokenColors = await this.getSemanticTokenColors(theme)
|
|
109
|
+
|
|
110
|
+
// Variable-dependent linting
|
|
111
|
+
results.colors.push(...this.#lintColors(colors, pool))
|
|
112
|
+
results.tokenColors.push(...this.lintTokenColors(tokenColors, pool))
|
|
113
|
+
results.semanticTokenColors.push(
|
|
114
|
+
...this.#lintSemanticTokenColors(semanticTokenColors, pool)
|
|
115
|
+
)
|
|
116
|
+
results.variables.push(...this.#lintVariables(theme, pool))
|
|
117
|
+
}
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
return results
|
|
120
|
+
}
|
|
97
121
|
|
|
98
|
-
|
|
99
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Performs structural linting of tokenColors that doesn't require variable
|
|
124
|
+
* information.
|
|
125
|
+
*
|
|
126
|
+
* Checks for duplicate scopes and precedence issues.
|
|
127
|
+
*
|
|
128
|
+
* @param {Array} tokenColors - Array of tokenColor entries
|
|
129
|
+
* @returns {Array} Array of structural issues
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
#lintTokenColorsStructure(tokenColors) {
|
|
133
|
+
return [
|
|
134
|
+
...this.#checkDuplicateScopes(tokenColors),
|
|
135
|
+
...this.#checkPrecedenceIssues(tokenColors),
|
|
136
|
+
]
|
|
137
|
+
}
|
|
100
138
|
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Performs variable-dependent linting of tokenColors data.
|
|
141
|
+
* Checks for undefined variable references.
|
|
142
|
+
*
|
|
143
|
+
* @param {Array} sourceTokenColors - Array of source tokenColor entries
|
|
144
|
+
* @param {ThemePool} pool - The theme's variable pool
|
|
145
|
+
* @returns {Array} Array of variable-related issues
|
|
146
|
+
*/
|
|
147
|
+
lintTokenColors(sourceTokenColors, pool) {
|
|
148
|
+
return pool
|
|
149
|
+
? this.#checkUndefinedVariables(sourceTokenColors, pool, "tokenColors")
|
|
150
|
+
: []
|
|
151
|
+
}
|
|
103
152
|
|
|
104
|
-
|
|
105
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Performs variable-dependent linting of semanticTokenColors data.
|
|
155
|
+
* Checks for undefined variable references.
|
|
156
|
+
*
|
|
157
|
+
* @param {Array} semanticTokenColors - Array of source semanticTokenColors entries
|
|
158
|
+
* @param {ThemePool} pool - The theme's variable pool
|
|
159
|
+
* @returns {Array} Array of variable-related issues
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
#lintSemanticTokenColors(semanticTokenColors, pool) {
|
|
163
|
+
return pool && semanticTokenColors.length > 0
|
|
164
|
+
? this.#checkUndefinedVariables(semanticTokenColors, pool, "semanticTokenColors")
|
|
165
|
+
: []
|
|
166
|
+
}
|
|
106
167
|
|
|
107
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Performs variable-dependent linting of colors data.
|
|
170
|
+
* Checks for undefined variable references.
|
|
171
|
+
*
|
|
172
|
+
* @param {Array} sourceColors - Array of source colors entries
|
|
173
|
+
* @param {ThemePool} pool - The theme's variable pool
|
|
174
|
+
* @returns {Array} Array of variable-related issues
|
|
175
|
+
* @private
|
|
176
|
+
*/
|
|
177
|
+
#lintColors(sourceColors, pool) {
|
|
178
|
+
return pool && sourceColors.length > 0
|
|
179
|
+
? this.#checkUndefinedVariables(sourceColors, pool, "colors")
|
|
180
|
+
: []
|
|
108
181
|
}
|
|
109
182
|
|
|
110
183
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
184
|
+
* Performs variable-dependent linting for unused variables.
|
|
185
|
+
* Checks for variables defined but never used.
|
|
186
|
+
*
|
|
187
|
+
* @param {Theme} theme - The theme object
|
|
188
|
+
* @param {ThemePool} pool - The theme's variable pool
|
|
189
|
+
* @returns {Array} Array of unused variable issues
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
#lintVariables(theme, pool) {
|
|
193
|
+
return pool
|
|
194
|
+
? this.#checkUnusedVariables(theme, pool)
|
|
195
|
+
: []
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Performs comprehensive linting of a theme.
|
|
200
|
+
* Returns an array of issues found during validation.
|
|
113
201
|
*
|
|
114
202
|
* @param {Theme} theme - The compiled theme object
|
|
115
|
-
* @returns {Promise<Array>} Array of
|
|
203
|
+
* @returns {Promise<Array>} Array of lint issues
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
async #lintTheme(theme) {
|
|
207
|
+
const results = await this.lint(theme)
|
|
208
|
+
|
|
209
|
+
// Flatten all results into a single array for backward compatibility
|
|
210
|
+
return [
|
|
211
|
+
...results.tokenColors,
|
|
212
|
+
...results.semanticTokenColors,
|
|
213
|
+
...results.colors,
|
|
214
|
+
...results.variables
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extracts the original source tokenColors data from theme.source and
|
|
220
|
+
* dependencies.
|
|
221
|
+
*
|
|
222
|
+
* Used for variable analysis since we need the uncompiled data with variable
|
|
223
|
+
* references.
|
|
224
|
+
*
|
|
225
|
+
* @param {Theme} theme - The compiled theme object (contains both output and source)
|
|
226
|
+
* @returns {Promise<Array>} Array of source tokenColors entries with variables intact
|
|
227
|
+
* @private
|
|
116
228
|
*/
|
|
117
|
-
async
|
|
229
|
+
async #getTokenColors(theme) {
|
|
118
230
|
const sourceTokenColors = []
|
|
119
231
|
|
|
120
232
|
// Get tokenColors from main theme source
|
|
121
|
-
if(theme.
|
|
122
|
-
sourceTokenColors.push(...theme.
|
|
233
|
+
if(theme.getSource()?.theme?.tokenColors)
|
|
234
|
+
sourceTokenColors.push(...theme.getSource().theme.tokenColors)
|
|
123
235
|
|
|
124
236
|
// Get tokenColors from imported files
|
|
125
|
-
if(theme.
|
|
126
|
-
for(const dependency of theme.
|
|
237
|
+
if(theme.hasDependencies()) {
|
|
238
|
+
for(const dependency of theme.getDependencies()) {
|
|
127
239
|
// Skip main file, already processed
|
|
128
|
-
if(dependency.path !== theme.
|
|
240
|
+
if(dependency.getSourceFile().path !== theme.getSourceFile().path) {
|
|
129
241
|
try {
|
|
130
|
-
const depData = await theme.
|
|
242
|
+
const depData = await theme.getCache().loadCachedData(dependency.getSourceFile())
|
|
243
|
+
|
|
131
244
|
if(depData?.theme?.tokenColors)
|
|
132
245
|
sourceTokenColors.push(...depData.theme.tokenColors)
|
|
133
246
|
} catch {
|
|
@@ -140,14 +253,90 @@ export default class LintCommand extends Command {
|
|
|
140
253
|
return sourceTokenColors
|
|
141
254
|
}
|
|
142
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Extracts the original source semanticTokenColors data from theme.source
|
|
258
|
+
* and dependencies.
|
|
259
|
+
*
|
|
260
|
+
* Used for variable analysis since we need the uncompiled data with variable
|
|
261
|
+
* references.
|
|
262
|
+
*
|
|
263
|
+
* @param {Theme} theme - The compiled theme object (contains both output and source)
|
|
264
|
+
* @returns {Promise<Array>} Array of source semanticTokenColors entries with variables intact
|
|
265
|
+
*/
|
|
266
|
+
async getSemanticTokenColors(theme) {
|
|
267
|
+
const sourceSemanticTokenColors = []
|
|
268
|
+
|
|
269
|
+
// Get semanticTokenColors from main theme source
|
|
270
|
+
if(theme.sourceHasSemanticTokenColors())
|
|
271
|
+
sourceSemanticTokenColors.push(theme.getSourceSemanticTokenColors())
|
|
272
|
+
|
|
273
|
+
// Get semanticTokenColors from imported files
|
|
274
|
+
if(theme.hasDependencies()) {
|
|
275
|
+
for(const dependency of theme.getDependencies()) {
|
|
276
|
+
// Skip main file, already processed
|
|
277
|
+
if(dependency.getSourceFile().path !== theme.getSourceFile().path) {
|
|
278
|
+
try {
|
|
279
|
+
const depData = await theme.getCache().loadCachedData(dependency.getSourceFile())
|
|
280
|
+
|
|
281
|
+
if(depData?.theme?.semanticTokenColors)
|
|
282
|
+
sourceSemanticTokenColors.push(depData.theme.semanticTokenColors)
|
|
283
|
+
} catch {
|
|
284
|
+
// nothing to see here.
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return sourceSemanticTokenColors
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Extracts the original source colors data from theme.source and
|
|
295
|
+
* dependencies.
|
|
296
|
+
*
|
|
297
|
+
* Used for variable analysis since we need the uncompiled data with variable
|
|
298
|
+
* references.
|
|
299
|
+
*
|
|
300
|
+
* @param {Theme} theme - The compiled theme object (contains both output and source)
|
|
301
|
+
* @returns {Promise<Array>} Array of source colors entries with variables intact
|
|
302
|
+
*/
|
|
303
|
+
async getColors(theme) {
|
|
304
|
+
const sourceColors = []
|
|
305
|
+
|
|
306
|
+
// Get colors from main theme source
|
|
307
|
+
if(theme.getSource()?.theme?.colors)
|
|
308
|
+
sourceColors.push(theme.getSource().theme.colors)
|
|
309
|
+
|
|
310
|
+
// Get colors from imported files
|
|
311
|
+
if(theme.hasDependencies()) {
|
|
312
|
+
for(const dependency of theme.getDependencies()) {
|
|
313
|
+
// Skip main file, already processed
|
|
314
|
+
if(dependency.getSourceFile().path !== theme.getSourceFile().path) {
|
|
315
|
+
try {
|
|
316
|
+
const depData = await theme.getCache().loadCachedData(dependency.getSourceFile())
|
|
317
|
+
|
|
318
|
+
if(depData?.theme?.colors)
|
|
319
|
+
sourceColors.push(depData.theme.colors)
|
|
320
|
+
} catch {
|
|
321
|
+
// nothing to see here.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return sourceColors
|
|
328
|
+
}
|
|
329
|
+
|
|
143
330
|
/**
|
|
144
331
|
* Reports lint issues to the user with appropriate formatting and colors.
|
|
145
332
|
*
|
|
146
333
|
* @param {Array} issues - Array of lint issues to report
|
|
334
|
+
* @private
|
|
147
335
|
*/
|
|
148
|
-
reportIssues(issues) {
|
|
336
|
+
#reportIssues(issues) {
|
|
149
337
|
if(issues.length === 0) {
|
|
150
338
|
Term.info(c`{success}✓{/} No linting issues found`)
|
|
339
|
+
|
|
151
340
|
return
|
|
152
341
|
}
|
|
153
342
|
|
|
@@ -156,7 +345,8 @@ export default class LintCommand extends Command {
|
|
|
156
345
|
const infos = issues.filter(i => i.severity === "low")
|
|
157
346
|
|
|
158
347
|
const allIssues = errors.concat(warnings, infos)
|
|
159
|
-
|
|
348
|
+
|
|
349
|
+
allIssues.forEach(issue => this.#reportSingleIssue(issue))
|
|
160
350
|
|
|
161
351
|
// Clean summary
|
|
162
352
|
const parts = []
|
|
@@ -173,6 +363,13 @@ export default class LintCommand extends Command {
|
|
|
173
363
|
Term.info(`\n${parts.join(", ")}`)
|
|
174
364
|
}
|
|
175
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Returns a colour-coded bullet indicator for a given severity level.
|
|
368
|
+
*
|
|
369
|
+
* @private
|
|
370
|
+
* @param {"high"|"medium"|"low"} severity - Severity level to represent
|
|
371
|
+
* @returns {string} A pre-coloured "●" character for terminal output
|
|
372
|
+
*/
|
|
176
373
|
#getIndicator(severity) {
|
|
177
374
|
switch(severity) {
|
|
178
375
|
case "high": return c`{error}●{/}`
|
|
@@ -186,19 +383,23 @@ export default class LintCommand extends Command {
|
|
|
186
383
|
* Reports a single lint issue with clean, minimal formatting.
|
|
187
384
|
*
|
|
188
385
|
* @param {object} issue - The issue to report
|
|
386
|
+
* @private
|
|
189
387
|
*/
|
|
190
|
-
reportSingleIssue(issue) {
|
|
388
|
+
#reportSingleIssue(issue) {
|
|
191
389
|
const indicator = this.#getIndicator(issue.severity)
|
|
192
390
|
|
|
193
391
|
switch(issue.type) {
|
|
194
392
|
case "duplicate-scope": {
|
|
195
393
|
const rules = issue.occurrences.map(occ => `{loc}'${occ.name}{/}'`).join(", ")
|
|
394
|
+
|
|
196
395
|
Term.info(c`${indicator} Scope '{context}${issue.scope}{/}' is duplicated in ${rules}`)
|
|
197
396
|
break
|
|
198
397
|
}
|
|
199
398
|
|
|
200
399
|
case "undefined-variable": {
|
|
201
|
-
|
|
400
|
+
const sectionInfo = issue.section && issue.section !== "tokenColors" ? ` in ${issue.section}` : ""
|
|
401
|
+
|
|
402
|
+
Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is used but not defined in '${issue.rule}' (${issue.property} property)${sectionInfo}`)
|
|
202
403
|
break
|
|
203
404
|
}
|
|
204
405
|
|
|
@@ -225,8 +426,9 @@ export default class LintCommand extends Command {
|
|
|
225
426
|
*
|
|
226
427
|
* @param {Array} tokenColors - Array of tokenColors entries
|
|
227
428
|
* @returns {Array} Array of duplicate scope issues
|
|
429
|
+
* @private
|
|
228
430
|
*/
|
|
229
|
-
checkDuplicateScopes(tokenColors) {
|
|
431
|
+
#checkDuplicateScopes(tokenColors) {
|
|
230
432
|
const issues = []
|
|
231
433
|
const scopeOccurrences = new Map()
|
|
232
434
|
|
|
@@ -235,6 +437,7 @@ export default class LintCommand extends Command {
|
|
|
235
437
|
return
|
|
236
438
|
|
|
237
439
|
const scopes = entry.scope.split(",").map(s => s.trim())
|
|
440
|
+
|
|
238
441
|
scopes.forEach(scope => {
|
|
239
442
|
if(!scopeOccurrences.has(scope)) {
|
|
240
443
|
scopeOccurrences.set(scope, [])
|
|
@@ -264,73 +467,131 @@ export default class LintCommand extends Command {
|
|
|
264
467
|
}
|
|
265
468
|
|
|
266
469
|
/**
|
|
267
|
-
* Checks for undefined variables referenced in
|
|
470
|
+
* Checks for undefined variables referenced in theme data.
|
|
268
471
|
* Returns issues for variables that are used but not defined.
|
|
269
472
|
*
|
|
270
|
-
* @param {Array}
|
|
473
|
+
* @param {Array|object} themeData - Array of entries or object containing theme data
|
|
271
474
|
* @param {ThemePool} pool - The theme's variable pool
|
|
475
|
+
* @param {string} section - The section name (tokenColors, semanticTokenColors, colors)
|
|
272
476
|
* @returns {Array} Array of undefined variable issues
|
|
477
|
+
* @private
|
|
273
478
|
*/
|
|
274
|
-
checkUndefinedVariables(
|
|
479
|
+
#checkUndefinedVariables(themeData, pool, section = "tokenColors") {
|
|
275
480
|
const issues = []
|
|
276
|
-
const definedVars = pool ? new Set(pool.getTokens.keys()) : new Set()
|
|
277
|
-
|
|
278
|
-
tokenColors.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
481
|
+
const definedVars = pool ? new Set(pool.getTokens().keys()) : new Set()
|
|
482
|
+
|
|
483
|
+
if(section === "tokenColors" && Array.isArray(themeData)) {
|
|
484
|
+
themeData.forEach((entry, index) => {
|
|
485
|
+
const settings = entry.settings || {}
|
|
486
|
+
|
|
487
|
+
for(const [key, value] of Object.entries(settings)) {
|
|
488
|
+
if(typeof value === "string") {
|
|
489
|
+
const {none,parens,braces} = Evaluator.sub.exec(value)?.groups ?? {}
|
|
490
|
+
const varName = none || parens || braces
|
|
491
|
+
|
|
492
|
+
if(!varName)
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
if(!definedVars.has(varName)) {
|
|
496
|
+
issues.push({
|
|
497
|
+
type: "undefined-variable",
|
|
498
|
+
severity: "high",
|
|
499
|
+
variable: value,
|
|
500
|
+
rule: entry.name || `Entry ${index + 1}`,
|
|
501
|
+
property: key,
|
|
502
|
+
section
|
|
503
|
+
})
|
|
504
|
+
}
|
|
296
505
|
}
|
|
297
506
|
}
|
|
298
|
-
}
|
|
299
|
-
})
|
|
507
|
+
})
|
|
508
|
+
} else if((section === "semanticTokenColors" || section === "colors") && Array.isArray(themeData)) {
|
|
509
|
+
// Handle semanticTokenColors and colors as objects
|
|
510
|
+
themeData.forEach((dataObject, objIndex) => {
|
|
511
|
+
if(dataObject && typeof dataObject === "object") {
|
|
512
|
+
this.#checkObjectForUndefinedVariables(dataObject, definedVars, issues, section, `Object ${objIndex + 1}`)
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
}
|
|
300
516
|
|
|
301
517
|
return issues
|
|
302
518
|
}
|
|
303
519
|
|
|
304
520
|
/**
|
|
305
|
-
*
|
|
521
|
+
* Recursively checks an object for undefined variable references.
|
|
522
|
+
*
|
|
523
|
+
* @param {object} obj - The object to check
|
|
524
|
+
* @param {Set} definedVars - Set of defined variable names
|
|
525
|
+
* @param {Array} issues - Array to push issues to
|
|
526
|
+
* @param {string} section - The section name
|
|
527
|
+
* @param {string} ruleName - The rule/object name for reporting
|
|
528
|
+
* @param {string} path - The current path in the object (for nested properties)
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
#checkObjectForUndefinedVariables(obj, definedVars, issues, section, ruleName, path = "") {
|
|
532
|
+
for(const [key, value] of Object.entries(obj)) {
|
|
533
|
+
const currentPath = path ? `${path}.${key}` : key
|
|
534
|
+
|
|
535
|
+
if(typeof value === "string") {
|
|
536
|
+
const {none, parens, braces} = Evaluator.sub.exec(value)?.groups ?? {}
|
|
537
|
+
const varName = none || parens || braces
|
|
538
|
+
|
|
539
|
+
if(varName && !definedVars.has(varName)) {
|
|
540
|
+
issues.push({
|
|
541
|
+
type: "undefined-variable",
|
|
542
|
+
severity: "high",
|
|
543
|
+
variable: value,
|
|
544
|
+
rule: ruleName,
|
|
545
|
+
property: currentPath,
|
|
546
|
+
section
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
} else if(value && typeof value === "object" && !Array.isArray(value)) {
|
|
550
|
+
this.#checkObjectForUndefinedVariables(
|
|
551
|
+
value, definedVars, issues, section, ruleName, currentPath
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Checks for unused variables defined in vars section but not referenced in
|
|
559
|
+
* theme content.
|
|
560
|
+
*
|
|
306
561
|
* Returns issues for variables that are defined in vars but never used.
|
|
307
562
|
*
|
|
308
563
|
* @param {Theme} theme - The compiled theme object
|
|
309
564
|
* @param {ThemePool} pool - The theme's variable pool
|
|
310
565
|
* @returns {Array} Array of unused variable issues
|
|
566
|
+
* @private
|
|
311
567
|
*/
|
|
312
|
-
checkUnusedVariables(theme, pool) {
|
|
568
|
+
#checkUnusedVariables(theme, pool) {
|
|
313
569
|
const issues = []
|
|
314
570
|
|
|
315
|
-
if(!pool || !theme.
|
|
571
|
+
if(!pool || !theme.getSource())
|
|
316
572
|
return issues
|
|
317
573
|
|
|
318
574
|
// Get variables defined in the vars section only
|
|
319
575
|
const definedVars = new Map()
|
|
320
|
-
const
|
|
321
|
-
const mainFile = new FileObject(theme.
|
|
576
|
+
const cwd = this.getCwd()
|
|
577
|
+
const mainFile = new FileObject(theme.getSourceFile().path)
|
|
322
578
|
const relativeMainPath = File.relativeOrAbsolutePath(cwd, mainFile)
|
|
323
|
-
|
|
579
|
+
|
|
580
|
+
this.#collectVarsDefinitions(theme.getSource().vars, definedVars, "", relativeMainPath)
|
|
324
581
|
|
|
325
582
|
// Also check dependencies for vars definitions
|
|
326
|
-
if(theme.
|
|
327
|
-
for(const dependency of theme.
|
|
583
|
+
if(theme.hasDependencies()) {
|
|
584
|
+
for(const dependency of theme.getDependencies()) {
|
|
328
585
|
try {
|
|
329
|
-
const depData = theme.
|
|
586
|
+
const depData = theme.getCache()?.loadCachedDataSync?.(dependency.getSourceFile())
|
|
587
|
+
|
|
330
588
|
if(depData?.vars) {
|
|
331
|
-
const depFile = new FileObject(dependency.path)
|
|
332
|
-
const relativeDependencyPath = File.relativeOrAbsolutePath(
|
|
333
|
-
|
|
589
|
+
const depFile = new FileObject(dependency.getSourceFile().path)
|
|
590
|
+
const relativeDependencyPath = File.relativeOrAbsolutePath(
|
|
591
|
+
cwd, depFile
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
this.#collectVarsDefinitions(depData.vars, definedVars, "", relativeDependencyPath)
|
|
334
595
|
}
|
|
335
596
|
} catch {
|
|
336
597
|
// Ignore cache errors
|
|
@@ -341,32 +602,35 @@ export default class LintCommand extends Command {
|
|
|
341
602
|
const usedVars = new Set()
|
|
342
603
|
|
|
343
604
|
// Find variable usage in colors, tokenColors, and semanticColors sections
|
|
344
|
-
|
|
345
|
-
|
|
605
|
+
const themeSource = theme.getSource()
|
|
606
|
+
|
|
607
|
+
if(themeSource?.colors) {
|
|
608
|
+
this.#findVariableUsage(themeSource.colors, usedVars)
|
|
346
609
|
}
|
|
347
610
|
|
|
348
|
-
if(
|
|
349
|
-
this
|
|
611
|
+
if(themeSource?.tokenColors) {
|
|
612
|
+
this.#findVariableUsage(themeSource.tokenColors, usedVars)
|
|
350
613
|
}
|
|
351
614
|
|
|
352
|
-
if(
|
|
353
|
-
this
|
|
615
|
+
if(themeSource?.semanticColors) {
|
|
616
|
+
this.#findVariableUsage(themeSource.semanticColors, usedVars)
|
|
354
617
|
}
|
|
355
618
|
|
|
356
619
|
// Also check dependencies for usage in these sections
|
|
357
|
-
if(theme.
|
|
358
|
-
for(const dependency of theme.
|
|
620
|
+
if(theme.hasDependencies()) {
|
|
621
|
+
for(const dependency of theme.getDependencies()) {
|
|
359
622
|
try {
|
|
360
|
-
const depData = theme.
|
|
623
|
+
const depData = theme.getCache()?.loadCachedDataSync?.(dependency.getSourceFile())
|
|
624
|
+
|
|
361
625
|
if(depData) {
|
|
362
626
|
if(depData.colors)
|
|
363
|
-
this
|
|
627
|
+
this.#findVariableUsage(depData.colors, usedVars)
|
|
364
628
|
|
|
365
629
|
if(depData.tokenColors)
|
|
366
|
-
this
|
|
630
|
+
this.#findVariableUsage(depData.tokenColors, usedVars)
|
|
367
631
|
|
|
368
632
|
if(depData.semanticColors)
|
|
369
|
-
this
|
|
633
|
+
this.#findVariableUsage(depData.semanticColors, usedVars)
|
|
370
634
|
}
|
|
371
635
|
} catch {
|
|
372
636
|
// Ignore cache errors
|
|
@@ -393,57 +657,67 @@ export default class LintCommand extends Command {
|
|
|
393
657
|
* Recursively collects variable names defined in the vars section.
|
|
394
658
|
* Adds found variable names to the definedVars map.
|
|
395
659
|
*
|
|
396
|
-
* @param {
|
|
660
|
+
* @param {object|null} vars - The vars data structure to search
|
|
397
661
|
* @param {Map} definedVars - Map to add found variable names and filenames to
|
|
398
662
|
* @param {string} prefix - Current prefix for nested vars
|
|
399
663
|
* @param {string} filename - The filename where this variable is defined
|
|
664
|
+
* @private
|
|
400
665
|
*/
|
|
401
|
-
collectVarsDefinitions(vars, definedVars, prefix = "", filename = "") {
|
|
666
|
+
#collectVarsDefinitions(vars, definedVars, prefix = "", filename = "") {
|
|
402
667
|
if(!vars || typeof vars !== "object")
|
|
403
668
|
return
|
|
404
669
|
|
|
405
670
|
for(const [key, value] of Object.entries(vars)) {
|
|
406
671
|
const varName = prefix ? `${prefix}.${key}` : key
|
|
672
|
+
|
|
407
673
|
definedVars.set(varName, filename)
|
|
408
674
|
|
|
409
675
|
// If the value is an object, recurse for nested definitions
|
|
410
676
|
if(value && typeof value === "object" && !Array.isArray(value)) {
|
|
411
|
-
this
|
|
677
|
+
this.#collectVarsDefinitions(value, definedVars, varName, filename)
|
|
412
678
|
}
|
|
413
679
|
}
|
|
414
680
|
}
|
|
415
681
|
|
|
416
682
|
/**
|
|
417
683
|
* Recursively finds variable usage in any data structure.
|
|
684
|
+
*
|
|
418
685
|
* Adds found variable names to the usedVars set.
|
|
419
686
|
*
|
|
420
|
-
* @param {
|
|
687
|
+
* @param {string|Array|object} data - The data structure to search
|
|
421
688
|
* @param {Set} usedVars - Set to add found variable names to
|
|
689
|
+
* @private
|
|
422
690
|
*/
|
|
423
|
-
findVariableUsage(data, usedVars) {
|
|
691
|
+
#findVariableUsage(data, usedVars) {
|
|
424
692
|
if(typeof data === "string") {
|
|
425
693
|
if(Evaluator.sub.test(data)) {
|
|
426
694
|
const {none, parens, braces} = Evaluator.sub.exec(data)?.groups ?? {}
|
|
427
695
|
const varName = none || parens || braces
|
|
696
|
+
|
|
428
697
|
if(varName) {
|
|
429
698
|
usedVars.add(varName)
|
|
430
699
|
}
|
|
431
700
|
}
|
|
432
701
|
} else if(Array.isArray(data)) {
|
|
433
|
-
data.forEach(item => this
|
|
702
|
+
data.forEach(item => this.#findVariableUsage(item, usedVars))
|
|
434
703
|
} else if(data && typeof data === "object") {
|
|
435
|
-
Object.values(data).forEach(
|
|
704
|
+
Object.values(data).forEach(
|
|
705
|
+
value => this.#findVariableUsage(value, usedVars)
|
|
706
|
+
)
|
|
436
707
|
}
|
|
437
708
|
}
|
|
438
709
|
|
|
439
710
|
/**
|
|
440
711
|
* Checks for precedence issues where broad scopes override specific ones.
|
|
441
|
-
*
|
|
712
|
+
*
|
|
713
|
+
* Returns issues for cases where a general scope appears after a more
|
|
714
|
+
* specific one.
|
|
442
715
|
*
|
|
443
716
|
* @param {Array} tokenColors - Array of tokenColors entries
|
|
444
717
|
* @returns {Array} Array of precedence issue warnings
|
|
718
|
+
* @private
|
|
445
719
|
*/
|
|
446
|
-
checkPrecedenceIssues(tokenColors) {
|
|
720
|
+
#checkPrecedenceIssues(tokenColors) {
|
|
447
721
|
const issues = []
|
|
448
722
|
const allScopes = []
|
|
449
723
|
|
|
@@ -453,6 +727,7 @@ export default class LintCommand extends Command {
|
|
|
453
727
|
return
|
|
454
728
|
|
|
455
729
|
const scopes = entry.scope.split(",").map(s => s.trim())
|
|
730
|
+
|
|
456
731
|
scopes.forEach(scope => {
|
|
457
732
|
allScopes.push({
|
|
458
733
|
scope,
|
|
@@ -472,7 +747,7 @@ export default class LintCommand extends Command {
|
|
|
472
747
|
|
|
473
748
|
// Check if the current (earlier) scope is broader than the later one
|
|
474
749
|
// This means the broad scope will mask the specific scope
|
|
475
|
-
if(this
|
|
750
|
+
if(this.#isBroaderScope(current.scope, later.scope)) {
|
|
476
751
|
issues.push({
|
|
477
752
|
type: "precedence-issue",
|
|
478
753
|
severity: current.index === later.index ? "low" : "high",
|
|
@@ -492,13 +767,16 @@ export default class LintCommand extends Command {
|
|
|
492
767
|
|
|
493
768
|
/**
|
|
494
769
|
* Determines if one scope is broader than another.
|
|
495
|
-
*
|
|
770
|
+
*
|
|
771
|
+
* A broader scope will match the same tokens as a more specific scope, plus
|
|
772
|
+
* others.
|
|
496
773
|
*
|
|
497
774
|
* @param {string} broadScope - The potentially broader scope
|
|
498
775
|
* @param {string} specificScope - The potentially more specific scope
|
|
499
776
|
* @returns {boolean} True if broadScope is broader than specificScope
|
|
777
|
+
* @private
|
|
500
778
|
*/
|
|
501
|
-
isBroaderScope(broadScope, specificScope) {
|
|
779
|
+
#isBroaderScope(broadScope, specificScope) {
|
|
502
780
|
// Simple heuristic: if the specific scope starts with the broad scope + "."
|
|
503
781
|
// then the broad scope is indeed broader
|
|
504
782
|
// e.g., "keyword" is broader than "keyword.control", "keyword.control.import"
|