@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.
@@ -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.cliCommand = "lint <file>"
49
- this.cliOptions = {
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 {cwd} = this
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.cache = this.cache
71
-
69
+ theme.setCache(this.getCache())
72
70
  await theme.load()
73
71
  await theme.build()
74
72
 
75
- const issues = await this.lintTheme(theme)
73
+ const issues = await this.#lintTheme(theme)
76
74
 
77
- this.reportIssues(issues)
75
+ this.#reportIssues(issues)
78
76
  }
79
77
 
80
78
  /**
81
- * Performs comprehensive linting of a theme.
82
- * Returns an array of issues found during validation.
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<Array>} Array of lint issues
85
+ * @returns {Promise<object>} Object containing categorized lint results
86
86
  */
87
- async lintTheme(theme) {
88
- const issues = []
89
- const tokenColors = theme.output?.tokenColors || []
90
- const pool = theme.pool
87
+ async lint(theme) {
88
+ const results = {
89
+ tokenColors: [],
90
+ semanticTokenColors: [],
91
+ colors: [],
92
+ variables: []
93
+ }
91
94
 
92
- // Get source tokenColors data (before compilation) for variable usage analysis
93
- const sourceTokenColors = await this.getSourceTokenColors(theme)
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
- // 1. Check for duplicate scopes
96
- issues.push(...this.checkDuplicateScopes(tokenColors))
119
+ return results
120
+ }
97
121
 
98
- // 2. Check for undefined variables
99
- issues.push(...this.checkUndefinedVariables(sourceTokenColors, pool))
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
- // 3. Check for unused variables
102
- issues.push(...this.checkUnusedVariables(theme, pool))
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
- // 4. Check for precedence issues
105
- issues.push(...this.checkPrecedenceIssues(tokenColors))
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
- return issues
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
- * Extracts source tokenColors data before compilation for variable analysis.
112
- * This includes data from the main theme file and all imported files.
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 source tokenColors entries
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 getSourceTokenColors(theme) {
229
+ async #getTokenColors(theme) {
118
230
  const sourceTokenColors = []
119
231
 
120
232
  // Get tokenColors from main theme source
121
- if(theme.source?.theme?.tokenColors)
122
- sourceTokenColors.push(...theme.source.theme.tokenColors)
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.dependencies) {
126
- for(const dependency of theme.dependencies) {
237
+ if(theme.hasDependencies()) {
238
+ for(const dependency of theme.getDependencies()) {
127
239
  // Skip main file, already processed
128
- if(dependency.path !== theme.sourceFile.path) {
240
+ if(dependency.getSourceFile().path !== theme.getSourceFile().path) {
129
241
  try {
130
- const depData = await theme.cache.loadCachedData(dependency)
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
- allIssues.forEach(issue => this.reportSingleIssue(issue))
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
- Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is used but not defined in '${issue.rule}' (${issue.property} property)`)
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 tokenColors.
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} tokenColors - Array of tokenColors entries
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(tokenColors, pool) {
479
+ #checkUndefinedVariables(themeData, pool, section = "tokenColors") {
275
480
  const issues = []
276
- const definedVars = pool ? new Set(pool.getTokens.keys()) : new Set()
277
-
278
- tokenColors.forEach((entry, index) => {
279
- const settings = entry.settings || {}
280
- for(const [key, value] of Object.entries(settings)) {
281
- if(typeof value === "string") {
282
- const {none,parens,braces} = Evaluator.sub.exec(value)?.groups ?? {}
283
- const varName = none || parens || braces
284
-
285
- if(!varName)
286
- return
287
-
288
- if(!definedVars.has(varName)) {
289
- issues.push({
290
- type: "undefined-variable",
291
- severity: "high",
292
- variable: value,
293
- rule: entry.name || `Entry ${index + 1}`,
294
- property: key
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
- * Checks for unused variables defined in vars section but not referenced in theme content.
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.source)
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 {cwd} = this
321
- const mainFile = new FileObject(theme.sourceFile.path)
576
+ const cwd = this.getCwd()
577
+ const mainFile = new FileObject(theme.getSourceFile().path)
322
578
  const relativeMainPath = File.relativeOrAbsolutePath(cwd, mainFile)
323
- this.collectVarsDefinitions(theme.source.vars, definedVars, "", relativeMainPath)
579
+
580
+ this.#collectVarsDefinitions(theme.getSource().vars, definedVars, "", relativeMainPath)
324
581
 
325
582
  // Also check dependencies for vars definitions
326
- if(theme.dependencies) {
327
- for(const dependency of theme.dependencies) {
583
+ if(theme.hasDependencies()) {
584
+ for(const dependency of theme.getDependencies()) {
328
585
  try {
329
- const depData = theme.cache?.loadCachedDataSync?.(dependency)
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(cwd, depFile)
333
- this.collectVarsDefinitions(depData.vars, definedVars, "", relativeDependencyPath)
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
- if(theme.source.colors) {
345
- this.findVariableUsage(theme.source.colors, usedVars)
605
+ const themeSource = theme.getSource()
606
+
607
+ if(themeSource?.colors) {
608
+ this.#findVariableUsage(themeSource.colors, usedVars)
346
609
  }
347
610
 
348
- if(theme.source.tokenColors) {
349
- this.findVariableUsage(theme.source.tokenColors, usedVars)
611
+ if(themeSource?.tokenColors) {
612
+ this.#findVariableUsage(themeSource.tokenColors, usedVars)
350
613
  }
351
614
 
352
- if(theme.source.semanticColors) {
353
- this.findVariableUsage(theme.source.semanticColors, usedVars)
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.dependencies) {
358
- for(const dependency of theme.dependencies) {
620
+ if(theme.hasDependencies()) {
621
+ for(const dependency of theme.getDependencies()) {
359
622
  try {
360
- const depData = theme.cache?.loadCachedDataSync?.(dependency)
623
+ const depData = theme.getCache()?.loadCachedDataSync?.(dependency.getSourceFile())
624
+
361
625
  if(depData) {
362
626
  if(depData.colors)
363
- this.findVariableUsage(depData.colors, usedVars)
627
+ this.#findVariableUsage(depData.colors, usedVars)
364
628
 
365
629
  if(depData.tokenColors)
366
- this.findVariableUsage(depData.tokenColors, usedVars)
630
+ this.#findVariableUsage(depData.tokenColors, usedVars)
367
631
 
368
632
  if(depData.semanticColors)
369
- this.findVariableUsage(depData.semanticColors, usedVars)
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 {any} vars - The vars data structure to search
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.collectVarsDefinitions(value, definedVars, varName, filename)
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 {any} data - The data structure to search
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.findVariableUsage(item, usedVars))
702
+ data.forEach(item => this.#findVariableUsage(item, usedVars))
434
703
  } else if(data && typeof data === "object") {
435
- Object.values(data).forEach(value => this.findVariableUsage(value, usedVars))
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
- * Returns issues for cases where a general scope appears after a more specific one.
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.isBroaderScope(current.scope, later.scope)) {
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
- * A broader scope will match the same tokens as a more specific scope, plus others.
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"