@gesslar/sassy 0.21.0 → 0.21.3
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 +10 -5
- package/src/BuildCommand.js +1 -2
- package/src/Colour.js +1 -3
- package/src/Command.js +1 -3
- package/src/Compiler.js +1 -6
- package/src/Evaluator.js +53 -4
- package/src/LintCommand.js +216 -259
- package/src/ResolveCommand.js +17 -6
- package/src/Session.js +4 -5
- package/src/Theme.js +19 -10
- package/src/ThemePool.js +1 -1
- package/src/ThemeToken.js +1 -1
- package/src/cli.js +3 -6
- package/src/index.js +40 -0
- package/src/Cache.js +0 -74
- package/src/Data.js +0 -533
- package/src/DirectoryObject.js +0 -188
- package/src/File.js +0 -346
- package/src/FileObject.js +0 -226
- package/src/Sass.js +0 -166
- package/src/Term.js +0 -175
- package/src/Type.js +0 -207
- package/src/Util.js +0 -132
- package/src/Valid.js +0 -50
package/src/LintCommand.js
CHANGED
|
@@ -20,10 +20,8 @@ import c from "@gesslar/colours"
|
|
|
20
20
|
|
|
21
21
|
import Command from "./Command.js"
|
|
22
22
|
import Evaluator from "./Evaluator.js"
|
|
23
|
-
import File from "./File.js"
|
|
24
|
-
import FileObject from "./FileObject.js"
|
|
25
|
-
import Term from "./Term.js"
|
|
26
23
|
import Theme from "./Theme.js"
|
|
24
|
+
import {Term} from "@gesslar/toolkit"
|
|
27
25
|
import ThemePool from "./ThemePool.js"
|
|
28
26
|
|
|
29
27
|
// oops, need to have @gesslar/colours support this, too!
|
|
@@ -36,6 +34,36 @@ import ThemePool from "./ThemePool.js"
|
|
|
36
34
|
* behaviour.
|
|
37
35
|
*/
|
|
38
36
|
export default class LintCommand extends Command {
|
|
37
|
+
|
|
38
|
+
// Theme section constants
|
|
39
|
+
static SECTIONS = {
|
|
40
|
+
VARS: "vars",
|
|
41
|
+
COLORS: "colors",
|
|
42
|
+
TOKEN_COLORS: "tokenColors",
|
|
43
|
+
SEMANTIC_TOKEN_COLORS: "semanticTokenColors"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Issue severity levels
|
|
47
|
+
static SEVERITY = {
|
|
48
|
+
HIGH: "high",
|
|
49
|
+
MEDIUM: "medium",
|
|
50
|
+
LOW: "low"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Issue type constants
|
|
54
|
+
static ISSUE_TYPES = {
|
|
55
|
+
DUPLICATE_SCOPE: "duplicate-scope",
|
|
56
|
+
UNDEFINED_VARIABLE: "undefined-variable",
|
|
57
|
+
UNUSED_VARIABLE: "unused-variable",
|
|
58
|
+
PRECEDENCE_ISSUE: "precedence-issue"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Template strings for dynamic rule names
|
|
62
|
+
static TEMPLATES = {
|
|
63
|
+
ENTRY_NAME: index => `Entry ${index + 1}`,
|
|
64
|
+
OBJECT_NAME: index => `Object ${index + 1}`,
|
|
65
|
+
VARIABLE_PREFIX: "$"
|
|
66
|
+
}
|
|
39
67
|
/**
|
|
40
68
|
* Creates a new LintCommand instance.
|
|
41
69
|
*
|
|
@@ -86,34 +114,40 @@ export default class LintCommand extends Command {
|
|
|
86
114
|
*/
|
|
87
115
|
async lint(theme) {
|
|
88
116
|
const results = {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
117
|
+
[LC.SECTIONS.TOKEN_COLORS]: [],
|
|
118
|
+
[LC.SECTIONS.SEMANTIC_TOKEN_COLORS]: [],
|
|
119
|
+
[LC.SECTIONS.COLORS]: [],
|
|
92
120
|
variables: []
|
|
93
121
|
}
|
|
94
122
|
|
|
95
|
-
const
|
|
123
|
+
const output = theme.getOutput()
|
|
96
124
|
|
|
97
125
|
// Always perform structural linting (works with or without pool)
|
|
98
|
-
if(
|
|
99
|
-
results.
|
|
100
|
-
...this.#lintTokenColorsStructure(
|
|
101
|
-
|
|
126
|
+
if(output?.tokenColors)
|
|
127
|
+
results[LC.SECTIONS.TOKEN_COLORS]
|
|
128
|
+
.push(...this.#lintTokenColorsStructure(output.tokenColors))
|
|
129
|
+
|
|
130
|
+
const pool = theme.getPool()
|
|
102
131
|
|
|
103
132
|
// Only perform variable-dependent linting if pool exists
|
|
104
133
|
if(pool) {
|
|
105
134
|
// Get source data for variable analysis
|
|
106
|
-
const colors =
|
|
107
|
-
|
|
108
|
-
const
|
|
135
|
+
const colors = this.#getSection(
|
|
136
|
+
theme, LC.SECTIONS.COLORS)
|
|
137
|
+
const tokenColorTuples = this.#getSection(
|
|
138
|
+
theme, LC.SECTIONS.TOKEN_COLORS)
|
|
139
|
+
const semanticTokenColors = this.#getSection(
|
|
140
|
+
theme, LC.SECTIONS.SEMANTIC_TOKEN_COLORS)
|
|
109
141
|
|
|
110
142
|
// Variable-dependent linting
|
|
111
|
-
results.
|
|
112
|
-
|
|
113
|
-
results.
|
|
114
|
-
...this.#
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
results[LC.SECTIONS.COLORS]
|
|
144
|
+
.push(...this.#lintColors(colors, pool))
|
|
145
|
+
results[LC.SECTIONS.TOKEN_COLORS]
|
|
146
|
+
.push(...this.#lintTokenColors(tokenColorTuples, pool))
|
|
147
|
+
results[LC.SECTIONS.SEMANTIC_TOKEN_COLORS]
|
|
148
|
+
.push(...this.#lintSemanticTokenColors(semanticTokenColors, pool))
|
|
149
|
+
results.variables
|
|
150
|
+
.push(...await this.#lintVariables(theme, pool))
|
|
117
151
|
}
|
|
118
152
|
|
|
119
153
|
return results
|
|
@@ -140,44 +174,71 @@ export default class LintCommand extends Command {
|
|
|
140
174
|
* Performs variable-dependent linting of tokenColors data.
|
|
141
175
|
* Checks for undefined variable references.
|
|
142
176
|
*
|
|
143
|
-
* @param {Array}
|
|
177
|
+
* @param {Array<[object, Array]>} tokenColorTuples - Array of [file, tokenColors] tuples
|
|
144
178
|
* @param {ThemePool} pool - The theme's variable pool
|
|
145
179
|
* @returns {Array} Array of variable-related issues
|
|
180
|
+
* @private
|
|
146
181
|
*/
|
|
147
|
-
lintTokenColors(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
182
|
+
#lintTokenColors(tokenColorTuples, pool) {
|
|
183
|
+
if(tokenColorTuples.length === 0)
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
const issues = []
|
|
187
|
+
|
|
188
|
+
for(const [_, tokenColors] of tokenColorTuples) {
|
|
189
|
+
if(Array.isArray(tokenColors))
|
|
190
|
+
issues.push(
|
|
191
|
+
...this.#checkUndefinedVariables(
|
|
192
|
+
tokenColors, pool, LC.SECTIONS.TOKEN_COLORS))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return issues
|
|
151
196
|
}
|
|
152
197
|
|
|
153
198
|
/**
|
|
154
199
|
* Performs variable-dependent linting of semanticTokenColors data.
|
|
155
200
|
* Checks for undefined variable references.
|
|
156
201
|
*
|
|
157
|
-
* @param {Array}
|
|
202
|
+
* @param {Array<[object, object]>} semanticTokenColorTuples - Array of [file, semanticTokenColors] tuples
|
|
158
203
|
* @param {ThemePool} pool - The theme's variable pool
|
|
159
204
|
* @returns {Array} Array of variable-related issues
|
|
160
205
|
* @private
|
|
161
206
|
*/
|
|
162
|
-
#lintSemanticTokenColors(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
207
|
+
#lintSemanticTokenColors(semanticTokenColorTuples, pool) {
|
|
208
|
+
if(semanticTokenColorTuples.length === 0)
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
const issues = []
|
|
212
|
+
|
|
213
|
+
for(const [_, semanticTokenColors] of semanticTokenColorTuples)
|
|
214
|
+
issues.push(...this.#checkUndefinedVariables(
|
|
215
|
+
[semanticTokenColors], pool, LC.SECTIONS.SEMANTIC_TOKEN_COLORS)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return issues
|
|
166
219
|
}
|
|
167
220
|
|
|
168
221
|
/**
|
|
169
222
|
* Performs variable-dependent linting of colors data.
|
|
170
223
|
* Checks for undefined variable references.
|
|
171
224
|
*
|
|
172
|
-
* @param {Array}
|
|
225
|
+
* @param {Array<[object, object]>} colorTuples - Array of [file, colors] tuples
|
|
173
226
|
* @param {ThemePool} pool - The theme's variable pool
|
|
174
227
|
* @returns {Array} Array of variable-related issues
|
|
175
228
|
* @private
|
|
176
229
|
*/
|
|
177
|
-
#lintColors(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
230
|
+
#lintColors(colorTuples, pool) {
|
|
231
|
+
if(colorTuples.length === 0)
|
|
232
|
+
return []
|
|
233
|
+
|
|
234
|
+
const issues = []
|
|
235
|
+
|
|
236
|
+
for(const [_, colors] of colorTuples)
|
|
237
|
+
issues.push(...this.#checkUndefinedVariables(
|
|
238
|
+
[colors], pool, LC.SECTIONS.COLORS)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return issues
|
|
181
242
|
}
|
|
182
243
|
|
|
183
244
|
/**
|
|
@@ -186,12 +247,12 @@ export default class LintCommand extends Command {
|
|
|
186
247
|
*
|
|
187
248
|
* @param {Theme} theme - The theme object
|
|
188
249
|
* @param {ThemePool} pool - The theme's variable pool
|
|
189
|
-
* @returns {Array} Array of unused variable issues
|
|
250
|
+
* @returns {Promise<Array>} Array of unused variable issues
|
|
190
251
|
* @private
|
|
191
252
|
*/
|
|
192
|
-
#lintVariables(theme, pool) {
|
|
253
|
+
async #lintVariables(theme, pool) {
|
|
193
254
|
return pool
|
|
194
|
-
? this.#checkUnusedVariables(theme, pool)
|
|
255
|
+
? await this.#checkUnusedVariables(theme, pool)
|
|
195
256
|
: []
|
|
196
257
|
}
|
|
197
258
|
|
|
@@ -208,123 +269,33 @@ export default class LintCommand extends Command {
|
|
|
208
269
|
|
|
209
270
|
// Flatten all results into a single array for backward compatibility
|
|
210
271
|
return [
|
|
211
|
-
...results.
|
|
212
|
-
...results.
|
|
213
|
-
...results.
|
|
272
|
+
...results[LC.SECTIONS.TOKEN_COLORS],
|
|
273
|
+
...results[LC.SECTIONS.SEMANTIC_TOKEN_COLORS],
|
|
274
|
+
...results[LC.SECTIONS.COLORS],
|
|
214
275
|
...results.variables
|
|
215
276
|
]
|
|
216
277
|
}
|
|
217
278
|
|
|
218
279
|
/**
|
|
219
|
-
* Extracts
|
|
220
|
-
* dependencies.
|
|
280
|
+
* Extracts a specific section from all theme dependencies (including main theme).
|
|
221
281
|
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
282
|
+
* Returns an array of [FileObject, sectionData] tuples for linting methods that need
|
|
283
|
+
* to track which file each piece of data originated from for proper error reporting.
|
|
224
284
|
*
|
|
225
|
-
* @param {Theme} theme - The
|
|
226
|
-
* @
|
|
285
|
+
* @param {Theme} theme - The theme object with dependencies
|
|
286
|
+
* @param {string} section - The section name to extract (vars, colors, tokenColors, semanticTokenColors)
|
|
287
|
+
* @returns {Array<[object, object|Array]>} Array of [file, sectionData] tuples
|
|
227
288
|
* @private
|
|
228
289
|
*/
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// Get tokenColors from main theme source
|
|
233
|
-
if(theme.getSource()?.theme?.tokenColors)
|
|
234
|
-
sourceTokenColors.push(...theme.getSource().theme.tokenColors)
|
|
235
|
-
|
|
236
|
-
// Get tokenColors from imported files
|
|
237
|
-
if(theme.hasDependencies()) {
|
|
238
|
-
for(const dependency of theme.getDependencies()) {
|
|
239
|
-
// Skip main file, already processed
|
|
240
|
-
if(dependency.getSourceFile().path !== theme.getSourceFile().path) {
|
|
241
|
-
try {
|
|
242
|
-
const depData = await theme.getCache().loadCachedData(dependency.getSourceFile())
|
|
243
|
-
|
|
244
|
-
if(depData?.theme?.tokenColors)
|
|
245
|
-
sourceTokenColors.push(...depData.theme.tokenColors)
|
|
246
|
-
} catch {
|
|
247
|
-
// nothing to see here.
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
290
|
+
#getSection(theme, section) {
|
|
291
|
+
return Array.from(theme.getDependencies()).map(dep => {
|
|
292
|
+
const source = dep.getSource()
|
|
252
293
|
|
|
253
|
-
|
|
254
|
-
|
|
294
|
+
if(source?.has(section))
|
|
295
|
+
return [dep.getSourceFile(),source.get(section)]
|
|
255
296
|
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
297
|
+
return false
|
|
298
|
+
}).filter(Boolean)
|
|
328
299
|
}
|
|
329
300
|
|
|
330
301
|
/**
|
|
@@ -340,10 +311,9 @@ export default class LintCommand extends Command {
|
|
|
340
311
|
return
|
|
341
312
|
}
|
|
342
313
|
|
|
343
|
-
const errors = issues.filter(i => i.severity ===
|
|
344
|
-
const warnings = issues.filter(i => i.severity ===
|
|
345
|
-
const infos = issues.filter(i => i.severity ===
|
|
346
|
-
|
|
314
|
+
const errors = issues.filter(i => i.severity === LC.SEVERITY.HIGH)
|
|
315
|
+
const warnings = issues.filter(i => i.severity === LC.SEVERITY.MEDIUM)
|
|
316
|
+
const infos = issues.filter(i => i.severity === LC.SEVERITY.LOW)
|
|
347
317
|
const allIssues = errors.concat(warnings, infos)
|
|
348
318
|
|
|
349
319
|
allIssues.forEach(issue => this.#reportSingleIssue(issue))
|
|
@@ -372,9 +342,9 @@ export default class LintCommand extends Command {
|
|
|
372
342
|
*/
|
|
373
343
|
#getIndicator(severity) {
|
|
374
344
|
switch(severity) {
|
|
375
|
-
case
|
|
376
|
-
case
|
|
377
|
-
case
|
|
345
|
+
case LC.SEVERITY.HIGH: return c`{error}●{/}`
|
|
346
|
+
case LC.SEVERITY.MEDIUM: return c`{warn}●{/}`
|
|
347
|
+
case LC.SEVERITY.LOW:
|
|
378
348
|
default: return c`{info}●{/}`
|
|
379
349
|
}
|
|
380
350
|
}
|
|
@@ -389,26 +359,26 @@ export default class LintCommand extends Command {
|
|
|
389
359
|
const indicator = this.#getIndicator(issue.severity)
|
|
390
360
|
|
|
391
361
|
switch(issue.type) {
|
|
392
|
-
case
|
|
362
|
+
case LC.ISSUE_TYPES.DUPLICATE_SCOPE: {
|
|
393
363
|
const rules = issue.occurrences.map(occ => `{loc}'${occ.name}{/}'`).join(", ")
|
|
394
364
|
|
|
395
365
|
Term.info(c`${indicator} Scope '{context}${issue.scope}{/}' is duplicated in ${rules}`)
|
|
396
366
|
break
|
|
397
367
|
}
|
|
398
368
|
|
|
399
|
-
case
|
|
400
|
-
const sectionInfo = issue.section && issue.section !==
|
|
369
|
+
case LC.ISSUE_TYPES.UNDEFINED_VARIABLE: {
|
|
370
|
+
const sectionInfo = issue.section && issue.section !== LC.SECTIONS.TOKEN_COLORS ? ` in ${issue.section}` : ""
|
|
401
371
|
|
|
402
372
|
Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is used but not defined in '${issue.rule}' (${issue.property} property)${sectionInfo}`)
|
|
403
373
|
break
|
|
404
374
|
}
|
|
405
375
|
|
|
406
|
-
case
|
|
407
|
-
Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is defined in '{loc}${issue.
|
|
376
|
+
case LC.ISSUE_TYPES.UNUSED_VARIABLE: {
|
|
377
|
+
Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is defined in '{loc}${issue.occurrence}{/}', but is never used`)
|
|
408
378
|
break
|
|
409
379
|
}
|
|
410
380
|
|
|
411
|
-
case
|
|
381
|
+
case LC.ISSUE_TYPES.PRECEDENCE_ISSUE: {
|
|
412
382
|
if(issue.broadIndex === issue.specificIndex) {
|
|
413
383
|
Term.info(c`${indicator} Scope '{context}${issue.broadScope}{/}' makes more specific '{context}${issue.specificScope}{/}' redundant in '{loc}${issue.broadRule}{/}'`)
|
|
414
384
|
} else {
|
|
@@ -439,13 +409,12 @@ export default class LintCommand extends Command {
|
|
|
439
409
|
const scopes = entry.scope.split(",").map(s => s.trim())
|
|
440
410
|
|
|
441
411
|
scopes.forEach(scope => {
|
|
442
|
-
if(!scopeOccurrences.has(scope))
|
|
412
|
+
if(!scopeOccurrences.has(scope))
|
|
443
413
|
scopeOccurrences.set(scope, [])
|
|
444
|
-
}
|
|
445
414
|
|
|
446
415
|
scopeOccurrences.get(scope).push({
|
|
447
416
|
index: index + 1,
|
|
448
|
-
name: entry.name ||
|
|
417
|
+
name: entry.name || LC.TEMPLATES.ENTRY_NAME(index),
|
|
449
418
|
entry
|
|
450
419
|
})
|
|
451
420
|
})
|
|
@@ -455,8 +424,8 @@ export default class LintCommand extends Command {
|
|
|
455
424
|
for(const [scope, occurrences] of scopeOccurrences) {
|
|
456
425
|
if(occurrences.length > 1) {
|
|
457
426
|
issues.push({
|
|
458
|
-
type:
|
|
459
|
-
severity:
|
|
427
|
+
type: LC.ISSUE_TYPES.DUPLICATE_SCOPE,
|
|
428
|
+
severity: LC.SEVERITY.MEDIUM,
|
|
460
429
|
scope,
|
|
461
430
|
occurrences
|
|
462
431
|
})
|
|
@@ -476,28 +445,27 @@ export default class LintCommand extends Command {
|
|
|
476
445
|
* @returns {Array} Array of undefined variable issues
|
|
477
446
|
* @private
|
|
478
447
|
*/
|
|
479
|
-
#checkUndefinedVariables(themeData, pool, section
|
|
448
|
+
#checkUndefinedVariables(themeData, pool, section=LC.SECTIONS.TOKEN_COLORS) {
|
|
480
449
|
const issues = []
|
|
481
450
|
const definedVars = pool ? new Set(pool.getTokens().keys()) : new Set()
|
|
482
451
|
|
|
483
|
-
if(section ===
|
|
452
|
+
if(section === LC.SECTIONS.TOKEN_COLORS && Array.isArray(themeData)) {
|
|
484
453
|
themeData.forEach((entry, index) => {
|
|
485
454
|
const settings = entry.settings || {}
|
|
486
455
|
|
|
487
456
|
for(const [key, value] of Object.entries(settings)) {
|
|
488
457
|
if(typeof value === "string") {
|
|
489
|
-
const
|
|
490
|
-
const varName = none || parens || braces
|
|
458
|
+
const varName = Evaluator.extractVariableName(value)
|
|
491
459
|
|
|
492
460
|
if(!varName)
|
|
493
461
|
return
|
|
494
462
|
|
|
495
463
|
if(!definedVars.has(varName)) {
|
|
496
464
|
issues.push({
|
|
497
|
-
type:
|
|
498
|
-
severity:
|
|
465
|
+
type: LC.ISSUE_TYPES.UNDEFINED_VARIABLE,
|
|
466
|
+
severity: LC.SEVERITY.HIGH,
|
|
499
467
|
variable: value,
|
|
500
|
-
rule: entry.name ||
|
|
468
|
+
rule: entry.name || LC.TEMPLATES.ENTRY_NAME(index),
|
|
501
469
|
property: key,
|
|
502
470
|
section
|
|
503
471
|
})
|
|
@@ -505,12 +473,18 @@ export default class LintCommand extends Command {
|
|
|
505
473
|
}
|
|
506
474
|
}
|
|
507
475
|
})
|
|
508
|
-
} else if((section ===
|
|
476
|
+
} else if((section === LC.SECTIONS.SEMANTIC_TOKEN_COLORS ||
|
|
477
|
+
section === LC.SECTIONS.COLORS)
|
|
478
|
+
&& Array.isArray(themeData)) {
|
|
509
479
|
// Handle semanticTokenColors and colors as objects
|
|
510
480
|
themeData.forEach((dataObject, objIndex) => {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
481
|
+
this.#checkObjectForUndefinedVariables(
|
|
482
|
+
dataObject,
|
|
483
|
+
definedVars,
|
|
484
|
+
issues,
|
|
485
|
+
section,
|
|
486
|
+
LC.TEMPLATES.OBJECT_NAME(objIndex)
|
|
487
|
+
)
|
|
514
488
|
})
|
|
515
489
|
}
|
|
516
490
|
|
|
@@ -529,24 +503,23 @@ export default class LintCommand extends Command {
|
|
|
529
503
|
* @private
|
|
530
504
|
*/
|
|
531
505
|
#checkObjectForUndefinedVariables(obj, definedVars, issues, section, ruleName, path = "") {
|
|
532
|
-
for(const [key, value] of Object.entries(obj)) {
|
|
506
|
+
for(const [key, value] of Object.entries(obj ?? {})) {
|
|
533
507
|
const currentPath = path ? `${path}.${key}` : key
|
|
534
508
|
|
|
535
509
|
if(typeof value === "string") {
|
|
536
|
-
const
|
|
537
|
-
const varName = none || parens || braces
|
|
510
|
+
const varName = Evaluator.extractVariableName(value)
|
|
538
511
|
|
|
539
512
|
if(varName && !definedVars.has(varName)) {
|
|
540
513
|
issues.push({
|
|
541
|
-
type:
|
|
542
|
-
severity:
|
|
514
|
+
type: LC.ISSUE_TYPES.UNDEFINED_VARIABLE,
|
|
515
|
+
severity: LC.SEVERITY.HIGH,
|
|
543
516
|
variable: value,
|
|
544
517
|
rule: ruleName,
|
|
545
518
|
property: currentPath,
|
|
546
519
|
section
|
|
547
520
|
})
|
|
548
521
|
}
|
|
549
|
-
} else if(
|
|
522
|
+
} else if(typeof value === "object" && !Array.isArray(value)) {
|
|
550
523
|
this.#checkObjectForUndefinedVariables(
|
|
551
524
|
value, definedVars, issues, section, ruleName, currentPath
|
|
552
525
|
)
|
|
@@ -562,10 +535,10 @@ export default class LintCommand extends Command {
|
|
|
562
535
|
*
|
|
563
536
|
* @param {Theme} theme - The compiled theme object
|
|
564
537
|
* @param {ThemePool} pool - The theme's variable pool
|
|
565
|
-
* @returns {Array} Array of unused variable issues
|
|
538
|
+
* @returns {Promise<Array>} Array of unused variable issues
|
|
566
539
|
* @private
|
|
567
540
|
*/
|
|
568
|
-
#checkUnusedVariables(theme, pool) {
|
|
541
|
+
async #checkUnusedVariables(theme, pool) {
|
|
569
542
|
const issues = []
|
|
570
543
|
|
|
571
544
|
if(!pool || !theme.getSource())
|
|
@@ -574,67 +547,36 @@ export default class LintCommand extends Command {
|
|
|
574
547
|
// Get variables defined in the vars section only
|
|
575
548
|
const definedVars = new Map()
|
|
576
549
|
const cwd = this.getCwd()
|
|
577
|
-
const mainFile = new FileObject(theme.getSourceFile().path)
|
|
578
|
-
const relativeMainPath = File.relativeOrAbsolutePath(cwd, mainFile)
|
|
579
|
-
|
|
580
|
-
this.#collectVarsDefinitions(theme.getSource().vars, definedVars, "", relativeMainPath)
|
|
581
|
-
|
|
582
|
-
// Also check dependencies for vars definitions
|
|
583
|
-
if(theme.hasDependencies()) {
|
|
584
|
-
for(const dependency of theme.getDependencies()) {
|
|
585
|
-
try {
|
|
586
|
-
const depData = theme.getCache()?.loadCachedDataSync?.(dependency.getSourceFile())
|
|
587
|
-
|
|
588
|
-
if(depData?.vars) {
|
|
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)
|
|
595
|
-
}
|
|
596
|
-
} catch {
|
|
597
|
-
// Ignore cache errors
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
550
|
|
|
602
551
|
const usedVars = new Set()
|
|
603
552
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if(theme.hasDependencies()) {
|
|
621
|
-
for(const dependency of theme.getDependencies()) {
|
|
622
|
-
try {
|
|
623
|
-
const depData = theme.getCache()?.loadCachedDataSync?.(dependency.getSourceFile())
|
|
624
|
-
|
|
625
|
-
if(depData) {
|
|
626
|
-
if(depData.colors)
|
|
627
|
-
this.#findVariableUsage(depData.colors, usedVars)
|
|
628
|
-
|
|
629
|
-
if(depData.tokenColors)
|
|
630
|
-
this.#findVariableUsage(depData.tokenColors, usedVars)
|
|
631
|
-
|
|
632
|
-
if(depData.semanticColors)
|
|
633
|
-
this.#findVariableUsage(depData.semanticColors, usedVars)
|
|
634
|
-
}
|
|
635
|
-
} catch {
|
|
636
|
-
// Ignore cache errors
|
|
553
|
+
for(const dependency of theme.getDependencies()) {
|
|
554
|
+
try {
|
|
555
|
+
const depData = dependency.getSource()
|
|
556
|
+
const depFile = dependency.getSourceFile()
|
|
557
|
+
|
|
558
|
+
// Collect vars definitions
|
|
559
|
+
if(depData?.vars) {
|
|
560
|
+
const relativeDependencyPath =
|
|
561
|
+
File.relativeOrAbsolutePath(cwd, depFile)
|
|
562
|
+
|
|
563
|
+
this.#collectVarsDefinitions(
|
|
564
|
+
depData.vars,
|
|
565
|
+
definedVars,
|
|
566
|
+
"",
|
|
567
|
+
relativeDependencyPath
|
|
568
|
+
)
|
|
637
569
|
}
|
|
570
|
+
|
|
571
|
+
// Find variable usage in colors, tokenColors, and semanticTokenColors sections
|
|
572
|
+
this.#findVariableUsage(depData
|
|
573
|
+
?.get(LC.SECTIONS.COLORS), usedVars)
|
|
574
|
+
this.#findVariableUsage(depData
|
|
575
|
+
?.get(LC.SECTIONS.TOKEN_COLORS), usedVars)
|
|
576
|
+
this.#findVariableUsage(depData
|
|
577
|
+
?.get(LC.SECTIONS.SEMANTIC_TOKEN_COLORS), usedVars)
|
|
578
|
+
} catch {
|
|
579
|
+
// Ignore cache errors
|
|
638
580
|
}
|
|
639
581
|
}
|
|
640
582
|
|
|
@@ -642,10 +584,10 @@ export default class LintCommand extends Command {
|
|
|
642
584
|
for(const [varName, filename] of definedVars) {
|
|
643
585
|
if(!usedVars.has(varName)) {
|
|
644
586
|
issues.push({
|
|
645
|
-
type:
|
|
646
|
-
severity:
|
|
647
|
-
variable:
|
|
648
|
-
|
|
587
|
+
type: LC.ISSUE_TYPES.UNUSED_VARIABLE,
|
|
588
|
+
severity: LC.SEVERITY.LOW,
|
|
589
|
+
variable: `${LC.TEMPLATES.VARIABLE_PREFIX}${varName}`,
|
|
590
|
+
occurrence: filename,
|
|
649
591
|
})
|
|
650
592
|
}
|
|
651
593
|
}
|
|
@@ -664,18 +606,14 @@ export default class LintCommand extends Command {
|
|
|
664
606
|
* @private
|
|
665
607
|
*/
|
|
666
608
|
#collectVarsDefinitions(vars, definedVars, prefix = "", filename = "") {
|
|
667
|
-
|
|
668
|
-
return
|
|
669
|
-
|
|
670
|
-
for(const [key, value] of Object.entries(vars)) {
|
|
609
|
+
for(const [key, value] of Object.entries(vars ?? {})) {
|
|
671
610
|
const varName = prefix ? `${prefix}.${key}` : key
|
|
672
611
|
|
|
673
612
|
definedVars.set(varName, filename)
|
|
674
613
|
|
|
675
614
|
// If the value is an object, recurse for nested definitions
|
|
676
|
-
if(
|
|
615
|
+
if(typeof value === "object" && !Array.isArray(value))
|
|
677
616
|
this.#collectVarsDefinitions(value, definedVars, varName, filename)
|
|
678
|
-
}
|
|
679
617
|
}
|
|
680
618
|
}
|
|
681
619
|
|
|
@@ -689,10 +627,12 @@ export default class LintCommand extends Command {
|
|
|
689
627
|
* @private
|
|
690
628
|
*/
|
|
691
629
|
#findVariableUsage(data, usedVars) {
|
|
630
|
+
if(!data)
|
|
631
|
+
return
|
|
632
|
+
|
|
692
633
|
if(typeof data === "string") {
|
|
693
634
|
if(Evaluator.sub.test(data)) {
|
|
694
|
-
const
|
|
695
|
-
const varName = none || parens || braces
|
|
635
|
+
const varName = Evaluator.extractVariableName(data)
|
|
696
636
|
|
|
697
637
|
if(varName) {
|
|
698
638
|
usedVars.add(varName)
|
|
@@ -700,7 +640,7 @@ export default class LintCommand extends Command {
|
|
|
700
640
|
}
|
|
701
641
|
} else if(Array.isArray(data)) {
|
|
702
642
|
data.forEach(item => this.#findVariableUsage(item, usedVars))
|
|
703
|
-
} else if(
|
|
643
|
+
} else if(typeof data === "object") {
|
|
704
644
|
Object.values(data).forEach(
|
|
705
645
|
value => this.#findVariableUsage(value, usedVars)
|
|
706
646
|
)
|
|
@@ -732,12 +672,14 @@ export default class LintCommand extends Command {
|
|
|
732
672
|
allScopes.push({
|
|
733
673
|
scope,
|
|
734
674
|
index: index + 1,
|
|
735
|
-
name: entry.name ||
|
|
675
|
+
name: entry.name || LC.TEMPLATES.ENTRY_NAME(index),
|
|
736
676
|
entry
|
|
737
677
|
})
|
|
738
678
|
})
|
|
739
679
|
})
|
|
740
680
|
|
|
681
|
+
const {LOW,HIGH} = LC.SEVERITY
|
|
682
|
+
|
|
741
683
|
// Check each scope against all later scopes
|
|
742
684
|
for(let i = 0; i < allScopes.length; i++) {
|
|
743
685
|
const current = allScopes[i]
|
|
@@ -749,8 +691,8 @@ export default class LintCommand extends Command {
|
|
|
749
691
|
// This means the broad scope will mask the specific scope
|
|
750
692
|
if(this.#isBroaderScope(current.scope, later.scope)) {
|
|
751
693
|
issues.push({
|
|
752
|
-
type:
|
|
753
|
-
severity: current.index === later.index ?
|
|
694
|
+
type: LC.ISSUE_TYPES.PRECEDENCE_ISSUE,
|
|
695
|
+
severity: current.index === later.index ? LOW : HIGH,
|
|
754
696
|
specificScope: later.scope,
|
|
755
697
|
broadScope: current.scope,
|
|
756
698
|
specificRule: later.name,
|
|
@@ -769,7 +711,7 @@ export default class LintCommand extends Command {
|
|
|
769
711
|
* Determines if one scope is broader than another.
|
|
770
712
|
*
|
|
771
713
|
* A broader scope will match the same tokens as a more specific scope, plus
|
|
772
|
-
* others.
|
|
714
|
+
* others. Uses proper TextMate scope hierarchy rules.
|
|
773
715
|
*
|
|
774
716
|
* @param {string} broadScope - The potentially broader scope
|
|
775
717
|
* @param {string} specificScope - The potentially more specific scope
|
|
@@ -777,9 +719,24 @@ export default class LintCommand extends Command {
|
|
|
777
719
|
* @private
|
|
778
720
|
*/
|
|
779
721
|
#isBroaderScope(broadScope, specificScope) {
|
|
780
|
-
//
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
722
|
+
// Scopes must be different to have a precedence relationship
|
|
723
|
+
if(broadScope === specificScope)
|
|
724
|
+
return false
|
|
725
|
+
|
|
726
|
+
// Split both scopes into segments for proper comparison
|
|
727
|
+
const broadSegments = broadScope.split(".")
|
|
728
|
+
const specificSegments = specificScope.split(".")
|
|
729
|
+
|
|
730
|
+
// A broader scope must have fewer or equal segments
|
|
731
|
+
if(broadSegments.length > specificSegments.length)
|
|
732
|
+
return false
|
|
733
|
+
|
|
734
|
+
// All segments of the broad scope must match the specific scope's prefix
|
|
735
|
+
return broadSegments.every((segment, index) =>
|
|
736
|
+
segment === specificSegments[index]
|
|
737
|
+
)
|
|
784
738
|
}
|
|
785
739
|
}
|
|
740
|
+
|
|
741
|
+
// Aliases
|
|
742
|
+
const LC = LintCommand
|