Skip to main content

bamts_compiler/
lint.rs

1use std::{fmt, fmt::Write as _, str::FromStr, sync::Arc};
2
3use crate::source::ScriptKind;
4
5/// A lint's ordered severity. A `forbid` level is an immutable lock.
6#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub enum LintLevel {
8    Allow,
9    Warn,
10    Deny,
11    Forbid,
12}
13
14impl LintLevel {
15    #[must_use]
16    pub const fn as_str(self) -> &'static str {
17        match self {
18            Self::Allow => "allow",
19            Self::Warn => "warn",
20            Self::Deny => "deny",
21            Self::Forbid => "forbid",
22        }
23    }
24}
25
26impl fmt::Display for LintLevel {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        formatter.write_str(self.as_str())
29    }
30}
31
32impl FromStr for LintLevel {
33    type Err = ParseLintLevelError;
34
35    fn from_str(value: &str) -> Result<Self, Self::Err> {
36        match value {
37            "allow" => Ok(Self::Allow),
38            "warn" => Ok(Self::Warn),
39            "deny" => Ok(Self::Deny),
40            "forbid" => Ok(Self::Forbid),
41            _ => Err(ParseLintLevelError(Arc::from(value))),
42        }
43    }
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct ParseLintLevelError(Arc<str>);
48
49impl fmt::Display for ParseLintLevelError {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(formatter, "unknown lint level {:?}", self.0)
52    }
53}
54
55impl std::error::Error for ParseLintLevelError {}
56
57/// The eleven stable semantic families exposed by `bamts.toml` and the CLI.
58#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
59pub enum RuleGroup {
60    Unsoundness,
61    EscapeHatches,
62    NonErasable,
63    LegacySyntax,
64    Modules,
65    ClassSemantics,
66    EnumSemantics,
67    DeclarationMerging,
68    JavaScriptCompatibility,
69    Opinionated,
70    ControlFlow,
71}
72
73impl RuleGroup {
74    pub const ALL: [Self; 11] = [
75        Self::Unsoundness,
76        Self::EscapeHatches,
77        Self::NonErasable,
78        Self::LegacySyntax,
79        Self::Modules,
80        Self::ClassSemantics,
81        Self::EnumSemantics,
82        Self::DeclarationMerging,
83        Self::JavaScriptCompatibility,
84        Self::Opinionated,
85        Self::ControlFlow,
86    ];
87
88    #[must_use]
89    pub const fn slug(self) -> &'static str {
90        match self {
91            Self::Unsoundness => "unsoundness",
92            Self::EscapeHatches => "escape-hatches",
93            Self::NonErasable => "non-erasable",
94            Self::LegacySyntax => "legacy-syntax",
95            Self::Modules => "modules",
96            Self::ClassSemantics => "class-semantics",
97            Self::EnumSemantics => "enum-semantics",
98            Self::DeclarationMerging => "declaration-merging",
99            Self::JavaScriptCompatibility => "javascript-compatibility",
100            Self::Opinionated => "opinionated",
101            Self::ControlFlow => "control-flow",
102        }
103    }
104}
105
106impl FromStr for RuleGroup {
107    type Err = ParseRuleGroupError;
108
109    fn from_str(value: &str) -> Result<Self, Self::Err> {
110        Self::ALL
111            .into_iter()
112            .find(|group| group.slug() == value)
113            .ok_or_else(|| ParseRuleGroupError(Arc::from(value)))
114    }
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct ParseRuleGroupError(Arc<str>);
119
120impl fmt::Display for ParseRuleGroupError {
121    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(formatter, "unknown lint group {:?}", self.0)
123    }
124}
125
126impl std::error::Error for ParseRuleGroupError {}
127
128/// The inseparable stable and human-readable identity of one registered rule.
129#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub struct RuleId {
131    code: &'static str,
132    slug: &'static str,
133}
134
135impl RuleId {
136    #[must_use]
137    pub const fn code(self) -> &'static str {
138        self.code
139    }
140
141    #[must_use]
142    pub const fn slug(self) -> &'static str {
143        self.slug
144    }
145}
146
147/// One source in an executable lint example. `resolves_to` names another source
148/// in the same example program and is used only when module semantics require it.
149#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
150pub struct RuleExampleSource {
151    script_kind: ScriptKind,
152    text: &'static str,
153    resolves_to: Option<usize>,
154}
155
156impl RuleExampleSource {
157    #[must_use]
158    pub const fn new(script_kind: ScriptKind, text: &'static str) -> Self {
159        Self {
160            script_kind,
161            text,
162            resolves_to: None,
163        }
164    }
165
166    #[must_use]
167    pub const fn resolving_to(mut self, source: usize) -> Self {
168        self.resolves_to = Some(source);
169        self
170    }
171
172    #[must_use]
173    pub const fn script_kind(self) -> ScriptKind {
174        self.script_kind
175    }
176
177    #[must_use]
178    pub const fn text(self) -> &'static str {
179        self.text
180    }
181
182    #[must_use]
183    pub const fn resolves_to(self) -> Option<usize> {
184        self.resolves_to
185    }
186}
187
188/// Compiler-option state consumed by configuration-phase lint rules.
189#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
190pub struct CompilerLintOptions {
191    pub preserve_const_enums: bool,
192    pub emit_decorator_metadata: bool,
193    pub use_define_for_class_fields: bool,
194}
195
196impl CompilerLintOptions {
197    pub const STANDARD: Self = Self {
198        preserve_const_enums: false,
199        emit_decorator_metadata: false,
200        use_define_for_class_fields: true,
201    };
202}
203
204/// The typed execution phase for one catalog example.
205#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
206pub enum RuleExampleCase {
207    Source(RuleExampleSource),
208    Program(&'static [RuleExampleSource]),
209    CompilerOptions(CompilerLintOptions),
210}
211
212/// A positive and negative executable contract carried by one rule definition.
213#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
214pub struct RuleExamples {
215    trigger: RuleExampleCase,
216    clean: RuleExampleCase,
217}
218
219impl RuleExamples {
220    #[must_use]
221    pub const fn new(trigger: RuleExampleCase, clean: RuleExampleCase) -> Self {
222        Self { trigger, clean }
223    }
224
225    #[must_use]
226    pub const fn trigger(self) -> RuleExampleCase {
227        self.trigger
228    }
229
230    #[must_use]
231    pub const fn clean(self) -> RuleExampleCase {
232        self.clean
233    }
234}
235
236/// The complete metadata required to register a lint rule.
237///
238/// Fields and construction are private: a rule identity can only come from [`RULES`],
239/// so a code or slug cannot exist without the rest of its metadata.
240#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
241pub struct RuleDefinition {
242    id: RuleId,
243    group: RuleGroup,
244    default_level: LintLevel,
245    rationale: &'static str,
246    sound_alternative: &'static str,
247    silence_flag: &'static str,
248    examples: RuleExamples,
249}
250
251impl RuleDefinition {
252    const fn new(
253        id: RuleId,
254        group: RuleGroup,
255        default_level: LintLevel,
256        rationale: &'static str,
257        sound_alternative: &'static str,
258        silence_flag: &'static str,
259        examples: RuleExamples,
260    ) -> Self {
261        Self {
262            id,
263            group,
264            default_level,
265            rationale,
266            sound_alternative,
267            silence_flag,
268            examples,
269        }
270    }
271
272    #[must_use]
273    pub const fn id(&self) -> RuleId {
274        self.id
275    }
276
277    #[must_use]
278    pub const fn code(&self) -> &'static str {
279        self.id.code()
280    }
281
282    #[must_use]
283    pub const fn slug(&self) -> &'static str {
284        self.id.slug()
285    }
286
287    #[must_use]
288    pub const fn group(&self) -> RuleGroup {
289        self.group
290    }
291
292    #[must_use]
293    pub const fn default_level(&self) -> LintLevel {
294        self.default_level
295    }
296
297    #[must_use]
298    pub const fn rationale(&self) -> &'static str {
299        self.rationale
300    }
301
302    #[must_use]
303    pub const fn sound_alternative(&self) -> &'static str {
304        self.sound_alternative
305    }
306
307    #[must_use]
308    pub const fn silence_flag(&self) -> &'static str {
309        self.silence_flag
310    }
311
312    #[must_use]
313    pub const fn examples(&self) -> RuleExamples {
314        self.examples
315    }
316}
317
318macro_rules! source_example {
319    ($kind:ident, $text:literal) => {
320        RuleExampleCase::Source(RuleExampleSource::new(ScriptKind::$kind, $text))
321    };
322}
323
324macro_rules! examples {
325    ($trigger:literal, $clean:literal) => {
326        RuleExamples::new(
327            source_example!(TypeScript, $trigger),
328            source_example!(TypeScript, $clean),
329        )
330    };
331    ($kind:ident, $trigger:literal, $clean:literal) => {
332        RuleExamples::new(
333            source_example!($kind, $trigger),
334            source_example!($kind, $clean),
335        )
336    };
337}
338
339macro_rules! rule {
340    ($code:literal, $slug:literal, $group:ident, $level:ident, $rationale:literal, $alternative:literal, $examples:expr) => {
341        RuleDefinition::new(
342            RuleId {
343                code: $code,
344                slug: $slug,
345            },
346            RuleGroup::$group,
347            LintLevel::$level,
348            $rationale,
349            $alternative,
350            concat!("-A ", $slug),
351            $examples,
352        )
353    };
354}
355
356/// The single authoritative identity and default-level registry for all BamTS rules.
357pub static RULES: [RuleDefinition; 86] = [
358    rule!(
359        "BAMTS-W001",
360        "method-parameter-bivariance",
361        Unsoundness,
362        Warn,
363        "Method parameters are bivariant, so a narrower handler can receive an incompatible value.",
364        "Use a function-property callback with a contravariant parameter.",
365        examples!(
366            "interface H { handle(x: Dog): void }",
367            "const safe: number = 1;"
368        )
369    ),
370    rule!(
371        "BAMTS-W002",
372        "mutable-array-covariance",
373        Unsoundness,
374        Warn,
375        "Mutable arrays are covariant, so a widened alias can write the wrong element type.",
376        "Expose readonly arrays across type boundaries.",
377        examples!(
378            "const dogs: Dog[] = []; const animals: Animal[] = dogs;",
379            "const dogs: Animal[] = []; const animals: Animal[] = dogs;"
380        )
381    ),
382    rule!(
383        "BAMTS-W003",
384        "non-fresh-excess-property",
385        Unsoundness,
386        Warn,
387        "A non-fresh object can bypass excess-property checks and hide misspelled fields.",
388        "Validate the object at its construction boundary.",
389        examples!(
390            "const candidate = { keep: 1, extra: true }; const target: { keep: number } = candidate;",
391            "const target: { keep: number } = { keep: 1, extra: true };"
392        )
393    ),
394    rule!(
395        "BAMTS-W004",
396        "delete-required-property",
397        Unsoundness,
398        Warn,
399        "Deleting a required property breaks the declared object shape.",
400        "Model removability with an optional property or a separate value.",
401        examples!(
402            "const item: { required: number } = { required: 1 }; delete item.required;",
403            "const item: { optional?: number } = {}; delete item.optional;"
404        )
405    ),
406    rule!(
407        "BAMTS-W005",
408        "unchecked-catch-member",
409        Unsoundness,
410        Warn,
411        "A catch binding is untrusted until it is narrowed before member access.",
412        "Narrow the caught value with a runtime guard.",
413        examples!(
414            "try {} catch (error) { error.message; }",
415            "try {} catch (error) { if (error instanceof Error) error.message; }"
416        )
417    ),
418    rule!(
419        "BAMTS-W006",
420        "generic-any-downcast",
421        EscapeHatches,
422        Warn,
423        "Casting any through a generic return loses the proof required by every caller.",
424        "Validate the input and return a concrete checked type.",
425        examples!(
426            "function f<T>(x:any):T{return x as T}",
427            "const safe: number = 1;"
428        )
429    ),
430    rule!(
431        "BAMTS-W007",
432        "dynamic-tuple-index",
433        Unsoundness,
434        Warn,
435        "A dynamic tuple index can read beyond the tuple's known bounds.",
436        "Use a literal index or prove the index is in range.",
437        examples!(
438            "const pair: [string, number] = [\"a\", 1]; pair[index];",
439            "const pair: [string, number] = [\"a\", 1]; pair[1];"
440        )
441    ),
442    rule!(
443        "BAMTS-W008",
444        "unchecked-index-signature-read",
445        Unsoundness,
446        Warn,
447        "An index-signature read can be absent even when its value type excludes undefined.",
448        "Handle undefined after the lookup.",
449        examples!(
450            "interface D {[key: string]: number} declare const d:D; declare const k:string; const n=d[k];",
451            "const colors: Record<'red', number>={red:1}; const n=colors['red'];"
452        )
453    ),
454    rule!(
455        "BAMTS-W009",
456        "explicit-undefined-for-optional",
457        Unsoundness,
458        Warn,
459        "An optional property without undefined distinguishes absence from an explicit undefined value.",
460        "Omit the property or include undefined in its declared type.",
461        examples!(
462            "const o: {x?: number} = {x: undefined};",
463            "const safe: number = 1;"
464        )
465    ),
466    rule!(
467        "BAMTS-W010",
468        "detached-this-method",
469        Unsoundness,
470        Warn,
471        "Extracting a receiver-dependent method loses the this binding it requires.",
472        "Bind the method or call it through its receiver.",
473        examples!("const f = obj.method; f();", "const safe: number = 1;")
474    ),
475    rule!(
476        "BAMTS-W011",
477        "divergent-accessor-types",
478        Unsoundness,
479        Warn,
480        "Different getter and setter types hide an unsafe property boundary.",
481        "Use one compatible property type or an explicit conversion method.",
482        examples!(
483            "class C { get x(): number { return 1 } set x(v: string | number) {} }",
484            "const safe: number = 1;"
485        )
486    ),
487    rule!(
488        "BAMTS-W012",
489        "readonly-alias-mutation",
490        Unsoundness,
491        Warn,
492        "A writable alias can mutate data promised as readonly elsewhere.",
493        "Keep the mutable value private and expose a readonly view.",
494        examples!(
495            "const r: {readonly x:number}=m; m.x=2;",
496            "const safe: number = 1;"
497        )
498    ),
499    rule!(
500        "BAMTS-W013",
501        "fewer-callback-parameters",
502        Unsoundness,
503        Warn,
504        "A callback that accepts fewer parameters can silently discard required protocol data.",
505        "Declare the callback parameters you intentionally receive.",
506        examples!(
507            "const f: (x:number,y:string)=>void = () => {};",
508            "const safe: number = 1;"
509        )
510    ),
511    rule!(
512        "BAMTS-W014",
513        "value-returning-void-callback",
514        Unsoundness,
515        Warn,
516        "A value returned from a void callback is silently discarded.",
517        "Use a block body when the return value is intentionally ignored.",
518        examples!("const f: () => void = () => 42;", "const safe: number = 1;")
519    ),
520    rule!(
521        "BAMTS-W015",
522        "open-object-keys-assumption",
523        Unsoundness,
524        Warn,
525        "Object.keys does not prove that runtime keys are limited to keyof T.",
526        "Validate keys at runtime or work from a closed key list.",
527        examples!(
528            "const ks = Object.keys(x) as (keyof typeof x)[];",
529            "const safe: number = 1;"
530        )
531    ),
532    rule!(
533        "BAMTS-W016",
534        "index-signature-dot-access",
535        Unsoundness,
536        Warn,
537        "Dot access through an index signature hides that a property may be absent.",
538        "Use bracket access and handle the missing value.",
539        examples!(
540            "interface D {[key:string]: number} declare const d:D; d.username;",
541            "interface D {[key:string]: number} declare const d:D; d['username'];"
542        )
543    ),
544    rule!(
545        "BAMTS-W017",
546        "explicit-any",
547        EscapeHatches,
548        Warn,
549        "Explicit any disables type checking at the annotated boundary.",
550        "Use unknown and narrow it before use.",
551        examples!("let value: any;", "const safe: number = 1;")
552    ),
553    rule!(
554        "BAMTS-W018",
555        "implicit-any",
556        EscapeHatches,
557        Warn,
558        "An inferred any lets an untyped value flow without an explicit boundary.",
559        "Add an explicit checked type or unknown annotation.",
560        examples!("function f(x) { return x; }", "const safe: number = 1;")
561    ),
562    rule!(
563        "BAMTS-W019",
564        "unchecked-type-assertion",
565        EscapeHatches,
566        Warn,
567        "A type assertion claims a narrower type without runtime proof.",
568        "Narrow with a guard or validate with a decoder.",
569        examples!("const n = value as number;", "const safe: number = 1;")
570    ),
571    rule!(
572        "BAMTS-W020",
573        "double-assertion",
574        EscapeHatches,
575        Warn,
576        "A double assertion bypasses assignability through any or unknown.",
577        "Convert or validate the value at the boundary.",
578        examples!(
579            "const n = value as unknown as number;",
580            "const safe: number = 1;"
581        )
582    ),
583    rule!(
584        "BAMTS-W021",
585        "non-null-assertion",
586        EscapeHatches,
587        Warn,
588        "A non-null assertion erases a possible null or undefined value.",
589        "Narrow the value before accessing it.",
590        examples!("node!.textContent;", "const safe: number = 1;")
591    ),
592    rule!(
593        "BAMTS-W022",
594        "definite-assignment-assertion",
595        EscapeHatches,
596        Warn,
597        "A definite-assignment assertion skips proof that a field is initialized.",
598        "Initialize the field or assign it in every constructor path.",
599        examples!("class C { value!: string }", "const safe: number = 1;")
600    ),
601    rule!(
602        "BAMTS-W023",
603        "diagnostic-suppression-directive",
604        EscapeHatches,
605        Warn,
606        "A TypeScript diagnostic directive hides a compiler check instead of resolving it.",
607        "Fix the diagnostic or make the boundary explicit.",
608        examples!("// @ts-ignore", "const safe: number = 1;")
609    ),
610    rule!(
611        "BAMTS-W024",
612        "runtime-namespace",
613        NonErasable,
614        Warn,
615        "A value-bearing namespace requires runtime code instead of erasing as type syntax.",
616        "Use ES modules or an ambient namespace.",
617        examples!(
618            "namespace N { export const x = 1 }",
619            "const safe: number = 1;"
620        )
621    ),
622    rule!(
623        "BAMTS-W025",
624        "parameter-property",
625        NonErasable,
626        Warn,
627        "A parameter property synthesizes a field assignment during compilation.",
628        "Declare the field and assign the constructor parameter explicitly.",
629        examples!(
630            "class C { constructor(public x: number) {} }",
631            "const safe: number = 1;"
632        )
633    ),
634    rule!(
635        "BAMTS-W026",
636        "legacy-decorator-semantics",
637        LegacySyntax,
638        Warn,
639        "Legacy decorators have semantics that differ from standard ECMAScript decorators.",
640        "Use standard decorators or an explicit wrapper.",
641        examples!("@sealed class C {}", "const safe: number = 1;")
642    ),
643    rule!(
644        "BAMTS-W027",
645        "angle-bracket-assertion",
646        LegacySyntax,
647        Warn,
648        "Angle-bracket assertions are ambiguous with JSX syntax.",
649        "Use the `as T` assertion spelling.",
650        examples!("const n = <number>value;", "const safe: number = 1;")
651    ),
652    rule!(
653        "BAMTS-W028",
654        "declaration-inference-dependency",
655        LegacySyntax,
656        Warn,
657        "Declaration output that depends on cross-file inference is fragile and non-local.",
658        "Write an explicit exported type annotation.",
659        RuleExamples::new(
660            RuleExampleCase::Program(&[
661                RuleExampleSource::new(
662                    ScriptKind::TypeScript,
663                    "import { make } from './dep.js'; export const value = make();"
664                )
665                .resolving_to(1),
666                RuleExampleSource::new(
667                    ScriptKind::TypeScript,
668                    "export const make = (): number => 1;"
669                )
670            ]),
671            source_example!(TypeScript, "export const value: number = 1;")
672        )
673    ),
674    rule!(
675        "BAMTS-W029",
676        "jsx-transform-required",
677        LegacySyntax,
678        Warn,
679        "JSX requires a configured runtime transform and cannot simply be erased.",
680        "Configure a JSX runtime or use ordinary function calls.",
681        examples!(
682            TypeScriptReact,
683            "const el = <Widget value={1} />;",
684            "const safe = 1;"
685        )
686    ),
687    rule!(
688        "BAMTS-W030",
689        "import-export-equals",
690        Modules,
691        Warn,
692        "TypeScript import-equals and export-equals require target-specific module rewriting.",
693        "Use standard ESM import and export syntax.",
694        examples!("import fs = require(\"fs\");", "const safe: number = 1;")
695    ),
696    rule!(
697        "BAMTS-W031",
698        "type-imported-as-value",
699        Modules,
700        Warn,
701        "A type-only import emitted as a value import creates a runtime dependency.",
702        "Use `import type` for type-only symbols.",
703        RuleExamples::new(
704            RuleExampleCase::Program(&[
705                RuleExampleSource::new(
706                    ScriptKind::TypeScript,
707                    "import { User } from './types.js'; const user: User = { name: 'Ada' };"
708                )
709                .resolving_to(1),
710                RuleExampleSource::new(
711                    ScriptKind::TypeScript,
712                    "export interface User { name: string }"
713                )
714            ]),
715            RuleExampleCase::Program(&[
716                RuleExampleSource::new(
717                    ScriptKind::TypeScript,
718                    "import type { User } from './types.js'; const user: User = { name: 'Ada' };"
719                )
720                .resolving_to(1),
721                RuleExampleSource::new(
722                    ScriptKind::TypeScript,
723                    "export interface User { name: string }"
724                )
725            ])
726        )
727    ),
728    rule!(
729        "BAMTS-W032",
730        "type-reexported-as-value",
731        Modules,
732        Warn,
733        "A type-only re-export emitted as a value re-export creates a runtime dependency.",
734        "Use `export type` for type-only symbols.",
735        RuleExamples::new(
736            RuleExampleCase::Program(&[
737                RuleExampleSource::new(
738                    ScriptKind::TypeScript,
739                    "export { User } from './types.js';"
740                )
741                .resolving_to(1),
742                RuleExampleSource::new(
743                    ScriptKind::TypeScript,
744                    "export interface User { name: string }"
745                )
746            ]),
747            RuleExampleCase::Program(&[
748                RuleExampleSource::new(
749                    ScriptKind::TypeScript,
750                    "export type { User } from './types.js';"
751                )
752                .resolving_to(1),
753                RuleExampleSource::new(
754                    ScriptKind::TypeScript,
755                    "export interface User { name: string }"
756                )
757            ])
758        )
759    ),
760    rule!(
761        "BAMTS-W033",
762        "commonjs-in-esm",
763        Modules,
764        Allow,
765        "CommonJS globals inside an ESM module depend on host-specific interop.",
766        "Use ESM exports or isolate the CommonJS bridge.",
767        examples!(
768            "export const x = require('x');",
769            "const x = require('x'); x;"
770        )
771    ),
772    rule!(
773        "BAMTS-W034",
774        "implicit-script-file",
775        Modules,
776        Allow,
777        "A file without imports or exports silently becomes a global script.",
778        "Add an explicit export or force module detection.",
779        examples!("const shared = 1;", "export {}; const shared = 1;")
780    ),
781    rule!(
782        "BAMTS-W035",
783        "unchecked-side-effect-import",
784        Modules,
785        Warn,
786        "An unresolved side-effect import can conceal a missing runtime dependency.",
787        "Resolve the module or declare the host-provided virtual module.",
788        RuleExamples::new(
789            RuleExampleCase::Program(&[RuleExampleSource::new(
790                ScriptKind::TypeScript,
791                "import './missing.js';"
792            )]),
793            RuleExampleCase::Program(&[
794                RuleExampleSource::new(ScriptKind::TypeScript, "import './polyfill.js';")
795                    .resolving_to(1),
796                RuleExampleSource::new(ScriptKind::JavaScript, "globalThis.ready = true;")
797            ])
798        )
799    ),
800    rule!(
801        "BAMTS-W036",
802        "extensionless-relative-import",
803        Modules,
804        Warn,
805        "Relative ESM imports need a runtime file extension in Node-style resolution.",
806        "Write the explicit runtime extension.",
807        examples!("import {x} from \"./util\";", "const safe: number = 1;")
808    ),
809    rule!(
810        "BAMTS-W037",
811        "interop-dependent-default-import",
812        Modules,
813        Warn,
814        "A default import from CommonJS can rely on synthetic interop semantics.",
815        "Use a namespace import or a real ESM default export.",
816        RuleExamples::new(
817            RuleExampleCase::Program(&[
818                RuleExampleSource::new(
819                    ScriptKind::TypeScript,
820                    "import legacy from './legacy.js'; legacy();"
821                )
822                .resolving_to(1),
823                RuleExampleSource::new(
824                    ScriptKind::JavaScript,
825                    "module.exports = function legacy() {};"
826                )
827            ]),
828            RuleExampleCase::Program(&[
829                RuleExampleSource::new(
830                    ScriptKind::TypeScript,
831                    "import modern from './modern.js'; modern();"
832                )
833                .resolving_to(1),
834                RuleExampleSource::new(
835                    ScriptKind::JavaScript,
836                    "export default function modern() {}"
837                )
838            ])
839        )
840    ),
841    rule!(
842        "BAMTS-W038",
843        "virtual-call-in-constructor",
844        ClassSemantics,
845        Allow,
846        "A constructor dispatching to an overridable method can observe uninitialized derived state.",
847        "Defer the hook until construction is complete.",
848        examples!(
849            "class B { constructor(){ this.init() } }",
850            "const safe: number = 1;"
851        )
852    ),
853    rule!(
854        "BAMTS-W039",
855        "uninitialized-field-emit-split",
856        ClassSemantics,
857        Allow,
858        "An uninitialized field has different runtime presence under competing emit modes.",
859        "Initialize it or use `declare` when no own field is intended.",
860        examples!("class C { value: string; }", "const safe: number = 1;")
861    ),
862    rule!(
863        "BAMTS-W040",
864        "field-overrides-accessor",
865        ClassSemantics,
866        Allow,
867        "A defined field can shadow an inherited accessor instead of invoking it.",
868        "Use an accessor, `declare`, or a distinct field name.",
869        examples!(
870            "class B { get data():number{return 1} } class D extends B { data = 1; }",
871            "class B { get data():number{return 1} } class D extends B { declare data:number; }"
872        )
873    ),
874    rule!(
875        "BAMTS-W041",
876        "implicit-override",
877        ClassSemantics,
878        Allow,
879        "An unmarked override can silently drift when its base member changes.",
880        "Mark the member with `override`.",
881        examples!(
882            "class B { run(){} } class D extends B { run(){} }",
883            "class B { run(){} } class D extends B { override run(){} }"
884        )
885    ),
886    rule!(
887        "BAMTS-W042",
888        "typescript-private-field",
889        ClassSemantics,
890        Allow,
891        "A TypeScript private modifier erases and does not provide runtime privacy.",
892        "Use an ECMAScript `#private` field for runtime privacy.",
893        examples!("class C { private secret = 1 }", "const safe: number = 1;")
894    ),
895    rule!(
896        "BAMTS-W043",
897        "runtime-enum",
898        EnumSemantics,
899        Warn,
900        "A non-const enum creates a runtime object with non-erasable behavior.",
901        "Use a union or a const object when a runtime object is intentional.",
902        examples!("enum Color { Red, Blue }", "const safe: number = 1;")
903    ),
904    rule!(
905        "BAMTS-W044",
906        "const-enum",
907        EnumSemantics,
908        Warn,
909        "A const enum relies on compile-time inlining across compilation boundaries.",
910        "Use a union or a const object.",
911        examples!("const enum Code { Ok = 200 }", "const safe: number = 1;")
912    ),
913    rule!(
914        "BAMTS-W045",
915        "numeric-enum-number-flow",
916        EnumSemantics,
917        Warn,
918        "Numeric enums accept arbitrary numbers, weakening the enum boundary.",
919        "Use a string enum or validate the numeric value.",
920        examples!(
921            "enum E { A } let e:E=E.A; let n:number=e;",
922            "enum E { A } const e=E.A;"
923        )
924    ),
925    rule!(
926        "BAMTS-W046",
927        "heterogeneous-enum",
928        EnumSemantics,
929        Warn,
930        "A heterogeneous enum mixes unrelated representations and complicates consumers.",
931        "Use one representation or a discriminated union.",
932        examples!(
933            "enum Answer { No = 0, Yes = \"YES\" }",
934            "const safe: number = 1;"
935        )
936    ),
937    rule!(
938        "BAMTS-W047",
939        "computed-enum-member",
940        EnumSemantics,
941        Warn,
942        "A computed enum member depends on runtime evaluation rather than a stable constant.",
943        "Use a constant initializer or a separate runtime value.",
944        examples!("enum E { X = getValue() }", "const safe: number = 1;")
945    ),
946    rule!(
947        "BAMTS-W048",
948        "numeric-enum-reverse-lookup",
949        EnumSemantics,
950        Warn,
951        "Numeric enum reverse lookup depends on generated runtime mappings.",
952        "Store the display name explicitly.",
953        examples!(
954            "enum E { A } const name=E[E.A];",
955            "enum E { A } const value=E.A;"
956        )
957    ),
958    rule!(
959        "BAMTS-W049",
960        "interface-declaration-merge",
961        DeclarationMerging,
962        Warn,
963        "Same-scope interfaces merge implicitly, making a type's shape non-local.",
964        "Declare one complete interface or use a closed type alias.",
965        examples!(
966            "interface Box {x:number} interface Box {y:number}",
967            "const safe: number = 1;"
968        )
969    ),
970    rule!(
971        "BAMTS-W050",
972        "namespace-value-merge",
973        DeclarationMerging,
974        Warn,
975        "A namespace merged with a value creates an implicit hybrid declaration.",
976        "Use an explicit object or separate module export.",
977        examples!(
978            "function f(){} namespace f { export const x=1 }",
979            "const safe: number = 1;"
980        )
981    ),
982    rule!(
983        "BAMTS-W051",
984        "global-augmentation",
985        DeclarationMerging,
986        Warn,
987        "A global augmentation mutates ambient types for unrelated code.",
988        "Expose a local wrapper or explicit global installation boundary.",
989        examples!(
990            "declare global { interface Window { x: number } }",
991            "const safe: number = 1;"
992        )
993    ),
994    rule!(
995        "BAMTS-W052",
996        "module-augmentation",
997        DeclarationMerging,
998        Warn,
999        "A module augmentation changes another module's contract outside that module.",
1000        "Wrap or extend the module through an explicit local API.",
1001        examples!(
1002            "declare module \"lib\" { interface X { y: number } }",
1003            "const safe: number = 1;"
1004        )
1005    ),
1006    rule!(
1007        "BAMTS-W053",
1008        "ambient-value-declaration",
1009        DeclarationMerging,
1010        Warn,
1011        "An ambient value declaration cannot prove that the runtime provides the value.",
1012        "Pass the value explicitly or install it through a checked host API.",
1013        examples!("declare const injected: string;", "const safe: number = 1;")
1014    ),
1015    rule!(
1016        "BAMTS-W054",
1017        "javascript-input",
1018        JavaScriptCompatibility,
1019        Allow,
1020        "JavaScript source enters a typed program with weaker static guarantees.",
1021        "Convert the source to TypeScript or isolate it behind typed declarations.",
1022        RuleExamples::new(
1023            source_example!(JavaScript, "const legacy = 1;"),
1024            source_example!(TypeScript, "const safe: number = 1;")
1025        )
1026    ),
1027    rule!(
1028        "BAMTS-W055",
1029        "jsdoc-type-syntax",
1030        JavaScriptCompatibility,
1031        Allow,
1032        "JSDoc types make JavaScript comments carry part of the type system.",
1033        "Move the file to TypeScript with native type syntax.",
1034        examples!(
1035            JavaScript,
1036            "/** @type {number} */ let n = 1;",
1037            "const safe = 1;"
1038        )
1039    ),
1040    rule!(
1041        "BAMTS-W056",
1042        "prototype-class-pattern",
1043        JavaScriptCompatibility,
1044        Allow,
1045        "Prototype assignment spreads class behavior across mutable runtime objects.",
1046        "Use class syntax or an explicit factory object.",
1047        examples!(
1048            JavaScript,
1049            "Ctor.prototype.run = function() {};",
1050            "const safe = 1;"
1051        )
1052    ),
1053    rule!(
1054        "BAMTS-W057",
1055        "ts-check-directive",
1056        JavaScriptCompatibility,
1057        Allow,
1058        "A per-file ts-check directive makes type-checking policy non-uniform.",
1059        "Use project-wide checkJs or convert the file to TypeScript.",
1060        examples!(JavaScript, "// @ts-check", "const safe = 1;")
1061    ),
1062    rule!(
1063        "BAMTS-W058",
1064        "prefer-type-alias",
1065        Opinionated,
1066        Allow,
1067        "An interface can merge later, leaving an API shape open unintentionally.",
1068        "Use a type alias for a closed shape.",
1069        examples!("interface Point { x: number }", "const safe: number = 1;")
1070    ),
1071    rule!(
1072        "BAMTS-W059",
1073        "prefer-readonly-array",
1074        Opinionated,
1075        Allow,
1076        "A mutable array type advertises mutation where a read-only view may suffice.",
1077        "Accept `readonly T[]` unless mutation is required.",
1078        examples!("function f(xs: string[]) {}", "const safe: number = 1;")
1079    ),
1080    rule!(
1081        "BAMTS-W060",
1082        "prefer-function-property",
1083        Opinionated,
1084        Allow,
1085        "A method signature keeps bivariant parameter checking.",
1086        "Use a function-property signature for callback members.",
1087        examples!(
1088            "interface H { run(x: Animal): void }",
1089            "const safe: number = 1;"
1090        )
1091    ),
1092    rule!(
1093        "BAMTS-W061",
1094        "no-barrel-star-export",
1095        Opinionated,
1096        Allow,
1097        "A wildcard barrel export obscures the package's public dependency surface.",
1098        "Re-export the intended names explicitly.",
1099        examples!(
1100            "export * from \"./internal.js\";",
1101            "const safe: number = 1;"
1102        )
1103    ),
1104    rule!(
1105        "BAMTS-W062",
1106        "no-default-export",
1107        Opinionated,
1108        Allow,
1109        "A default export lets importers rename one public binding arbitrarily.",
1110        "Use a named export.",
1111        examples!(
1112            "export default function run() {}",
1113            "const safe: number = 1;"
1114        )
1115    ),
1116    rule!(
1117        "BAMTS-W063",
1118        "exhaustive-discriminated-switch",
1119        Opinionated,
1120        Allow,
1121        "A discriminated-union switch omits a reachable variant.",
1122        "Handle every variant and assert never in the default branch.",
1123        examples!(
1124            "type S = { kind: \"a\" } | { kind: \"b\" }; function f(s: S) { switch (s.kind) { case \"a\": break; } }",
1125            "const safe: number = 1;"
1126        )
1127    ),
1128    rule!(
1129        "BAMTS-W064",
1130        "long-parameter-list",
1131        Opinionated,
1132        Allow,
1133        "A long positional parameter list makes calls easy to misorder.",
1134        "Use a parameter object or smaller cohesive functions.",
1135        examples!(
1136            "function f(a:number,b:number,c:number,d:number,e:number) {}",
1137            "const safe: number = 1;"
1138        )
1139    ),
1140    rule!(
1141        "BAMTS-W065",
1142        "implicit-return-path",
1143        ControlFlow,
1144        Warn,
1145        "A function can complete without returning the value its signature implies.",
1146        "Return on every reachable path or include undefined in the return type.",
1147        examples!(
1148            "function f(x:boolean){ if(x)return 1 }",
1149            "function f(x:boolean){ if(x)return 0; try { return 1; } catch { return 2; } }"
1150        )
1151    ),
1152    rule!(
1153        "BAMTS-W066",
1154        "switch-fallthrough",
1155        ControlFlow,
1156        Warn,
1157        "A non-empty switch case falls through without an explicit transfer.",
1158        "Add break, return, throw, or an explicit fallthrough marker.",
1159        examples!(
1160            "switch(x){case 1: work(); case 2: stop();}",
1161            "const safe: number = 1;"
1162        )
1163    ),
1164    rule!(
1165        "BAMTS-W067",
1166        "unreachable-code",
1167        ControlFlow,
1168        Warn,
1169        "A statement is unreachable under the program's control flow.",
1170        "Remove it or restructure the surrounding control flow.",
1171        examples!("function f(){ return; work(); }", "const safe: number = 1;")
1172    ),
1173    rule!(
1174        "BAMTS-W068",
1175        "unused-label",
1176        ControlFlow,
1177        Warn,
1178        "A label is declared but never targeted, obscuring control flow.",
1179        "Remove the label or add its intended labeled transfer.",
1180        examples!(
1181            "unused: for (;;) { break; }",
1182            "outer: for (;;) { break outer; }"
1183        )
1184    ),
1185    rule!(
1186        "BAMTS-W069",
1187        "unused-local",
1188        ControlFlow,
1189        Warn,
1190        "A local binding is never read after declaration.",
1191        "Remove it or use it deliberately.",
1192        examples!(
1193            "function f(){ const x=1; }",
1194            "function f(){ const x=1; return x; }"
1195        )
1196    ),
1197    rule!(
1198        "BAMTS-W070",
1199        "unused-parameter",
1200        ControlFlow,
1201        Warn,
1202        "A declared parameter is never read by its function.",
1203        "Remove it or name an intentionally unused protocol parameter clearly.",
1204        examples!(
1205            "function f(unused: number) {}",
1206            "function f(used: number) { return used; }"
1207        )
1208    ),
1209    rule!(
1210        "BAMTS-W071",
1211        "invalid-number-formatting-options",
1212        Unsoundness,
1213        Warn,
1214        "Known number-formatting arguments lie outside the ECMAScript-supported range.",
1215        "Validate or clamp the argument before calling the method.",
1216        examples!("(42).toString(1);", "const safe: number = 1;")
1217    ),
1218    rule!(
1219        "BAMTS-W072",
1220        "unsound-numeric-key-order-assumption",
1221        Unsoundness,
1222        Warn,
1223        "Integer-like object keys are ordered before other keys, not purely by insertion.",
1224        "Avoid insertion-order dependence or sort the keys explicitly.",
1225        examples!("Object.keys({b: 1, \"2\": 2});", "const safe: number = 1;")
1226    ),
1227    rule!(
1228        "BAMTS-W073",
1229        "json-stringify-unserializable-type",
1230        Unsoundness,
1231        Warn,
1232        "JSON.stringify can throw for BigInt or return undefined for a top-level value.",
1233        "Validate serializability and handle the undefined result.",
1234        examples!("JSON.stringify(10n);", "const safe: number = 1;")
1235    ),
1236    rule!(
1237        "BAMTS-W074",
1238        "unchecked-json-parse-any",
1239        Unsoundness,
1240        Warn,
1241        "JSON.parse returns untrusted data that is consumed as a trusted type.",
1242        "Parse to unknown and validate with a decoder.",
1243        examples!(
1244            "const u: User = JSON.parse(text);",
1245            "const safe: number = 1;"
1246        )
1247    ),
1248    rule!(
1249        "BAMTS-W075",
1250        "numeric-array-default-sort",
1251        Unsoundness,
1252        Warn,
1253        "Comparator-free sort coerces elements to strings rather than numeric order.",
1254        "Pass an explicit numeric or domain comparator.",
1255        examples!("[10, 2, 5].sort();", "const safe: number = 1;")
1256    ),
1257    rule!(
1258        "BAMTS-W076",
1259        "loose-equality-coercion",
1260        Unsoundness,
1261        Warn,
1262        "Loose equality can depend on implicit abstract coercion.",
1263        "Use strict equality or an explicit conversion.",
1264        examples!("\"0\" == false;", "const safe: number = 1;")
1265    ),
1266    rule!(
1267        "BAMTS-W077",
1268        "object-implicit-toprimitive-coercion",
1269        Unsoundness,
1270        Warn,
1271        "Implicit object-to-primitive conversion can call surprising coercion hooks.",
1272        "Call String, Number, or an explicit conversion method.",
1273        examples!("\"key_\" + Object.create(null);", "const safe: number = 1;")
1274    ),
1275    rule!(
1276        "BAMTS-W078",
1277        "symbol-template-interpolation-throw",
1278        Unsoundness,
1279        Warn,
1280        "Interpolating a symbol directly into a template literal throws.",
1281        "Wrap it with String or use its description.",
1282        examples!("`ID: ${Symbol(\"x\")}`", "const safe: number = 1;")
1283    ),
1284    rule!(
1285        "BAMTS-W079",
1286        "nan-strict-comparison",
1287        Unsoundness,
1288        Warn,
1289        "NaN is never strictly equal to itself, so a direct comparison is ineffective.",
1290        "Use Number.isNaN.",
1291        examples!("if (value === NaN) {}", "const safe: number = 1;")
1292    ),
1293    rule!(
1294        "BAMTS-W080",
1295        "unsafe-tostringtag-override",
1296        Unsoundness,
1297        Warn,
1298        "A toStringTag override is not a trustworthy runtime brand.",
1299        "Use a string tag and validate the actual value shape.",
1300        examples!(
1301            "({ [Symbol.toStringTag]: 123 });",
1302            "const safe: number = 1;"
1303        )
1304    ),
1305    rule!(
1306        "BAMTS-W081",
1307        "uninitialized-class-field-shadowing",
1308        ClassSemantics,
1309        Allow,
1310        "An uninitialized derived field defines an own property that shadows an inherited accessor.",
1311        "Use `declare`, initialize deliberately, or rename the field.",
1312        examples!(
1313            "class B { get data():number{return 1} } class D extends B { data:number; }",
1314            "class B { get data():number{return 1} } class D extends B { declare data:number; }"
1315        )
1316    ),
1317    rule!(
1318        "BAMTS-W082",
1319        "preserve-const-enums-option",
1320        NonErasable,
1321        Warn,
1322        "Preserving const enums retains runtime enum objects while inlining their uses.",
1323        "Disable preserveConstEnums or replace the enum.",
1324        RuleExamples::new(
1325            RuleExampleCase::CompilerOptions(CompilerLintOptions {
1326                preserve_const_enums: true,
1327                ..CompilerLintOptions::STANDARD
1328            }),
1329            RuleExampleCase::CompilerOptions(CompilerLintOptions::STANDARD)
1330        )
1331    ),
1332    rule!(
1333        "BAMTS-W083",
1334        "emit-decorator-metadata-option",
1335        LegacySyntax,
1336        Warn,
1337        "Emitted decorator metadata couples runtime reflection to compiler type information.",
1338        "Disable metadata emit and provide explicit metadata.",
1339        RuleExamples::new(
1340            RuleExampleCase::CompilerOptions(CompilerLintOptions {
1341                emit_decorator_metadata: true,
1342                ..CompilerLintOptions::STANDARD
1343            }),
1344            RuleExampleCase::CompilerOptions(CompilerLintOptions::STANDARD)
1345        )
1346    ),
1347    rule!(
1348        "BAMTS-W084",
1349        "legacy-class-field-set-semantics",
1350        ClassSemantics,
1351        Allow,
1352        "Legacy class-field set semantics invoke inherited setters instead of defining fields.",
1353        "Enable standard define semantics.",
1354        RuleExamples::new(
1355            RuleExampleCase::CompilerOptions(CompilerLintOptions {
1356                use_define_for_class_fields: false,
1357                ..CompilerLintOptions::STANDARD
1358            }),
1359            RuleExampleCase::CompilerOptions(CompilerLintOptions::STANDARD)
1360        )
1361    ),
1362    rule!(
1363        "BAMTS-W085",
1364        "javascript-syntax-rejection",
1365        JavaScriptCompatibility,
1366        Deny,
1367        "TypeScript-only syntax in a JavaScript file violates that file's source dialect.",
1368        "Rename the file to TypeScript or remove the type syntax.",
1369        examples!(
1370            JavaScript,
1371            "interface Point { x: number }",
1372            "const safe = 1;"
1373        )
1374    ),
1375    rule!(
1376        "BAMTS-W086",
1377        "cjs-esm-named-export-mismatch",
1378        Modules,
1379        Warn,
1380        "An ESM named import from CommonJS may not exist in its statically detected exports.",
1381        "Use the CommonJS default export or a declared named export.",
1382        RuleExamples::new(
1383            RuleExampleCase::Program(&[
1384                RuleExampleSource::new(
1385                    ScriptKind::TypeScript,
1386                    "import { helper } from './legacy.js'; helper();"
1387                )
1388                .resolving_to(1),
1389                RuleExampleSource::new(ScriptKind::JavaScript, "exports.other = () => 1;")
1390            ]),
1391            RuleExampleCase::Program(&[
1392                RuleExampleSource::new(
1393                    ScriptKind::TypeScript,
1394                    "import { helper } from './legacy.js'; helper();"
1395                )
1396                .resolving_to(1),
1397                RuleExampleSource::new(
1398                    ScriptKind::JavaScript,
1399                    "function helper() {} module.exports = { helper };"
1400                )
1401            ])
1402        )
1403    ),
1404];
1405
1406/// Render the checked-in strictness reference directly from [`RULES`].
1407#[must_use]
1408pub fn rule_reference() -> String {
1409    let mut reference = String::from(
1410        "# BamTS strictness rules\n\nThis file is generated from `bamts_compiler::lint::RULES`; do not edit it manually.\n",
1411    );
1412
1413    for rule in RULES {
1414        writeln!(
1415            reference,
1416            "\n## `{}`: `{}`\n\n- Group: `{}`\n- Default level: `{}`\n- Rationale: {}\n- Sound alternative: {}\n- Silence: `{}`\n- Trigger: {}\n- Clean: {}",
1417            rule.code(),
1418            rule.slug(),
1419            rule.group().slug(),
1420            rule.default_level(),
1421            rule.rationale(),
1422            rule.sound_alternative(),
1423            rule.silence_flag(),
1424            render_example(rule.examples().trigger()),
1425            render_example(rule.examples().clean()),
1426        )
1427        .expect("writing to a String cannot fail");
1428    }
1429
1430    reference
1431}
1432
1433fn render_example(example: RuleExampleCase) -> String {
1434    match example {
1435        RuleExampleCase::Source(source) => render_example_source(source),
1436        RuleExampleCase::Program(sources) => sources
1437            .iter()
1438            .map(|source| render_example_source(*source))
1439            .collect::<Vec<_>>()
1440            .join("<br>"),
1441        RuleExampleCase::CompilerOptions(options) => format!(
1442            "<code>preserveConstEnums={}, emitDecoratorMetadata={}, useDefineForClassFields={}</code>",
1443            options.preserve_const_enums,
1444            options.emit_decorator_metadata,
1445            options.use_define_for_class_fields,
1446        ),
1447    }
1448}
1449
1450fn render_example_source(source: RuleExampleSource) -> String {
1451    let escaped = source
1452        .text()
1453        .replace('&', "&amp;")
1454        .replace('<', "&lt;")
1455        .replace('>', "&gt;");
1456    format!("<code>{:?}: {escaped}</code>", source.script_kind())
1457}
1458
1459#[must_use]
1460pub fn rule_by_code(code: &str) -> Option<&'static RuleDefinition> {
1461    RULES.iter().find(|rule| rule.code() == code)
1462}
1463
1464#[must_use]
1465pub fn rule_by_slug(slug: &str) -> Option<&'static RuleDefinition> {
1466    RULES.iter().find(|rule| rule.slug() == slug)
1467}
1468
1469#[must_use]
1470pub fn rule_by_name(name: &str) -> Option<&'static RuleDefinition> {
1471    rule_by_code(name)
1472        .or_else(|| rule_by_slug(name))
1473        .or_else(|| alias_by_name(name).and_then(|alias| rule_by_code(alias.target_code)))
1474}
1475
1476#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1477pub struct RuleAlias {
1478    alias: &'static str,
1479    target_code: &'static str,
1480}
1481
1482impl RuleAlias {
1483    #[must_use]
1484    pub const fn alias(self) -> &'static str {
1485        self.alias
1486    }
1487
1488    #[must_use]
1489    pub const fn target_code(self) -> &'static str {
1490        self.target_code
1491    }
1492}
1493
1494/// Permanent migrations for published slugs. Entries are never removed.
1495pub static RULE_ALIASES: [RuleAlias; 4] = [
1496    RuleAlias {
1497        alias: "any-downcast",
1498        target_code: "BAMTS-W006",
1499    },
1500    RuleAlias {
1501        alias: "excess-property-bypass",
1502        target_code: "BAMTS-W003",
1503    },
1504    RuleAlias {
1505        alias: "unchecked-catch-property-access",
1506        target_code: "BAMTS-W005",
1507    },
1508    RuleAlias {
1509        alias: "dynamic-tuple-out-of-bounds-indexing",
1510        target_code: "BAMTS-W007",
1511    },
1512];
1513
1514fn alias_by_name(name: &str) -> Option<RuleAlias> {
1515    RULE_ALIASES
1516        .iter()
1517        .copied()
1518        .find(|alias| alias.alias == name)
1519}
1520
1521#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1522pub struct RuleTombstone {
1523    code: &'static str,
1524}
1525
1526impl RuleTombstone {
1527    #[must_use]
1528    pub const fn code(self) -> &'static str {
1529        self.code
1530    }
1531}
1532
1533/// Reserved and retired rule codes. These values can never enter [`RULES`].
1534pub static RULE_TOMBSTONES: [RuleTombstone; 1] = [RuleTombstone { code: "BAMTS-W000" }];
1535
1536fn is_tombstone(name: &str) -> bool {
1537    RULE_TOMBSTONES.iter().any(|entry| entry.code == name)
1538}
1539
1540#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
1541pub enum LintProfile {
1542    #[default]
1543    Default,
1544    Strict,
1545    Pedantic,
1546}
1547
1548impl LintProfile {
1549    #[must_use]
1550    pub const fn as_str(self) -> &'static str {
1551        match self {
1552            Self::Default => "default",
1553            Self::Strict => "strict",
1554            Self::Pedantic => "pedantic",
1555        }
1556    }
1557
1558    fn level(self, rule: &RuleDefinition) -> LintLevel {
1559        let strict = match rule.group() {
1560            RuleGroup::Unsoundness
1561            | RuleGroup::EscapeHatches
1562            | RuleGroup::NonErasable
1563            | RuleGroup::LegacySyntax => LintLevel::Deny,
1564            RuleGroup::ClassSemantics => LintLevel::Warn,
1565            RuleGroup::JavaScriptCompatibility if rule.code() != "BAMTS-W085" => LintLevel::Warn,
1566            RuleGroup::EnumSemantics if rule.code() != "BAMTS-W044" => LintLevel::Deny,
1567            _ => rule.default_level(),
1568        };
1569        match self {
1570            Self::Default => rule.default_level(),
1571            Self::Strict => strict,
1572            Self::Pedantic => match rule.group() {
1573                RuleGroup::EscapeHatches => LintLevel::Forbid,
1574                RuleGroup::Opinionated => LintLevel::Warn,
1575                RuleGroup::ClassSemantics | RuleGroup::JavaScriptCompatibility => LintLevel::Deny,
1576                _ => strict,
1577            },
1578        }
1579    }
1580
1581    const fn unknown_level(self) -> LintLevel {
1582        match self {
1583            Self::Default => LintLevel::Warn,
1584            Self::Strict | Self::Pedantic => LintLevel::Deny,
1585        }
1586    }
1587}
1588
1589impl FromStr for LintProfile {
1590    type Err = ParseLintProfileError;
1591
1592    fn from_str(value: &str) -> Result<Self, Self::Err> {
1593        match value {
1594            "default" => Ok(Self::Default),
1595            "strict" => Ok(Self::Strict),
1596            "pedantic" => Ok(Self::Pedantic),
1597            _ => Err(ParseLintProfileError(Arc::from(value))),
1598        }
1599    }
1600}
1601
1602#[derive(Clone, Debug, Eq, PartialEq)]
1603pub struct ParseLintProfileError(Arc<str>);
1604
1605impl fmt::Display for ParseLintProfileError {
1606    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1607        write!(formatter, "unknown lint profile {:?}", self.0)
1608    }
1609}
1610
1611impl std::error::Error for ParseLintProfileError {}
1612
1613#[derive(Clone, Debug, Eq, PartialEq)]
1614pub struct LintSetting {
1615    name: Arc<str>,
1616    level: LintLevel,
1617    source: Arc<str>,
1618}
1619
1620impl LintSetting {
1621    #[must_use]
1622    pub fn new(name: impl Into<Arc<str>>, level: LintLevel, source: impl Into<Arc<str>>) -> Self {
1623        Self {
1624            name: name.into(),
1625            level,
1626            source: source.into(),
1627        }
1628    }
1629
1630    #[must_use]
1631    pub fn name(&self) -> &str {
1632        &self.name
1633    }
1634
1635    #[must_use]
1636    pub const fn level(&self) -> LintLevel {
1637        self.level
1638    }
1639
1640    #[must_use]
1641    pub fn source(&self) -> &str {
1642        &self.source
1643    }
1644}
1645
1646/// Parsed project baseline. Section order is irrelevant; rules are more specific than groups.
1647#[derive(Clone, Debug, Default, Eq, PartialEq)]
1648pub struct LintConfig {
1649    groups: Vec<LintSetting>,
1650    rules: Vec<LintSetting>,
1651}
1652
1653impl LintConfig {
1654    #[must_use]
1655    pub const fn new(groups: Vec<LintSetting>, rules: Vec<LintSetting>) -> Self {
1656        Self { groups, rules }
1657    }
1658
1659    #[must_use]
1660    pub fn groups(&self) -> &[LintSetting] {
1661        &self.groups
1662    }
1663
1664    #[must_use]
1665    pub fn rules(&self) -> &[LintSetting] {
1666        &self.rules
1667    }
1668}
1669
1670#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1671pub enum OverrideTargetKind {
1672    Group,
1673    Rule,
1674    Either,
1675}
1676
1677#[derive(Clone, Debug, Eq, PartialEq)]
1678pub struct LintOverride {
1679    setting: LintSetting,
1680    target_kind: OverrideTargetKind,
1681}
1682
1683impl LintOverride {
1684    #[must_use]
1685    pub fn new(name: impl Into<Arc<str>>, level: LintLevel, source: impl Into<Arc<str>>) -> Self {
1686        Self {
1687            setting: LintSetting::new(name, level, source),
1688            target_kind: OverrideTargetKind::Either,
1689        }
1690    }
1691
1692    #[must_use]
1693    pub fn group(group: RuleGroup, level: LintLevel, source: impl Into<Arc<str>>) -> Self {
1694        Self {
1695            setting: LintSetting::new(group.slug(), level, source),
1696            target_kind: OverrideTargetKind::Group,
1697        }
1698    }
1699
1700    #[must_use]
1701    pub fn rule(rule: RuleId, level: LintLevel, source: impl Into<Arc<str>>) -> Self {
1702        Self {
1703            setting: LintSetting::new(rule.code(), level, source),
1704            target_kind: OverrideTargetKind::Rule,
1705        }
1706    }
1707}
1708
1709#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)]
1710enum Specificity {
1711    Profile,
1712    Group,
1713    Rule,
1714}
1715
1716#[derive(Clone, Debug, Eq, PartialEq)]
1717struct AppliedLevel {
1718    level: LintLevel,
1719    source: Arc<str>,
1720    specificity: Specificity,
1721}
1722
1723#[derive(Clone, Debug, Eq, PartialEq)]
1724struct RuleState {
1725    profile: AppliedLevel,
1726    group: Option<AppliedLevel>,
1727    rule: Option<AppliedLevel>,
1728}
1729
1730impl RuleState {
1731    fn effective(&self) -> &AppliedLevel {
1732        self.rule
1733            .as_ref()
1734            .or(self.group.as_ref())
1735            .unwrap_or(&self.profile)
1736    }
1737}
1738
1739#[derive(Clone, Debug, Eq, PartialEq)]
1740pub enum LintIssueKind {
1741    RenamedRule { canonical: &'static str },
1742    RetiredCode,
1743    UnknownName { suggestion: Option<Arc<str>> },
1744}
1745
1746#[derive(Clone, Debug, Eq, PartialEq)]
1747pub struct LintIssue {
1748    name: Arc<str>,
1749    level: LintLevel,
1750    source: Arc<str>,
1751    kind: LintIssueKind,
1752}
1753
1754impl LintIssue {
1755    #[must_use]
1756    pub fn name(&self) -> &str {
1757        &self.name
1758    }
1759
1760    #[must_use]
1761    pub const fn level(&self) -> LintLevel {
1762        self.level
1763    }
1764
1765    #[must_use]
1766    pub fn source(&self) -> &str {
1767        &self.source
1768    }
1769
1770    #[must_use]
1771    pub const fn kind(&self) -> &LintIssueKind {
1772        &self.kind
1773    }
1774}
1775
1776#[derive(Clone, Debug, Eq, PartialEq)]
1777pub struct ForbidOverrideError {
1778    rule: RuleId,
1779    forbidden_by: Arc<str>,
1780    lowered_by: Arc<str>,
1781}
1782
1783impl ForbidOverrideError {
1784    #[must_use]
1785    pub const fn rule(&self) -> RuleId {
1786        self.rule
1787    }
1788
1789    #[must_use]
1790    pub fn forbidden_by(&self) -> &str {
1791        &self.forbidden_by
1792    }
1793
1794    #[must_use]
1795    pub fn lowered_by(&self) -> &str {
1796        &self.lowered_by
1797    }
1798}
1799
1800impl fmt::Display for ForbidOverrideError {
1801    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1802        write!(
1803            formatter,
1804            "rule {} ({}) was forbidden by {}; {} cannot lower it",
1805            self.rule.code(),
1806            self.rule.slug(),
1807            self.forbidden_by,
1808            self.lowered_by
1809        )
1810    }
1811}
1812
1813impl std::error::Error for ForbidOverrideError {}
1814
1815#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1816pub enum SourceDialect {
1817    TypeScript,
1818    JavaScript,
1819}
1820
1821/// A resolved table with independent group and rule lanes, preserving specificity.
1822#[derive(Clone, Debug, Eq, PartialEq)]
1823pub struct LintTable {
1824    profile: LintProfile,
1825    states: Vec<RuleState>,
1826}
1827
1828impl LintTable {
1829    #[must_use]
1830    pub fn new(profile: LintProfile) -> Self {
1831        let profile_source: Arc<str> = Arc::from(format!("{} profile", profile.as_str()));
1832        let states = RULES
1833            .iter()
1834            .map(|rule| RuleState {
1835                profile: AppliedLevel {
1836                    level: profile.level(rule),
1837                    source: Arc::clone(&profile_source),
1838                    specificity: Specificity::Profile,
1839                },
1840                group: None,
1841                rule: None,
1842            })
1843            .collect();
1844        Self { profile, states }
1845    }
1846
1847    #[must_use]
1848    pub const fn profile(&self) -> LintProfile {
1849        self.profile
1850    }
1851
1852    #[must_use]
1853    pub fn level(&self, rule: RuleId) -> LintLevel {
1854        self.state(rule).effective().level
1855    }
1856
1857    #[must_use]
1858    pub fn source(&self, rule: RuleId) -> &str {
1859        &self.state(rule).effective().source
1860    }
1861
1862    /// Applies the JavaScript compatibility dialect after ordinary resolution.
1863    #[must_use]
1864    pub fn level_for_source(&self, rule: RuleId, dialect: SourceDialect) -> LintLevel {
1865        if dialect == SourceDialect::TypeScript {
1866            return self.level(rule);
1867        }
1868        let spec_footgun = matches!(
1869            rule.code(),
1870            "BAMTS-W071"
1871                | "BAMTS-W072"
1872                | "BAMTS-W073"
1873                | "BAMTS-W074"
1874                | "BAMTS-W075"
1875                | "BAMTS-W076"
1876                | "BAMTS-W077"
1877                | "BAMTS-W078"
1878                | "BAMTS-W079"
1879                | "BAMTS-W080"
1880        );
1881        let control_flow = RULES[rule_index(rule)].group() == RuleGroup::ControlFlow;
1882        let javascript_compatibility =
1883            RULES[rule_index(rule)].group() == RuleGroup::JavaScriptCompatibility;
1884        if spec_footgun || control_flow || javascript_compatibility {
1885            let effective = self.level(rule);
1886            return if effective == LintLevel::Allow {
1887                LintLevel::Allow
1888            } else {
1889                LintLevel::Warn
1890            };
1891        }
1892        LintLevel::Allow
1893    }
1894
1895    /// Applies the project baseline: groups first, then the more-specific rule lane.
1896    pub fn apply_config(
1897        &mut self,
1898        config: &LintConfig,
1899    ) -> Result<Vec<LintIssue>, ForbidOverrideError> {
1900        let mut issues = Vec::new();
1901        for setting in config.groups() {
1902            self.apply_setting(setting, OverrideTargetKind::Group, &mut issues)?;
1903        }
1904        for setting in config.rules() {
1905            self.apply_setting(setting, OverrideTargetKind::Rule, &mut issues)?;
1906        }
1907        Ok(issues)
1908    }
1909
1910    /// Applies CLI overrides in declaration order. Rule specificity beats group order.
1911    pub fn apply_cli(
1912        &mut self,
1913        overrides: impl IntoIterator<Item = LintOverride>,
1914    ) -> Result<Vec<LintIssue>, ForbidOverrideError> {
1915        let mut issues = Vec::new();
1916        for lint_override in overrides {
1917            self.apply_setting(
1918                &lint_override.setting,
1919                lint_override.target_kind,
1920                &mut issues,
1921            )?;
1922        }
1923        Ok(issues)
1924    }
1925
1926    fn apply_setting(
1927        &mut self,
1928        setting: &LintSetting,
1929        kind: OverrideTargetKind,
1930        issues: &mut Vec<LintIssue>,
1931    ) -> Result<(), ForbidOverrideError> {
1932        if kind != OverrideTargetKind::Rule {
1933            if let Ok(group) = RuleGroup::from_str(setting.name()) {
1934                return self.apply_group(group, setting);
1935            }
1936            if kind == OverrideTargetKind::Group {
1937                issues.push(self.unknown_issue(setting, group_suggestion(setting.name())));
1938                return Ok(());
1939            }
1940        }
1941
1942        if is_tombstone(setting.name()) {
1943            issues.push(LintIssue {
1944                name: Arc::clone(&setting.name),
1945                level: LintLevel::Deny,
1946                source: Arc::clone(&setting.source),
1947                kind: LintIssueKind::RetiredCode,
1948            });
1949            return Ok(());
1950        }
1951        if let Some(rule) = rule_by_code(setting.name()).or_else(|| rule_by_slug(setting.name())) {
1952            return self.apply_rule(rule, setting);
1953        }
1954        if let Some(alias) = alias_by_name(setting.name()) {
1955            let rule = rule_by_code(alias.target_code).expect("alias target must be registered");
1956            issues.push(LintIssue {
1957                name: Arc::clone(&setting.name),
1958                level: LintLevel::Warn,
1959                source: Arc::clone(&setting.source),
1960                kind: LintIssueKind::RenamedRule {
1961                    canonical: rule.slug(),
1962                },
1963            });
1964            return self.apply_rule(rule, setting);
1965        }
1966        issues.push(self.unknown_issue(setting, rule_suggestion(setting.name())));
1967        Ok(())
1968    }
1969
1970    fn apply_group(
1971        &mut self,
1972        group: RuleGroup,
1973        setting: &LintSetting,
1974    ) -> Result<(), ForbidOverrideError> {
1975        let targets: Vec<usize> = RULES
1976            .iter()
1977            .enumerate()
1978            .filter_map(|(index, rule)| (rule.group() == group).then_some(index))
1979            .collect();
1980        self.check_forbid(&targets, setting, Specificity::Group)?;
1981        for index in targets {
1982            self.states[index].group = Some(AppliedLevel {
1983                level: setting.level,
1984                source: Arc::clone(&setting.source),
1985                specificity: Specificity::Group,
1986            });
1987        }
1988        Ok(())
1989    }
1990
1991    fn apply_rule(
1992        &mut self,
1993        rule: &'static RuleDefinition,
1994        setting: &LintSetting,
1995    ) -> Result<(), ForbidOverrideError> {
1996        let index = rule_index(rule.id());
1997        self.check_forbid(&[index], setting, Specificity::Rule)?;
1998        self.states[index].rule = Some(AppliedLevel {
1999            level: setting.level,
2000            source: Arc::clone(&setting.source),
2001            specificity: Specificity::Rule,
2002        });
2003        Ok(())
2004    }
2005
2006    fn check_forbid(
2007        &self,
2008        indices: &[usize],
2009        setting: &LintSetting,
2010        specificity: Specificity,
2011    ) -> Result<(), ForbidOverrideError> {
2012        if setting.level == LintLevel::Forbid {
2013            return Ok(());
2014        }
2015        for &index in indices {
2016            let active = self.states[index].effective();
2017            if active.level == LintLevel::Forbid && specificity >= active.specificity {
2018                return Err(ForbidOverrideError {
2019                    rule: RULES[index].id(),
2020                    forbidden_by: Arc::clone(&active.source),
2021                    lowered_by: Arc::clone(&setting.source),
2022                });
2023            }
2024        }
2025        Ok(())
2026    }
2027
2028    fn unknown_issue(&self, setting: &LintSetting, suggestion: Option<Arc<str>>) -> LintIssue {
2029        LintIssue {
2030            name: Arc::clone(&setting.name),
2031            level: self.profile.unknown_level(),
2032            source: Arc::clone(&setting.source),
2033            kind: LintIssueKind::UnknownName { suggestion },
2034        }
2035    }
2036
2037    fn state(&self, rule: RuleId) -> &RuleState {
2038        &self.states[rule_index(rule)]
2039    }
2040}
2041
2042fn rule_index(id: RuleId) -> usize {
2043    let code = id.code().as_bytes();
2044    let number = usize::from(code[7] - b'0') * 100
2045        + usize::from(code[8] - b'0') * 10
2046        + usize::from(code[9] - b'0');
2047    number - 1
2048}
2049
2050fn group_suggestion(name: &str) -> Option<Arc<str>> {
2051    nearest_name(name, RuleGroup::ALL.into_iter().map(RuleGroup::slug))
2052}
2053
2054fn rule_suggestion(name: &str) -> Option<Arc<str>> {
2055    nearest_name(
2056        name,
2057        RULES
2058            .iter()
2059            .flat_map(|rule| [rule.code(), rule.slug()])
2060            .chain(RULE_ALIASES.iter().map(|alias| alias.alias)),
2061    )
2062}
2063
2064fn nearest_name<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<Arc<str>> {
2065    candidates
2066        .map(|candidate| (levenshtein(name, candidate), candidate))
2067        .min_by_key(|(distance, candidate)| (*distance, *candidate))
2068        .map(|(_, candidate)| Arc::from(candidate))
2069}
2070
2071fn levenshtein(left: &str, right: &str) -> usize {
2072    let right_chars: Vec<char> = right.chars().collect();
2073    let mut previous: Vec<usize> = (0..=right_chars.len()).collect();
2074    let mut current = vec![0; right_chars.len() + 1];
2075    for (left_index, left_char) in left.chars().enumerate() {
2076        current[0] = left_index + 1;
2077        for (right_index, right_char) in right_chars.iter().enumerate() {
2078            let substitution = previous[right_index] + usize::from(left_char != *right_char);
2079            current[right_index + 1] = (current[right_index] + 1)
2080                .min(previous[right_index + 1] + 1)
2081                .min(substitution);
2082        }
2083        std::mem::swap(&mut previous, &mut current);
2084    }
2085    previous[right_chars.len()]
2086}
2087
2088#[cfg(test)]
2089mod tests {
2090    use super::*;
2091
2092    fn rule(slug: &str) -> RuleId {
2093        rule_by_slug(slug).expect("test rule must exist").id()
2094    }
2095
2096    #[test]
2097    fn registry_is_complete_and_unique() {
2098        assert_eq!(RULES.len(), 86);
2099        for (index, rule) in RULES.iter().enumerate() {
2100            assert!(rule.code().starts_with("BAMTS-W"));
2101            assert!(!rule.slug().is_empty());
2102            assert!(
2103                !RULES[..index]
2104                    .iter()
2105                    .any(|other| other.code() == rule.code())
2106            );
2107            assert!(
2108                !RULES[..index]
2109                    .iter()
2110                    .any(|other| other.slug() == rule.slug())
2111            );
2112            assert!(
2113                !RULE_TOMBSTONES
2114                    .iter()
2115                    .any(|entry| entry.code() == rule.code())
2116            );
2117        }
2118    }
2119
2120    #[test]
2121    fn ordered_overrides_keep_rule_specificity_over_later_group() {
2122        let target = rule("explicit-any");
2123        let mut table = LintTable::new(LintProfile::Default);
2124        table
2125            .apply_cli([
2126                LintOverride::group(
2127                    RuleGroup::EscapeHatches,
2128                    LintLevel::Deny,
2129                    "-D escape-hatches",
2130                ),
2131                LintOverride::rule(target, LintLevel::Allow, "-A explicit-any"),
2132                LintOverride::group(
2133                    RuleGroup::EscapeHatches,
2134                    LintLevel::Warn,
2135                    "-W escape-hatches",
2136                ),
2137            ])
2138            .unwrap();
2139        assert_eq!(table.level(target), LintLevel::Allow);
2140        assert_eq!(table.source(target), "-A explicit-any");
2141        assert_eq!(table.level(rule("implicit-any")), LintLevel::Warn);
2142    }
2143
2144    #[test]
2145    fn later_override_wins_within_the_same_specificity() {
2146        let target = rule("unused-local");
2147        let mut table = LintTable::new(LintProfile::Default);
2148        table
2149            .apply_cli([
2150                LintOverride::rule(target, LintLevel::Deny, "first"),
2151                LintOverride::rule(target, LintLevel::Warn, "second"),
2152            ])
2153            .unwrap();
2154        assert_eq!(table.level(target), LintLevel::Warn);
2155        assert_eq!(table.source(target), "second");
2156    }
2157
2158    #[test]
2159    fn forbid_lock_reports_both_sources() {
2160        let target = rule("explicit-any");
2161        let mut table = LintTable::new(LintProfile::Default);
2162        table
2163            .apply_cli([LintOverride::rule(
2164                target,
2165                LintLevel::Forbid,
2166                "security policy",
2167            )])
2168            .unwrap();
2169        let error = table
2170            .apply_cli([LintOverride::rule(
2171                target,
2172                LintLevel::Warn,
2173                "developer flag",
2174            )])
2175            .unwrap_err();
2176        assert_eq!(error.rule(), target);
2177        assert_eq!(error.forbidden_by(), "security policy");
2178        assert_eq!(error.lowered_by(), "developer flag");
2179    }
2180
2181    #[test]
2182    fn profiles_expand_the_settled_families() {
2183        let escape = rule("explicit-any");
2184        let opinionated = rule("prefer-type-alias");
2185        let module_exception = rule("commonjs-in-esm");
2186        let const_enum = rule("const-enum");
2187        assert_eq!(
2188            LintTable::new(LintProfile::Default).level(escape),
2189            LintLevel::Warn
2190        );
2191        assert_eq!(
2192            LintTable::new(LintProfile::Strict).level(escape),
2193            LintLevel::Deny
2194        );
2195        assert_eq!(
2196            LintTable::new(LintProfile::Pedantic).level(escape),
2197            LintLevel::Forbid
2198        );
2199        assert_eq!(
2200            LintTable::new(LintProfile::Default).level(opinionated),
2201            LintLevel::Allow
2202        );
2203        assert_eq!(
2204            LintTable::new(LintProfile::Strict).level(opinionated),
2205            LintLevel::Allow
2206        );
2207        assert_eq!(
2208            LintTable::new(LintProfile::Pedantic).level(opinionated),
2209            LintLevel::Warn
2210        );
2211        assert_eq!(
2212            LintTable::new(LintProfile::Strict).level(module_exception),
2213            LintLevel::Allow
2214        );
2215        assert_eq!(
2216            LintTable::new(LintProfile::Strict).level(const_enum),
2217            LintLevel::Warn
2218        );
2219        assert_eq!(
2220            LintTable::new(LintProfile::Strict).level(rule("runtime-enum")),
2221            LintLevel::Deny
2222        );
2223    }
2224
2225    #[test]
2226    fn aliases_resolve_and_warn_without_losing_the_setting() {
2227        let mut table = LintTable::new(LintProfile::Default);
2228        let issues = table
2229            .apply_cli([LintOverride::new(
2230                "any-downcast",
2231                LintLevel::Deny,
2232                "legacy config",
2233            )])
2234            .unwrap();
2235        assert_eq!(table.level(rule("generic-any-downcast")), LintLevel::Deny);
2236        assert!(matches!(
2237            issues[0].kind(),
2238            LintIssueKind::RenamedRule {
2239                canonical: "generic-any-downcast"
2240            }
2241        ));
2242    }
2243
2244    #[test]
2245    fn tombstones_are_rejected() {
2246        let mut table = LintTable::new(LintProfile::Default);
2247        let issues = table
2248            .apply_cli([LintOverride::new("BAMTS-W000", LintLevel::Warn, "config")])
2249            .unwrap();
2250        assert_eq!(issues[0].level(), LintLevel::Deny);
2251        assert_eq!(issues[0].kind(), &LintIssueKind::RetiredCode);
2252    }
2253
2254    #[test]
2255    fn unknown_names_warn_by_default_and_deny_in_stricter_profiles() {
2256        for (profile, level) in [
2257            (LintProfile::Default, LintLevel::Warn),
2258            (LintProfile::Strict, LintLevel::Deny),
2259            (LintProfile::Pedantic, LintLevel::Deny),
2260        ] {
2261            let mut table = LintTable::new(profile);
2262            let issues = table
2263                .apply_cli([LintOverride::new("explicit-ang", LintLevel::Warn, "config")])
2264                .unwrap();
2265            assert_eq!(issues[0].level(), level);
2266            assert!(matches!(
2267                issues[0].kind(),
2268                LintIssueKind::UnknownName { suggestion: Some(name) } if name.as_ref() == "explicit-any"
2269            ));
2270        }
2271    }
2272
2273    #[test]
2274    fn javascript_dialect_preserves_allow_and_clamps_enabled_rules_to_warning() {
2275        let table = LintTable::new(LintProfile::Pedantic);
2276        assert_eq!(
2277            table.level_for_source(
2278                rule("invalid-number-formatting-options"),
2279                SourceDialect::JavaScript
2280            ),
2281            LintLevel::Warn
2282        );
2283        assert_eq!(
2284            table.level_for_source(rule("unused-local"), SourceDialect::JavaScript),
2285            LintLevel::Warn
2286        );
2287        assert_eq!(
2288            table.level_for_source(rule("explicit-any"), SourceDialect::JavaScript),
2289            LintLevel::Allow
2290        );
2291        assert_eq!(
2292            table.level_for_source(
2293                rule("javascript-syntax-rejection"),
2294                SourceDialect::JavaScript
2295            ),
2296            LintLevel::Warn
2297        );
2298        assert_eq!(
2299            LintTable::new(LintProfile::Default)
2300                .level_for_source(rule("javascript-input"), SourceDialect::JavaScript,),
2301            LintLevel::Allow
2302        );
2303    }
2304}