Skip to content

Commit de93dd0

Browse files
authored
Linter: Access rule type from class instead of instance (#1298)
This pull request refactors the linter by accessing the `static type` field directly on rule classes instead of instantiating rules and reading it from `rule.constructor`. The instance-level type guards (`isLexerRule`, `isSourceRule`, `isParserRule`) are replaced with class-level equivalents (`isLexerRuleClass`, `isSourceRuleClass`, `isParserRuleClass`) that take a `ruleClass` rather than a Rule instance. It also extracts a `findRuleClass(ruleName)` helper to deduplicate the six occurrences of `this.rules.find(rule => rule.ruleName === ...)`, and renames `RuleClass` local variables to `ruleClass` to follow camelCase conventions for non-type identifiers. Follow up on #1268
1 parent 88e8e76 commit de93dd0

File tree

1 file changed

+57
-46
lines changed

1 file changed

+57
-46
lines changed

javascript/packages/linter/src/linter.ts

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -160,32 +160,37 @@ export class Linter {
160160
return this.rules.length
161161
}
162162

163+
protected findRuleClass(ruleName: string): RuleClass | undefined {
164+
return this.rules.find(ruleClass => ruleClass.ruleName === ruleName)
165+
}
166+
163167
/**
164-
* Type guard to check if a rule is a LexerRule
168+
* Type guard to check if a rule class is a LexerRule class
165169
*/
166-
protected isLexerRule(rule: Rule): rule is LexerRule {
167-
return (rule.constructor as any).type === "lexer"
170+
protected isLexerRuleClass(ruleClass: RuleClass): boolean {
171+
return ruleClass.type === "lexer"
168172
}
169173

170174
/**
171-
* Type guard to check if a rule is a SourceRule
175+
* Type guard to check if a rule class is a SourceRule class
172176
*/
173-
protected isSourceRule(rule: Rule): rule is SourceRule {
174-
return (rule.constructor as any).type === "source"
177+
protected isSourceRuleClass(ruleClass: RuleClass): boolean {
178+
return ruleClass.type === "source"
175179
}
176180

177181
/**
178-
* Type guard to check if a rule is a ParserRule
182+
* Type guard to check if a rule class is a ParserRule class
179183
*/
180-
protected isParserRule(rule: Rule): rule is ParserRule {
181-
return (rule.constructor as any).type === "parser"
184+
protected isParserRuleClass(ruleClass: RuleClass): boolean {
185+
return ruleClass.type === "parser" || ruleClass.type === undefined
182186
}
183187

184188
/**
185189
* Execute a single rule and return its unbound offenses.
186190
* Handles rule type checking (Lexer/Parser/Source) and isEnabled checks.
187191
*/
188192
private executeRule(
193+
ruleClass: RuleClass,
189194
rule: Rule,
190195
parseResult: ParseResult,
191196
lexResult: LexResult,
@@ -215,34 +220,40 @@ export class Linter {
215220
let isEnabled = true
216221
let ruleOffenses: UnboundLintOffense[]
217222

218-
if (this.isLexerRule(rule)) {
219-
if (rule.isEnabled) {
220-
isEnabled = rule.isEnabled(lexResult, context)
223+
if (this.isLexerRuleClass(ruleClass)) {
224+
const lexerRule = rule as LexerRule
225+
226+
if (lexerRule.isEnabled) {
227+
isEnabled = lexerRule.isEnabled(lexResult, context)
221228
}
222229

223230
if (isEnabled) {
224-
ruleOffenses = (rule as LexerRule).check(lexResult, context)
231+
ruleOffenses = lexerRule.check(lexResult, context)
225232
} else {
226233
ruleOffenses = []
227234
}
228235

229-
} else if (this.isSourceRule(rule)) {
230-
if (rule.isEnabled) {
231-
isEnabled = rule.isEnabled(source, context)
236+
} else if (this.isSourceRuleClass(ruleClass)) {
237+
const sourceRule = rule as SourceRule
238+
239+
if (sourceRule.isEnabled) {
240+
isEnabled = sourceRule.isEnabled(source, context)
232241
}
233242

234243
if (isEnabled) {
235-
ruleOffenses = (rule as SourceRule).check(source, context)
244+
ruleOffenses = sourceRule.check(source, context)
236245
} else {
237246
ruleOffenses = []
238247
}
239248
} else {
240-
if (rule.isEnabled) {
241-
isEnabled = rule.isEnabled(parseResult, context)
249+
const parserRule = rule as ParserRule
250+
251+
if (parserRule.isEnabled) {
252+
isEnabled = parserRule.isEnabled(parseResult, context)
242253
}
243254

244255
if (isEnabled) {
245-
ruleOffenses = (rule as ParserRule).check(parseResult, context)
256+
ruleOffenses = parserRule.check(parseResult, context)
246257
} else {
247258
ruleOffenses = []
248259
}
@@ -336,7 +347,7 @@ export class Linter {
336347
const herbDisableCache = new Map<number, string[]>()
337348

338349
if (hasParserErrors) {
339-
const hasParserRule = this.rules.find(RuleClass => RuleClass.ruleName === "parser-no-errors")
350+
const hasParserRule = this.findRuleClass("parser-no-errors")
340351

341352
if (hasParserRule) {
342353
const rule = new ParserNoErrorsRule()
@@ -356,31 +367,31 @@ export class Linter {
356367

357368
context = {
358369
...context,
359-
validRuleNames: this.getAvailableRules().map(RuleClass => RuleClass.ruleName),
370+
validRuleNames: this.getAvailableRules().map(ruleClass => ruleClass.ruleName),
360371
ignoredOffensesByLine
361372
}
362373

363-
const regularRules = this.rules.filter(RuleClass => RuleClass.ruleName !== "herb-disable-comment-unnecessary")
374+
const regularRules = this.rules.filter(ruleClass => ruleClass.ruleName !== "herb-disable-comment-unnecessary")
364375

365-
for (const RuleClass of regularRules) {
366-
const rule = new RuleClass()
367-
const parserOptions = this.isParserRule(rule) ? rule.parserOptions : {}
376+
for (const ruleClass of regularRules) {
377+
const rule = new ruleClass()
378+
const parserOptions = this.isParserRuleClass(ruleClass) ? (rule as ParserRule).parserOptions : {}
368379
const parseResult = this.parseCache.get(source, parserOptions)
369380

370381
// Skip parser rules whose parse result has errors (parser-no-errors handled above)
371382
// Skip lexer/source rules when the default parse has errors
372-
if (this.isParserRule(rule)) {
383+
if (this.isParserRuleClass(ruleClass)) {
373384
if (parseResult.recursiveErrors().length > 0) continue
374385
} else if (hasParserErrors) {
375386
continue
376387
}
377388

378-
const unboundOffenses = this.executeRule(rule, parseResult, lexResult, source, context)
379-
const boundOffenses = this.bindSeverity(unboundOffenses, RuleClass.ruleName)
389+
const unboundOffenses = this.executeRule(ruleClass, rule, parseResult, lexResult, source, context)
390+
const boundOffenses = this.bindSeverity(unboundOffenses, ruleClass.ruleName)
380391

381392
const { kept, ignored, wouldBeIgnored } = this.filterOffenses(
382393
boundOffenses,
383-
RuleClass.ruleName,
394+
ruleClass.ruleName,
384395
ignoredOffensesByLine,
385396
herbDisableCache,
386397
context?.ignoreDisableComments
@@ -391,7 +402,7 @@ export class Linter {
391402
this.offenses.push(...kept)
392403
}
393404

394-
const unnecessaryRuleClass = this.rules.find(RuleClass => RuleClass.ruleName === "herb-disable-comment-unnecessary")
405+
const unnecessaryRuleClass = this.findRuleClass("herb-disable-comment-unnecessary")
395406

396407
if (unnecessaryRuleClass) {
397408
const unnecessaryRule = new unnecessaryRuleClass() as ParserRule
@@ -437,16 +448,16 @@ export class Linter {
437448
* @returns Array of offenses with severity bound
438449
*/
439450
protected bindSeverity(unboundOffenses: UnboundLintOffense[], ruleName: string): LintOffense[] {
440-
const RuleClass = this.rules.find(rule => rule.ruleName === ruleName)
451+
const ruleClass = this.findRuleClass(ruleName)
441452

442-
if (!RuleClass) {
453+
if (!ruleClass) {
443454
return unboundOffenses.map(offense => ({
444455
...offense,
445456
severity: "error" as const
446457
}))
447458
}
448459

449-
const ruleInstance = new RuleClass()
460+
const ruleInstance = new ruleClass()
450461
const defaultSeverity = ruleInstance.defaultConfig?.severity ?? DEFAULT_RULE_CONFIG.severity
451462

452463
const userRuleConfig = this.config?.linter?.rules?.[ruleName]
@@ -477,13 +488,13 @@ export class Linter {
477488
const sourceOffenses: LintOffense[] = []
478489

479490
for (const offense of lintResult.offenses) {
480-
const RuleClass = this.rules.find(rule => rule.ruleName === offense.rule)
491+
const ruleClass = this.findRuleClass(offense.rule)
481492

482-
if (!RuleClass) continue
493+
if (!ruleClass) continue
483494

484-
if ((RuleClass as any).type === "lexer") {
495+
if (this.isLexerRuleClass(ruleClass)) {
485496
lexerOffenses.push(offense)
486-
} else if ((RuleClass as any).type === "source") {
497+
} else if (this.isSourceRuleClass(ruleClass)) {
487498
sourceOffenses.push(offense)
488499
} else {
489500
parserOffenses.push(offense)
@@ -498,16 +509,16 @@ export class Linter {
498509
const parseResult = this.parseCache.get(currentSource)
499510

500511
for (const offense of parserOffenses) {
501-
const RuleClass = this.rules.find(rule => rule.ruleName === offense.rule)
512+
const ruleClass = this.findRuleClass(offense.rule)
502513

503-
if (!RuleClass) {
514+
if (!ruleClass) {
504515
unfixed.push(offense)
505516

506517
continue
507518
}
508519

509-
const rule = new RuleClass() as ParserRule
510-
const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
520+
const rule = new ruleClass() as ParserRule
521+
const isUnsafe = (ruleClass as any).unsafeAutocorrectable === true
511522

512523
if (!rule.autofix) {
513524
unfixed.push(offense)
@@ -565,15 +576,15 @@ export class Linter {
565576
})
566577

567578
for (const offense of sortedSourceOffenses) {
568-
const RuleClass = this.rules.find(rule => rule.ruleName === offense.rule)
579+
const ruleClass = this.findRuleClass(offense.rule)
569580

570-
if (!RuleClass) {
581+
if (!ruleClass) {
571582
unfixed.push(offense)
572583
continue
573584
}
574585

575-
const rule = new RuleClass() as SourceRule
576-
const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
586+
const rule = new ruleClass() as SourceRule
587+
const isUnsafe = (ruleClass as any).unsafeAutocorrectable === true
577588

578589
if (!rule.autofix) {
579590
unfixed.push(offense)

0 commit comments

Comments
 (0)