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