@gesslar/sassy 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +605 -0
- package/UNLICENSE.txt +24 -0
- package/package.json +60 -0
- package/src/BuildCommand.js +183 -0
- package/src/Cache.js +73 -0
- package/src/Colour.js +414 -0
- package/src/Command.js +212 -0
- package/src/Compiler.js +310 -0
- package/src/Data.js +545 -0
- package/src/DirectoryObject.js +188 -0
- package/src/Evaluator.js +348 -0
- package/src/File.js +334 -0
- package/src/FileObject.js +226 -0
- package/src/LintCommand.js +498 -0
- package/src/ResolveCommand.js +433 -0
- package/src/Sass.js +165 -0
- package/src/Session.js +360 -0
- package/src/Term.js +175 -0
- package/src/Theme.js +289 -0
- package/src/ThemePool.js +139 -0
- package/src/ThemeToken.js +280 -0
- package/src/Type.js +206 -0
- package/src/Util.js +132 -0
- package/src/Valid.js +50 -0
- package/src/cli.js +155 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file LintCommand.js
|
|
3
|
+
*
|
|
4
|
+
* Defines the LintCommand class for comprehensive theme file validation.
|
|
5
|
+
* Provides static analysis of theme configuration files to identify:
|
|
6
|
+
* - Duplicate scope definitions across tokenColor rules
|
|
7
|
+
* - Undefined variable references in theme content
|
|
8
|
+
* - Unused variables defined in vars but never referenced
|
|
9
|
+
* - Scope precedence issues where broad scopes mask specific ones
|
|
10
|
+
* - TextMate scope selector conflicts and redundancies
|
|
11
|
+
*
|
|
12
|
+
* Integrates with the theme compilation pipeline to analyse both source
|
|
13
|
+
* and compiled theme data, ensuring accurate variable resolution tracking.
|
|
14
|
+
* Supports modular themes with import dependencies and provides detailed,
|
|
15
|
+
* colour-coded reporting for different severity levels.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import c from "@gesslar/colours"
|
|
19
|
+
// import colorSupport from "color-support"
|
|
20
|
+
|
|
21
|
+
import Command from "./Command.js"
|
|
22
|
+
import Evaluator from "./Evaluator.js"
|
|
23
|
+
import Term from "./Term.js"
|
|
24
|
+
import Theme from "./Theme.js"
|
|
25
|
+
import ThemePool from "./ThemePool.js"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// oops, need to have @gesslar/colours support this, too!
|
|
29
|
+
// ansiColors.enabled = colorSupport.hasBasic
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Command handler for linting theme files for potential issues.
|
|
33
|
+
* Validates tokenColors for duplicate scopes, undefined variables, unused
|
|
34
|
+
* variables, and precedence issues that could cause unexpected theme
|
|
35
|
+
* behaviour.
|
|
36
|
+
*/
|
|
37
|
+
export default class LintCommand extends Command {
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new LintCommand instance.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} base - Base configuration containing cwd and packageJson
|
|
42
|
+
*/
|
|
43
|
+
constructor(base) {
|
|
44
|
+
super(base)
|
|
45
|
+
|
|
46
|
+
this.cliCommand = "lint <file>"
|
|
47
|
+
this.cliOptions = {
|
|
48
|
+
// Future options could include:
|
|
49
|
+
// "fix": ["-f, --fix", "automatically fix issues where possible"],
|
|
50
|
+
// "strict": ["--strict", "treat warnings as errors"],
|
|
51
|
+
// "format": ["--format <type>", "output format (text, json)", "text"],
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Executes the lint command for a given theme file.
|
|
57
|
+
* Validates the theme and reports any issues found.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} inputArg - Path to the theme file to lint
|
|
60
|
+
* @param {object} options - Linting options
|
|
61
|
+
* @returns {Promise<void>} Resolves when linting is complete
|
|
62
|
+
*/
|
|
63
|
+
async execute(inputArg, options = {}) {
|
|
64
|
+
const {cwd} = this
|
|
65
|
+
const fileObject = await this.resolveThemeFileName(inputArg, cwd)
|
|
66
|
+
const theme = new Theme(fileObject, cwd, options)
|
|
67
|
+
|
|
68
|
+
theme.cache = this.cache
|
|
69
|
+
|
|
70
|
+
await theme.load()
|
|
71
|
+
await theme.build()
|
|
72
|
+
|
|
73
|
+
const issues = await this.lintTheme(theme)
|
|
74
|
+
|
|
75
|
+
this.reportIssues(issues)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Performs comprehensive linting of a theme.
|
|
80
|
+
* Returns an array of issues found during validation.
|
|
81
|
+
*
|
|
82
|
+
* @param {Theme} theme - The compiled theme object
|
|
83
|
+
* @returns {Promise<Array>} Array of lint issues
|
|
84
|
+
*/
|
|
85
|
+
async lintTheme(theme) {
|
|
86
|
+
const issues = []
|
|
87
|
+
const tokenColors = theme.output?.tokenColors || []
|
|
88
|
+
const pool = theme.pool
|
|
89
|
+
|
|
90
|
+
// Get source tokenColors data (before compilation) for variable usage analysis
|
|
91
|
+
const sourceTokenColors = await this.getSourceTokenColors(theme)
|
|
92
|
+
|
|
93
|
+
// 1. Check for duplicate scopes
|
|
94
|
+
issues.push(...this.checkDuplicateScopes(tokenColors))
|
|
95
|
+
|
|
96
|
+
// 2. Check for undefined variables
|
|
97
|
+
issues.push(...this.checkUndefinedVariables(sourceTokenColors, pool))
|
|
98
|
+
|
|
99
|
+
// 3. Check for unused variables
|
|
100
|
+
issues.push(...this.checkUnusedVariables(theme, pool))
|
|
101
|
+
|
|
102
|
+
// 4. Check for precedence issues
|
|
103
|
+
issues.push(...this.checkPrecedenceIssues(tokenColors))
|
|
104
|
+
|
|
105
|
+
return issues
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extracts source tokenColors data before compilation for variable analysis.
|
|
110
|
+
* This includes data from the main theme file and all imported files.
|
|
111
|
+
*
|
|
112
|
+
* @param {Theme} theme - The compiled theme object
|
|
113
|
+
* @returns {Promise<Array>} Array of source tokenColors entries
|
|
114
|
+
*/
|
|
115
|
+
async getSourceTokenColors(theme) {
|
|
116
|
+
const sourceTokenColors = []
|
|
117
|
+
|
|
118
|
+
// Get tokenColors from main theme source
|
|
119
|
+
if(theme.source?.theme?.tokenColors)
|
|
120
|
+
sourceTokenColors.push(...theme.source.theme.tokenColors)
|
|
121
|
+
|
|
122
|
+
// Get tokenColors from imported files
|
|
123
|
+
if(theme.dependencies) {
|
|
124
|
+
for(const dependency of theme.dependencies) {
|
|
125
|
+
// Skip main file, already processed
|
|
126
|
+
if(dependency.path !== theme.sourceFile.path) {
|
|
127
|
+
try {
|
|
128
|
+
const depData = await theme.cache.loadCachedData(dependency)
|
|
129
|
+
if(depData?.theme?.tokenColors)
|
|
130
|
+
sourceTokenColors.push(...depData.theme.tokenColors)
|
|
131
|
+
} catch {
|
|
132
|
+
// nothing to see here.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return sourceTokenColors
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reports lint issues to the user with appropriate formatting and colors.
|
|
143
|
+
*
|
|
144
|
+
* @param {Array} issues - Array of lint issues to report
|
|
145
|
+
*/
|
|
146
|
+
reportIssues(issues) {
|
|
147
|
+
if(issues.length === 0) {
|
|
148
|
+
Term.info(c`{success}✓{/} No linting issues found`)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const errors = issues.filter(i => i.severity === "high")
|
|
153
|
+
const warnings = issues.filter(i => i.severity === "medium")
|
|
154
|
+
const infos = issues.filter(i => i.severity === "low")
|
|
155
|
+
|
|
156
|
+
const allIssues = errors.concat(warnings, infos)
|
|
157
|
+
allIssues.forEach(issue => this.reportSingleIssue(issue))
|
|
158
|
+
|
|
159
|
+
// Clean summary
|
|
160
|
+
const parts = []
|
|
161
|
+
|
|
162
|
+
if(errors.length > 0)
|
|
163
|
+
parts.push(`${errors.length} error${errors.length === 1 ? "" : "s"}`)
|
|
164
|
+
|
|
165
|
+
if(warnings.length > 0)
|
|
166
|
+
parts.push(`${warnings.length} warning${warnings.length === 1 ? "" : "s"}`)
|
|
167
|
+
|
|
168
|
+
if(infos.length > 0)
|
|
169
|
+
parts.push(`${infos.length} info`)
|
|
170
|
+
|
|
171
|
+
Term.info(`\n${parts.join(", ")}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#getIndicator(severity) {
|
|
175
|
+
switch(severity) {
|
|
176
|
+
case "high": return c`{error}●{/}`
|
|
177
|
+
case "medium": return c`{warn}●{/}`
|
|
178
|
+
case "low":
|
|
179
|
+
default: return c`{info}●{/}`
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Reports a single lint issue with clean, minimal formatting.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} issue - The issue to report
|
|
187
|
+
*/
|
|
188
|
+
reportSingleIssue(issue) {
|
|
189
|
+
const indicator = this.#getIndicator(issue.severity)
|
|
190
|
+
|
|
191
|
+
switch(issue.type) {
|
|
192
|
+
case "duplicate-scope": {
|
|
193
|
+
const rules = issue.occurrences.map(occ => `'${occ.name}'`).join(", ")
|
|
194
|
+
Term.info(c`${indicator} Scope '{context}${issue.scope}{/}' is duplicated in ${rules}`)
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case "undefined-variable": {
|
|
199
|
+
Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is used but not defined in '${issue.rule}' (${issue.property} property)`)
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case "unused-variable": {
|
|
204
|
+
Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is defined but never used`)
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case "precedence-issue": {
|
|
209
|
+
if(issue.broadIndex === issue.specificIndex) {
|
|
210
|
+
Term.info(c`${indicator} Scope '{context}${issue.broadScope}{/}' makes more specific '{context}${issue.specificScope}' redundant in '${issue.broadRule}{/}'`)
|
|
211
|
+
} else {
|
|
212
|
+
Term.info(c`${indicator} Scope '{context}${issue.broadScope}{/}' in '${issue.broadRule}' masks more specific '{context}${issue.specificScope}{/}' in '${issue.specificRule}'`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
break
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Checks for duplicate scopes across tokenColors rules.
|
|
222
|
+
* Returns issues for scopes that appear in multiple rules.
|
|
223
|
+
*
|
|
224
|
+
* @param {Array} tokenColors - Array of tokenColors entries
|
|
225
|
+
* @returns {Array} Array of duplicate scope issues
|
|
226
|
+
*/
|
|
227
|
+
checkDuplicateScopes(tokenColors) {
|
|
228
|
+
const issues = []
|
|
229
|
+
const scopeOccurrences = new Map()
|
|
230
|
+
|
|
231
|
+
tokenColors.forEach((entry, index) => {
|
|
232
|
+
if(!entry.scope)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
const scopes = entry.scope.split(",").map(s => s.trim())
|
|
236
|
+
scopes.forEach(scope => {
|
|
237
|
+
if(!scopeOccurrences.has(scope)) {
|
|
238
|
+
scopeOccurrences.set(scope, [])
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
scopeOccurrences.get(scope).push({
|
|
242
|
+
index: index + 1,
|
|
243
|
+
name: entry.name || `Entry ${index + 1}`,
|
|
244
|
+
entry
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// Report duplicate scopes
|
|
250
|
+
for(const [scope, occurrences] of scopeOccurrences) {
|
|
251
|
+
if(occurrences.length > 1) {
|
|
252
|
+
issues.push({
|
|
253
|
+
type: "duplicate-scope",
|
|
254
|
+
severity: "medium",
|
|
255
|
+
scope,
|
|
256
|
+
occurrences
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return issues
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Checks for undefined variables referenced in tokenColors.
|
|
266
|
+
* Returns issues for variables that are used but not defined.
|
|
267
|
+
*
|
|
268
|
+
* @param {Array} tokenColors - Array of tokenColors entries
|
|
269
|
+
* @param {ThemePool} pool - The theme's variable pool
|
|
270
|
+
* @returns {Array} Array of undefined variable issues
|
|
271
|
+
*/
|
|
272
|
+
checkUndefinedVariables(tokenColors, pool) {
|
|
273
|
+
const issues = []
|
|
274
|
+
const definedVars = pool ? new Set(pool.getTokens.keys()) : new Set()
|
|
275
|
+
|
|
276
|
+
tokenColors.forEach((entry, index) => {
|
|
277
|
+
const settings = entry.settings || {}
|
|
278
|
+
for(const [key, value] of Object.entries(settings)) {
|
|
279
|
+
if(typeof value === "string") {
|
|
280
|
+
const {none,parens,braces} = Evaluator.sub.exec(value)?.groups ?? {}
|
|
281
|
+
const varName = none || parens || braces
|
|
282
|
+
|
|
283
|
+
if(!varName)
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
if(!definedVars.has(varName)) {
|
|
287
|
+
issues.push({
|
|
288
|
+
type: "undefined-variable",
|
|
289
|
+
severity: "high",
|
|
290
|
+
variable: value,
|
|
291
|
+
rule: entry.name || `Entry ${index + 1}`,
|
|
292
|
+
property: key
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
return issues
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Checks for unused variables defined in vars section but not referenced in theme content.
|
|
304
|
+
* Returns issues for variables that are defined in vars but never used.
|
|
305
|
+
*
|
|
306
|
+
* @param {Theme} theme - The compiled theme object
|
|
307
|
+
* @param {ThemePool} pool - The theme's variable pool
|
|
308
|
+
* @returns {Array} Array of unused variable issues
|
|
309
|
+
*/
|
|
310
|
+
checkUnusedVariables(theme, pool) {
|
|
311
|
+
const issues = []
|
|
312
|
+
|
|
313
|
+
if(!pool || !theme.source)
|
|
314
|
+
return issues
|
|
315
|
+
|
|
316
|
+
// Get variables defined in the vars section only
|
|
317
|
+
const definedVars = new Set()
|
|
318
|
+
this.collectVarsDefinitions(theme.source.vars, definedVars)
|
|
319
|
+
|
|
320
|
+
// Also check dependencies for vars definitions
|
|
321
|
+
if(theme.dependencies) {
|
|
322
|
+
for(const dependency of theme.dependencies) {
|
|
323
|
+
try {
|
|
324
|
+
const depData = theme.cache?.loadCachedDataSync?.(dependency)
|
|
325
|
+
if(depData?.vars) {
|
|
326
|
+
this.collectVarsDefinitions(depData.vars, definedVars)
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
// Ignore cache errors
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const usedVars = new Set()
|
|
335
|
+
|
|
336
|
+
// Find variable usage in colors, tokenColors, and semanticColors sections
|
|
337
|
+
if(theme.source.colors) {
|
|
338
|
+
this.findVariableUsage(theme.source.colors, usedVars)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if(theme.source.tokenColors) {
|
|
342
|
+
this.findVariableUsage(theme.source.tokenColors, usedVars)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if(theme.source.semanticColors) {
|
|
346
|
+
this.findVariableUsage(theme.source.semanticColors, usedVars)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Also check dependencies for usage in these sections
|
|
350
|
+
if(theme.dependencies) {
|
|
351
|
+
for(const dependency of theme.dependencies) {
|
|
352
|
+
try {
|
|
353
|
+
const depData = theme.cache?.loadCachedDataSync?.(dependency)
|
|
354
|
+
if(depData) {
|
|
355
|
+
if(depData.colors)
|
|
356
|
+
this.findVariableUsage(depData.colors, usedVars)
|
|
357
|
+
|
|
358
|
+
if(depData.tokenColors)
|
|
359
|
+
this.findVariableUsage(depData.tokenColors, usedVars)
|
|
360
|
+
|
|
361
|
+
if(depData.semanticColors)
|
|
362
|
+
this.findVariableUsage(depData.semanticColors, usedVars)
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
// Ignore cache errors
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Find vars-defined variables that are never used in content sections
|
|
371
|
+
for(const varName of definedVars) {
|
|
372
|
+
if(!usedVars.has(varName)) {
|
|
373
|
+
issues.push({
|
|
374
|
+
type: "unused-variable",
|
|
375
|
+
severity: "low",
|
|
376
|
+
variable: `$${varName}`
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return issues
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Recursively collects variable names defined in the vars section.
|
|
386
|
+
* Adds found variable names to the definedVars set.
|
|
387
|
+
*
|
|
388
|
+
* @param {any} vars - The vars data structure to search
|
|
389
|
+
* @param {Set} definedVars - Set to add found variable names to
|
|
390
|
+
* @param {string} prefix - Current prefix for nested vars
|
|
391
|
+
*/
|
|
392
|
+
collectVarsDefinitions(vars, definedVars, prefix = "") {
|
|
393
|
+
if(!vars || typeof vars !== "object")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
for(const [key, value] of Object.entries(vars)) {
|
|
397
|
+
const varName = prefix ? `${prefix}.${key}` : key
|
|
398
|
+
definedVars.add(varName)
|
|
399
|
+
|
|
400
|
+
// If the value is an object, recurse for nested definitions
|
|
401
|
+
if(value && typeof value === "object" && !Array.isArray(value)) {
|
|
402
|
+
this.collectVarsDefinitions(value, definedVars, varName)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Recursively finds variable usage in any data structure.
|
|
409
|
+
* Adds found variable names to the usedVars set.
|
|
410
|
+
*
|
|
411
|
+
* @param {any} data - The data structure to search
|
|
412
|
+
* @param {Set} usedVars - Set to add found variable names to
|
|
413
|
+
*/
|
|
414
|
+
findVariableUsage(data, usedVars) {
|
|
415
|
+
if(typeof data === "string") {
|
|
416
|
+
if(Evaluator.sub.test(data)) {
|
|
417
|
+
const {none, parens, braces} = Evaluator.sub.exec(data)?.groups ?? {}
|
|
418
|
+
const varName = none || parens || braces
|
|
419
|
+
if(varName) {
|
|
420
|
+
usedVars.add(varName)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else if(Array.isArray(data)) {
|
|
424
|
+
data.forEach(item => this.findVariableUsage(item, usedVars))
|
|
425
|
+
} else if(data && typeof data === "object") {
|
|
426
|
+
Object.values(data).forEach(value => this.findVariableUsage(value, usedVars))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Checks for precedence issues where broad scopes override specific ones.
|
|
432
|
+
* Returns issues for cases where a general scope appears after a more specific one.
|
|
433
|
+
*
|
|
434
|
+
* @param {Array} tokenColors - Array of tokenColors entries
|
|
435
|
+
* @returns {Array} Array of precedence issue warnings
|
|
436
|
+
*/
|
|
437
|
+
checkPrecedenceIssues(tokenColors) {
|
|
438
|
+
const issues = []
|
|
439
|
+
const allScopes = []
|
|
440
|
+
|
|
441
|
+
// Build a flat list of all scopes with their rule info
|
|
442
|
+
tokenColors.forEach((entry, index) => {
|
|
443
|
+
if(!entry.scope)
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
const scopes = entry.scope.split(",").map(s => s.trim())
|
|
447
|
+
scopes.forEach(scope => {
|
|
448
|
+
allScopes.push({
|
|
449
|
+
scope,
|
|
450
|
+
index: index + 1,
|
|
451
|
+
name: entry.name || `Entry ${index + 1}`,
|
|
452
|
+
entry
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// Check each scope against all later scopes
|
|
458
|
+
for(let i = 0; i < allScopes.length; i++) {
|
|
459
|
+
const current = allScopes[i]
|
|
460
|
+
|
|
461
|
+
for(let j = i + 1; j < allScopes.length; j++) {
|
|
462
|
+
const later = allScopes[j]
|
|
463
|
+
|
|
464
|
+
// Check if the current (earlier) scope is broader than the later one
|
|
465
|
+
// This means the broad scope will mask the specific scope
|
|
466
|
+
if(this.isBroaderScope(current.scope, later.scope)) {
|
|
467
|
+
issues.push({
|
|
468
|
+
type: "precedence-issue",
|
|
469
|
+
severity: current.index === later.index ? "low" : "high",
|
|
470
|
+
specificScope: later.scope,
|
|
471
|
+
broadScope: current.scope,
|
|
472
|
+
specificRule: later.name,
|
|
473
|
+
broadRule: current.name,
|
|
474
|
+
specificIndex: later.index,
|
|
475
|
+
broadIndex: current.index
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return issues
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Determines if one scope is broader than another.
|
|
486
|
+
* A broader scope will match the same tokens as a more specific scope, plus others.
|
|
487
|
+
*
|
|
488
|
+
* @param {string} broadScope - The potentially broader scope
|
|
489
|
+
* @param {string} specificScope - The potentially more specific scope
|
|
490
|
+
* @returns {boolean} True if broadScope is broader than specificScope
|
|
491
|
+
*/
|
|
492
|
+
isBroaderScope(broadScope, specificScope) {
|
|
493
|
+
// Simple heuristic: if the specific scope starts with the broad scope + "."
|
|
494
|
+
// then the broad scope is indeed broader
|
|
495
|
+
// e.g., "keyword" is broader than "keyword.control", "keyword.control.import"
|
|
496
|
+
return specificScope.startsWith(broadScope + ".")
|
|
497
|
+
}
|
|
498
|
+
}
|