@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.
@@ -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
- tokenColors: [],
90
- semanticTokenColors: [],
91
- colors: [],
117
+ [LC.SECTIONS.TOKEN_COLORS]: [],
118
+ [LC.SECTIONS.SEMANTIC_TOKEN_COLORS]: [],
119
+ [LC.SECTIONS.COLORS]: [],
92
120
  variables: []
93
121
  }
94
122
 
95
- const pool = theme.getPool()
123
+ const output = theme.getOutput()
96
124
 
97
125
  // 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
- )
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 = await this.getColors(theme)
107
- const tokenColors = await this.#getTokenColors(theme)
108
- const semanticTokenColors = await this.getSemanticTokenColors(theme)
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.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))
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} sourceTokenColors - Array of source tokenColor entries
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(sourceTokenColors, pool) {
148
- return pool
149
- ? this.#checkUndefinedVariables(sourceTokenColors, pool, "tokenColors")
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} semanticTokenColors - Array of source semanticTokenColors entries
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(semanticTokenColors, pool) {
163
- return pool && semanticTokenColors.length > 0
164
- ? this.#checkUndefinedVariables(semanticTokenColors, pool, "semanticTokenColors")
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} sourceColors - Array of source colors entries
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(sourceColors, pool) {
178
- return pool && sourceColors.length > 0
179
- ? this.#checkUndefinedVariables(sourceColors, pool, "colors")
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.tokenColors,
212
- ...results.semanticTokenColors,
213
- ...results.colors,
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 the original source tokenColors data from theme.source and
220
- * dependencies.
280
+ * Extracts a specific section from all theme dependencies (including main theme).
221
281
  *
222
- * Used for variable analysis since we need the uncompiled data with variable
223
- * references.
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 compiled theme object (contains both output and source)
226
- * @returns {Promise<Array>} Array of source tokenColors entries with variables intact
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
- async #getTokenColors(theme) {
230
- const sourceTokenColors = []
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
- return sourceTokenColors
254
- }
294
+ if(source?.has(section))
295
+ return [dep.getSourceFile(),source.get(section)]
255
296
 
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
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 === "high")
344
- const warnings = issues.filter(i => i.severity === "medium")
345
- const infos = issues.filter(i => i.severity === "low")
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 "high": return c`{error}●{/}`
376
- case "medium": return c`{warn}●{/}`
377
- case "low":
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 "duplicate-scope": {
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 "undefined-variable": {
400
- const sectionInfo = issue.section && issue.section !== "tokenColors" ? ` in ${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 "unused-variable": {
407
- Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is defined in '{loc}${issue.occurence}{/}', but is never used`)
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 "precedence-issue": {
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 || `Entry ${index + 1}`,
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: "duplicate-scope",
459
- severity: "medium",
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 = "tokenColors") {
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 === "tokenColors" && Array.isArray(themeData)) {
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 {none,parens,braces} = Evaluator.sub.exec(value)?.groups ?? {}
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: "undefined-variable",
498
- severity: "high",
465
+ type: LC.ISSUE_TYPES.UNDEFINED_VARIABLE,
466
+ severity: LC.SEVERITY.HIGH,
499
467
  variable: value,
500
- rule: entry.name || `Entry ${index + 1}`,
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 === "semanticTokenColors" || section === "colors") && Array.isArray(themeData)) {
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
- if(dataObject && typeof dataObject === "object") {
512
- this.#checkObjectForUndefinedVariables(dataObject, definedVars, issues, section, `Object ${objIndex + 1}`)
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 {none, parens, braces} = Evaluator.sub.exec(value)?.groups ?? {}
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: "undefined-variable",
542
- severity: "high",
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(value && typeof value === "object" && !Array.isArray(value)) {
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
- // Find variable usage in colors, tokenColors, and semanticColors sections
605
- const themeSource = theme.getSource()
606
-
607
- if(themeSource?.colors) {
608
- this.#findVariableUsage(themeSource.colors, usedVars)
609
- }
610
-
611
- if(themeSource?.tokenColors) {
612
- this.#findVariableUsage(themeSource.tokenColors, usedVars)
613
- }
614
-
615
- if(themeSource?.semanticColors) {
616
- this.#findVariableUsage(themeSource.semanticColors, usedVars)
617
- }
618
-
619
- // Also check dependencies for usage in these sections
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: "unused-variable",
646
- severity: "low",
647
- variable: `$${varName}`,
648
- occurence: filename,
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
- if(!vars || typeof vars !== "object")
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(value && typeof value === "object" && !Array.isArray(value)) {
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 {none, parens, braces} = Evaluator.sub.exec(data)?.groups ?? {}
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(data && typeof data === "object") {
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 || `Entry ${index + 1}`,
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: "precedence-issue",
753
- severity: current.index === later.index ? "low" : "high",
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
- // Simple heuristic: if the specific scope starts with the broad scope + "."
781
- // then the broad scope is indeed broader
782
- // e.g., "keyword" is broader than "keyword.control", "keyword.control.import"
783
- return specificScope.startsWith(broadScope + ".")
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