use std::{fmt, fmt::Write as _, str::FromStr, sync::Arc};
use crate::source::ScriptKind;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum LintLevel {
Allow,
Warn,
Deny,
Forbid,
}
impl LintLevel {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Allow => "allow",
Self::Warn => "warn",
Self::Deny => "deny",
Self::Forbid => "forbid",
}
}
}
impl fmt::Display for LintLevel {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for LintLevel {
type Err = ParseLintLevelError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"allow" => Ok(Self::Allow),
"warn" => Ok(Self::Warn),
"deny" => Ok(Self::Deny),
"forbid" => Ok(Self::Forbid),
_ => Err(ParseLintLevelError(Arc::from(value))),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseLintLevelError(Arc<str>);
impl fmt::Display for ParseLintLevelError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "unknown lint level {:?}", self.0)
}
}
impl std::error::Error for ParseLintLevelError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RuleGroup {
Unsoundness,
EscapeHatches,
NonErasable,
LegacySyntax,
Modules,
ClassSemantics,
EnumSemantics,
DeclarationMerging,
JavaScriptCompatibility,
Opinionated,
ControlFlow,
}
impl RuleGroup {
pub const ALL: [Self; 11] = [
Self::Unsoundness,
Self::EscapeHatches,
Self::NonErasable,
Self::LegacySyntax,
Self::Modules,
Self::ClassSemantics,
Self::EnumSemantics,
Self::DeclarationMerging,
Self::JavaScriptCompatibility,
Self::Opinionated,
Self::ControlFlow,
];
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Unsoundness => "unsoundness",
Self::EscapeHatches => "escape-hatches",
Self::NonErasable => "non-erasable",
Self::LegacySyntax => "legacy-syntax",
Self::Modules => "modules",
Self::ClassSemantics => "class-semantics",
Self::EnumSemantics => "enum-semantics",
Self::DeclarationMerging => "declaration-merging",
Self::JavaScriptCompatibility => "javascript-compatibility",
Self::Opinionated => "opinionated",
Self::ControlFlow => "control-flow",
}
}
}
impl FromStr for RuleGroup {
type Err = ParseRuleGroupError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|group| group.slug() == value)
.ok_or_else(|| ParseRuleGroupError(Arc::from(value)))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseRuleGroupError(Arc<str>);
impl fmt::Display for ParseRuleGroupError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "unknown lint group {:?}", self.0)
}
}
impl std::error::Error for ParseRuleGroupError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RuleId {
code: &'static str,
slug: &'static str,
}
impl RuleId {
#[must_use]
pub const fn code(self) -> &'static str {
self.code
}
#[must_use]
pub const fn slug(self) -> &'static str {
self.slug
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RuleExampleSource {
script_kind: ScriptKind,
text: &'static str,
resolves_to: Option<usize>,
}
impl RuleExampleSource {
#[must_use]
pub const fn new(script_kind: ScriptKind, text: &'static str) -> Self {
Self {
script_kind,
text,
resolves_to: None,
}
}
#[must_use]
pub const fn resolving_to(mut self, source: usize) -> Self {
self.resolves_to = Some(source);
self
}
#[must_use]
pub const fn script_kind(self) -> ScriptKind {
self.script_kind
}
#[must_use]
pub const fn text(self) -> &'static str {
self.text
}
#[must_use]
pub const fn resolves_to(self) -> Option<usize> {
self.resolves_to
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct CompilerLintOptions {
pub preserve_const_enums: bool,
pub emit_decorator_metadata: bool,
pub use_define_for_class_fields: bool,
}
impl CompilerLintOptions {
pub const STANDARD: Self = Self {
preserve_const_enums: false,
emit_decorator_metadata: false,
use_define_for_class_fields: true,
};
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RuleExampleCase {
Source(RuleExampleSource),
Program(&'static [RuleExampleSource]),
CompilerOptions(CompilerLintOptions),
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RuleExamples {
trigger: RuleExampleCase,
clean: RuleExampleCase,
}
impl RuleExamples {
#[must_use]
pub const fn new(trigger: RuleExampleCase, clean: RuleExampleCase) -> Self {
Self { trigger, clean }
}
#[must_use]
pub const fn trigger(self) -> RuleExampleCase {
self.trigger
}
#[must_use]
pub const fn clean(self) -> RuleExampleCase {
self.clean
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RuleDefinition {
id: RuleId,
group: RuleGroup,
default_level: LintLevel,
rationale: &'static str,
sound_alternative: &'static str,
silence_flag: &'static str,
examples: RuleExamples,
}
impl RuleDefinition {
const fn new(
id: RuleId,
group: RuleGroup,
default_level: LintLevel,
rationale: &'static str,
sound_alternative: &'static str,
silence_flag: &'static str,
examples: RuleExamples,
) -> Self {
Self {
id,
group,
default_level,
rationale,
sound_alternative,
silence_flag,
examples,
}
}
#[must_use]
pub const fn id(&self) -> RuleId {
self.id
}
#[must_use]
pub const fn code(&self) -> &'static str {
self.id.code()
}
#[must_use]
pub const fn slug(&self) -> &'static str {
self.id.slug()
}
#[must_use]
pub const fn group(&self) -> RuleGroup {
self.group
}
#[must_use]
pub const fn default_level(&self) -> LintLevel {
self.default_level
}
#[must_use]
pub const fn rationale(&self) -> &'static str {
self.rationale
}
#[must_use]
pub const fn sound_alternative(&self) -> &'static str {
self.sound_alternative
}
#[must_use]
pub const fn silence_flag(&self) -> &'static str {
self.silence_flag
}
#[must_use]
pub const fn examples(&self) -> RuleExamples {
self.examples
}
}
macro_rules! source_example {
($kind:ident, $text:literal) => {
RuleExampleCase::Source(RuleExampleSource::new(ScriptKind::$kind, $text))
};
}
macro_rules! examples {
($trigger:literal, $clean:literal) => {
RuleExamples::new(
source_example!(TypeScript, $trigger),
source_example!(TypeScript, $clean),
)
};
($kind:ident, $trigger:literal, $clean:literal) => {
RuleExamples::new(
source_example!($kind, $trigger),
source_example!($kind, $clean),
)
};
}
macro_rules! rule {
($code:literal, $slug:literal, $group:ident, $level:ident, $rationale:literal, $alternative:literal, $examples:expr) => {
RuleDefinition::new(
RuleId {
code: $code,
slug: $slug,
},
RuleGroup::$group,
LintLevel::$level,
$rationale,
$alternative,
concat!("-A ", $slug),
$examples,
)
};
}
pub static RULES: [RuleDefinition; 86] = [
rule!(
"BAMTS-W001",
"method-parameter-bivariance",
Unsoundness,
Warn,
"Method parameters are bivariant, so a narrower handler can receive an incompatible value.",
"Use a function-property callback with a contravariant parameter.",
examples!(
"interface H { handle(x: Dog): void }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W002",
"mutable-array-covariance",
Unsoundness,
Warn,
"Mutable arrays are covariant, so a widened alias can write the wrong element type.",
"Expose readonly arrays across type boundaries.",
examples!(
"const dogs: Dog[] = []; const animals: Animal[] = dogs;",
"const dogs: Animal[] = []; const animals: Animal[] = dogs;"
)
),
rule!(
"BAMTS-W003",
"non-fresh-excess-property",
Unsoundness,
Warn,
"A non-fresh object can bypass excess-property checks and hide misspelled fields.",
"Validate the object at its construction boundary.",
examples!(
"const candidate = { keep: 1, extra: true }; const target: { keep: number } = candidate;",
"const target: { keep: number } = { keep: 1, extra: true };"
)
),
rule!(
"BAMTS-W004",
"delete-required-property",
Unsoundness,
Warn,
"Deleting a required property breaks the declared object shape.",
"Model removability with an optional property or a separate value.",
examples!(
"const item: { required: number } = { required: 1 }; delete item.required;",
"const item: { optional?: number } = {}; delete item.optional;"
)
),
rule!(
"BAMTS-W005",
"unchecked-catch-member",
Unsoundness,
Warn,
"A catch binding is untrusted until it is narrowed before member access.",
"Narrow the caught value with a runtime guard.",
examples!(
"try {} catch (error) { error.message; }",
"try {} catch (error) { if (error instanceof Error) error.message; }"
)
),
rule!(
"BAMTS-W006",
"generic-any-downcast",
EscapeHatches,
Warn,
"Casting any through a generic return loses the proof required by every caller.",
"Validate the input and return a concrete checked type.",
examples!(
"function f<T>(x:any):T{return x as T}",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W007",
"dynamic-tuple-index",
Unsoundness,
Warn,
"A dynamic tuple index can read beyond the tuple's known bounds.",
"Use a literal index or prove the index is in range.",
examples!(
"const pair: [string, number] = [\"a\", 1]; pair[index];",
"const pair: [string, number] = [\"a\", 1]; pair[1];"
)
),
rule!(
"BAMTS-W008",
"unchecked-index-signature-read",
Unsoundness,
Warn,
"An index-signature read can be absent even when its value type excludes undefined.",
"Handle undefined after the lookup.",
examples!(
"interface D {[key: string]: number} declare const d:D; declare const k:string; const n=d[k];",
"const colors: Record<'red', number>={red:1}; const n=colors['red'];"
)
),
rule!(
"BAMTS-W009",
"explicit-undefined-for-optional",
Unsoundness,
Warn,
"An optional property without undefined distinguishes absence from an explicit undefined value.",
"Omit the property or include undefined in its declared type.",
examples!(
"const o: {x?: number} = {x: undefined};",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W010",
"detached-this-method",
Unsoundness,
Warn,
"Extracting a receiver-dependent method loses the this binding it requires.",
"Bind the method or call it through its receiver.",
examples!("const f = obj.method; f();", "const safe: number = 1;")
),
rule!(
"BAMTS-W011",
"divergent-accessor-types",
Unsoundness,
Warn,
"Different getter and setter types hide an unsafe property boundary.",
"Use one compatible property type or an explicit conversion method.",
examples!(
"class C { get x(): number { return 1 } set x(v: string | number) {} }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W012",
"readonly-alias-mutation",
Unsoundness,
Warn,
"A writable alias can mutate data promised as readonly elsewhere.",
"Keep the mutable value private and expose a readonly view.",
examples!(
"const r: {readonly x:number}=m; m.x=2;",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W013",
"fewer-callback-parameters",
Unsoundness,
Warn,
"A callback that accepts fewer parameters can silently discard required protocol data.",
"Declare the callback parameters you intentionally receive.",
examples!(
"const f: (x:number,y:string)=>void = () => {};",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W014",
"value-returning-void-callback",
Unsoundness,
Warn,
"A value returned from a void callback is silently discarded.",
"Use a block body when the return value is intentionally ignored.",
examples!("const f: () => void = () => 42;", "const safe: number = 1;")
),
rule!(
"BAMTS-W015",
"open-object-keys-assumption",
Unsoundness,
Warn,
"Object.keys does not prove that runtime keys are limited to keyof T.",
"Validate keys at runtime or work from a closed key list.",
examples!(
"const ks = Object.keys(x) as (keyof typeof x)[];",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W016",
"index-signature-dot-access",
Unsoundness,
Warn,
"Dot access through an index signature hides that a property may be absent.",
"Use bracket access and handle the missing value.",
examples!(
"interface D {[key:string]: number} declare const d:D; d.username;",
"interface D {[key:string]: number} declare const d:D; d['username'];"
)
),
rule!(
"BAMTS-W017",
"explicit-any",
EscapeHatches,
Warn,
"Explicit any disables type checking at the annotated boundary.",
"Use unknown and narrow it before use.",
examples!("let value: any;", "const safe: number = 1;")
),
rule!(
"BAMTS-W018",
"implicit-any",
EscapeHatches,
Warn,
"An inferred any lets an untyped value flow without an explicit boundary.",
"Add an explicit checked type or unknown annotation.",
examples!("function f(x) { return x; }", "const safe: number = 1;")
),
rule!(
"BAMTS-W019",
"unchecked-type-assertion",
EscapeHatches,
Warn,
"A type assertion claims a narrower type without runtime proof.",
"Narrow with a guard or validate with a decoder.",
examples!("const n = value as number;", "const safe: number = 1;")
),
rule!(
"BAMTS-W020",
"double-assertion",
EscapeHatches,
Warn,
"A double assertion bypasses assignability through any or unknown.",
"Convert or validate the value at the boundary.",
examples!(
"const n = value as unknown as number;",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W021",
"non-null-assertion",
EscapeHatches,
Warn,
"A non-null assertion erases a possible null or undefined value.",
"Narrow the value before accessing it.",
examples!("node!.textContent;", "const safe: number = 1;")
),
rule!(
"BAMTS-W022",
"definite-assignment-assertion",
EscapeHatches,
Warn,
"A definite-assignment assertion skips proof that a field is initialized.",
"Initialize the field or assign it in every constructor path.",
examples!("class C { value!: string }", "const safe: number = 1;")
),
rule!(
"BAMTS-W023",
"diagnostic-suppression-directive",
EscapeHatches,
Warn,
"A TypeScript diagnostic directive hides a compiler check instead of resolving it.",
"Fix the diagnostic or make the boundary explicit.",
examples!("// @ts-ignore", "const safe: number = 1;")
),
rule!(
"BAMTS-W024",
"runtime-namespace",
NonErasable,
Warn,
"A value-bearing namespace requires runtime code instead of erasing as type syntax.",
"Use ES modules or an ambient namespace.",
examples!(
"namespace N { export const x = 1 }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W025",
"parameter-property",
NonErasable,
Warn,
"A parameter property synthesizes a field assignment during compilation.",
"Declare the field and assign the constructor parameter explicitly.",
examples!(
"class C { constructor(public x: number) {} }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W026",
"legacy-decorator-semantics",
LegacySyntax,
Warn,
"Legacy decorators have semantics that differ from standard ECMAScript decorators.",
"Use standard decorators or an explicit wrapper.",
examples!("@sealed class C {}", "const safe: number = 1;")
),
rule!(
"BAMTS-W027",
"angle-bracket-assertion",
LegacySyntax,
Warn,
"Angle-bracket assertions are ambiguous with JSX syntax.",
"Use the `as T` assertion spelling.",
examples!("const n = <number>value;", "const safe: number = 1;")
),
rule!(
"BAMTS-W028",
"declaration-inference-dependency",
LegacySyntax,
Warn,
"Declaration output that depends on cross-file inference is fragile and non-local.",
"Write an explicit exported type annotation.",
RuleExamples::new(
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"import { make } from './dep.js'; export const value = make();"
)
.resolving_to(1),
RuleExampleSource::new(
ScriptKind::TypeScript,
"export const make = (): number => 1;"
)
]),
source_example!(TypeScript, "export const value: number = 1;")
)
),
rule!(
"BAMTS-W029",
"jsx-transform-required",
LegacySyntax,
Warn,
"JSX requires a configured runtime transform and cannot simply be erased.",
"Configure a JSX runtime or use ordinary function calls.",
examples!(
TypeScriptReact,
"const el = <Widget value={1} />;",
"const safe = 1;"
)
),
rule!(
"BAMTS-W030",
"import-export-equals",
Modules,
Warn,
"TypeScript import-equals and export-equals require target-specific module rewriting.",
"Use standard ESM import and export syntax.",
examples!("import fs = require(\"fs\");", "const safe: number = 1;")
),
rule!(
"BAMTS-W031",
"type-imported-as-value",
Modules,
Warn,
"A type-only import emitted as a value import creates a runtime dependency.",
"Use `import type` for type-only symbols.",
RuleExamples::new(
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"import { User } from './types.js'; const user: User = { name: 'Ada' };"
)
.resolving_to(1),
RuleExampleSource::new(
ScriptKind::TypeScript,
"export interface User { name: string }"
)
]),
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"import type { User } from './types.js'; const user: User = { name: 'Ada' };"
)
.resolving_to(1),
RuleExampleSource::new(
ScriptKind::TypeScript,
"export interface User { name: string }"
)
])
)
),
rule!(
"BAMTS-W032",
"type-reexported-as-value",
Modules,
Warn,
"A type-only re-export emitted as a value re-export creates a runtime dependency.",
"Use `export type` for type-only symbols.",
RuleExamples::new(
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"export { User } from './types.js';"
)
.resolving_to(1),
RuleExampleSource::new(
ScriptKind::TypeScript,
"export interface User { name: string }"
)
]),
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"export type { User } from './types.js';"
)
.resolving_to(1),
RuleExampleSource::new(
ScriptKind::TypeScript,
"export interface User { name: string }"
)
])
)
),
rule!(
"BAMTS-W033",
"commonjs-in-esm",
Modules,
Allow,
"CommonJS globals inside an ESM module depend on host-specific interop.",
"Use ESM exports or isolate the CommonJS bridge.",
examples!(
"export const x = require('x');",
"const x = require('x'); x;"
)
),
rule!(
"BAMTS-W034",
"implicit-script-file",
Modules,
Allow,
"A file without imports or exports silently becomes a global script.",
"Add an explicit export or force module detection.",
examples!("const shared = 1;", "export {}; const shared = 1;")
),
rule!(
"BAMTS-W035",
"unchecked-side-effect-import",
Modules,
Warn,
"An unresolved side-effect import can conceal a missing runtime dependency.",
"Resolve the module or declare the host-provided virtual module.",
RuleExamples::new(
RuleExampleCase::Program(&[RuleExampleSource::new(
ScriptKind::TypeScript,
"import './missing.js';"
)]),
RuleExampleCase::Program(&[
RuleExampleSource::new(ScriptKind::TypeScript, "import './polyfill.js';")
.resolving_to(1),
RuleExampleSource::new(ScriptKind::JavaScript, "globalThis.ready = true;")
])
)
),
rule!(
"BAMTS-W036",
"extensionless-relative-import",
Modules,
Warn,
"Relative ESM imports need a runtime file extension in Node-style resolution.",
"Write the explicit runtime extension.",
examples!("import {x} from \"./util\";", "const safe: number = 1;")
),
rule!(
"BAMTS-W037",
"interop-dependent-default-import",
Modules,
Warn,
"A default import from CommonJS can rely on synthetic interop semantics.",
"Use a namespace import or a real ESM default export.",
RuleExamples::new(
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"import legacy from './legacy.js'; legacy();"
)
.resolving_to(1),
RuleExampleSource::new(
ScriptKind::JavaScript,
"module.exports = function legacy() {};"
)
]),
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"import modern from './modern.js'; modern();"
)
.resolving_to(1),
RuleExampleSource::new(
ScriptKind::JavaScript,
"export default function modern() {}"
)
])
)
),
rule!(
"BAMTS-W038",
"virtual-call-in-constructor",
ClassSemantics,
Allow,
"A constructor dispatching to an overridable method can observe uninitialized derived state.",
"Defer the hook until construction is complete.",
examples!(
"class B { constructor(){ this.init() } }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W039",
"uninitialized-field-emit-split",
ClassSemantics,
Allow,
"An uninitialized field has different runtime presence under competing emit modes.",
"Initialize it or use `declare` when no own field is intended.",
examples!("class C { value: string; }", "const safe: number = 1;")
),
rule!(
"BAMTS-W040",
"field-overrides-accessor",
ClassSemantics,
Allow,
"A defined field can shadow an inherited accessor instead of invoking it.",
"Use an accessor, `declare`, or a distinct field name.",
examples!(
"class B { get data():number{return 1} } class D extends B { data = 1; }",
"class B { get data():number{return 1} } class D extends B { declare data:number; }"
)
),
rule!(
"BAMTS-W041",
"implicit-override",
ClassSemantics,
Allow,
"An unmarked override can silently drift when its base member changes.",
"Mark the member with `override`.",
examples!(
"class B { run(){} } class D extends B { run(){} }",
"class B { run(){} } class D extends B { override run(){} }"
)
),
rule!(
"BAMTS-W042",
"typescript-private-field",
ClassSemantics,
Allow,
"A TypeScript private modifier erases and does not provide runtime privacy.",
"Use an ECMAScript `#private` field for runtime privacy.",
examples!("class C { private secret = 1 }", "const safe: number = 1;")
),
rule!(
"BAMTS-W043",
"runtime-enum",
EnumSemantics,
Warn,
"A non-const enum creates a runtime object with non-erasable behavior.",
"Use a union or a const object when a runtime object is intentional.",
examples!("enum Color { Red, Blue }", "const safe: number = 1;")
),
rule!(
"BAMTS-W044",
"const-enum",
EnumSemantics,
Warn,
"A const enum relies on compile-time inlining across compilation boundaries.",
"Use a union or a const object.",
examples!("const enum Code { Ok = 200 }", "const safe: number = 1;")
),
rule!(
"BAMTS-W045",
"numeric-enum-number-flow",
EnumSemantics,
Warn,
"Numeric enums accept arbitrary numbers, weakening the enum boundary.",
"Use a string enum or validate the numeric value.",
examples!(
"enum E { A } let e:E=E.A; let n:number=e;",
"enum E { A } const e=E.A;"
)
),
rule!(
"BAMTS-W046",
"heterogeneous-enum",
EnumSemantics,
Warn,
"A heterogeneous enum mixes unrelated representations and complicates consumers.",
"Use one representation or a discriminated union.",
examples!(
"enum Answer { No = 0, Yes = \"YES\" }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W047",
"computed-enum-member",
EnumSemantics,
Warn,
"A computed enum member depends on runtime evaluation rather than a stable constant.",
"Use a constant initializer or a separate runtime value.",
examples!("enum E { X = getValue() }", "const safe: number = 1;")
),
rule!(
"BAMTS-W048",
"numeric-enum-reverse-lookup",
EnumSemantics,
Warn,
"Numeric enum reverse lookup depends on generated runtime mappings.",
"Store the display name explicitly.",
examples!(
"enum E { A } const name=E[E.A];",
"enum E { A } const value=E.A;"
)
),
rule!(
"BAMTS-W049",
"interface-declaration-merge",
DeclarationMerging,
Warn,
"Same-scope interfaces merge implicitly, making a type's shape non-local.",
"Declare one complete interface or use a closed type alias.",
examples!(
"interface Box {x:number} interface Box {y:number}",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W050",
"namespace-value-merge",
DeclarationMerging,
Warn,
"A namespace merged with a value creates an implicit hybrid declaration.",
"Use an explicit object or separate module export.",
examples!(
"function f(){} namespace f { export const x=1 }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W051",
"global-augmentation",
DeclarationMerging,
Warn,
"A global augmentation mutates ambient types for unrelated code.",
"Expose a local wrapper or explicit global installation boundary.",
examples!(
"declare global { interface Window { x: number } }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W052",
"module-augmentation",
DeclarationMerging,
Warn,
"A module augmentation changes another module's contract outside that module.",
"Wrap or extend the module through an explicit local API.",
examples!(
"declare module \"lib\" { interface X { y: number } }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W053",
"ambient-value-declaration",
DeclarationMerging,
Warn,
"An ambient value declaration cannot prove that the runtime provides the value.",
"Pass the value explicitly or install it through a checked host API.",
examples!("declare const injected: string;", "const safe: number = 1;")
),
rule!(
"BAMTS-W054",
"javascript-input",
JavaScriptCompatibility,
Allow,
"JavaScript source enters a typed program with weaker static guarantees.",
"Convert the source to TypeScript or isolate it behind typed declarations.",
RuleExamples::new(
source_example!(JavaScript, "const legacy = 1;"),
source_example!(TypeScript, "const safe: number = 1;")
)
),
rule!(
"BAMTS-W055",
"jsdoc-type-syntax",
JavaScriptCompatibility,
Allow,
"JSDoc types make JavaScript comments carry part of the type system.",
"Move the file to TypeScript with native type syntax.",
examples!(
JavaScript,
"/** @type {number} */ let n = 1;",
"const safe = 1;"
)
),
rule!(
"BAMTS-W056",
"prototype-class-pattern",
JavaScriptCompatibility,
Allow,
"Prototype assignment spreads class behavior across mutable runtime objects.",
"Use class syntax or an explicit factory object.",
examples!(
JavaScript,
"Ctor.prototype.run = function() {};",
"const safe = 1;"
)
),
rule!(
"BAMTS-W057",
"ts-check-directive",
JavaScriptCompatibility,
Allow,
"A per-file ts-check directive makes type-checking policy non-uniform.",
"Use project-wide checkJs or convert the file to TypeScript.",
examples!(JavaScript, "// @ts-check", "const safe = 1;")
),
rule!(
"BAMTS-W058",
"prefer-type-alias",
Opinionated,
Allow,
"An interface can merge later, leaving an API shape open unintentionally.",
"Use a type alias for a closed shape.",
examples!("interface Point { x: number }", "const safe: number = 1;")
),
rule!(
"BAMTS-W059",
"prefer-readonly-array",
Opinionated,
Allow,
"A mutable array type advertises mutation where a read-only view may suffice.",
"Accept `readonly T[]` unless mutation is required.",
examples!("function f(xs: string[]) {}", "const safe: number = 1;")
),
rule!(
"BAMTS-W060",
"prefer-function-property",
Opinionated,
Allow,
"A method signature keeps bivariant parameter checking.",
"Use a function-property signature for callback members.",
examples!(
"interface H { run(x: Animal): void }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W061",
"no-barrel-star-export",
Opinionated,
Allow,
"A wildcard barrel export obscures the package's public dependency surface.",
"Re-export the intended names explicitly.",
examples!(
"export * from \"./internal.js\";",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W062",
"no-default-export",
Opinionated,
Allow,
"A default export lets importers rename one public binding arbitrarily.",
"Use a named export.",
examples!(
"export default function run() {}",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W063",
"exhaustive-discriminated-switch",
Opinionated,
Allow,
"A discriminated-union switch omits a reachable variant.",
"Handle every variant and assert never in the default branch.",
examples!(
"type S = { kind: \"a\" } | { kind: \"b\" }; function f(s: S) { switch (s.kind) { case \"a\": break; } }",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W064",
"long-parameter-list",
Opinionated,
Allow,
"A long positional parameter list makes calls easy to misorder.",
"Use a parameter object or smaller cohesive functions.",
examples!(
"function f(a:number,b:number,c:number,d:number,e:number) {}",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W065",
"implicit-return-path",
ControlFlow,
Warn,
"A function can complete without returning the value its signature implies.",
"Return on every reachable path or include undefined in the return type.",
examples!(
"function f(x:boolean){ if(x)return 1 }",
"function f(x:boolean){ if(x)return 0; try { return 1; } catch { return 2; } }"
)
),
rule!(
"BAMTS-W066",
"switch-fallthrough",
ControlFlow,
Warn,
"A non-empty switch case falls through without an explicit transfer.",
"Add break, return, throw, or an explicit fallthrough marker.",
examples!(
"switch(x){case 1: work(); case 2: stop();}",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W067",
"unreachable-code",
ControlFlow,
Warn,
"A statement is unreachable under the program's control flow.",
"Remove it or restructure the surrounding control flow.",
examples!("function f(){ return; work(); }", "const safe: number = 1;")
),
rule!(
"BAMTS-W068",
"unused-label",
ControlFlow,
Warn,
"A label is declared but never targeted, obscuring control flow.",
"Remove the label or add its intended labeled transfer.",
examples!(
"unused: for (;;) { break; }",
"outer: for (;;) { break outer; }"
)
),
rule!(
"BAMTS-W069",
"unused-local",
ControlFlow,
Warn,
"A local binding is never read after declaration.",
"Remove it or use it deliberately.",
examples!(
"function f(){ const x=1; }",
"function f(){ const x=1; return x; }"
)
),
rule!(
"BAMTS-W070",
"unused-parameter",
ControlFlow,
Warn,
"A declared parameter is never read by its function.",
"Remove it or name an intentionally unused protocol parameter clearly.",
examples!(
"function f(unused: number) {}",
"function f(used: number) { return used; }"
)
),
rule!(
"BAMTS-W071",
"invalid-number-formatting-options",
Unsoundness,
Warn,
"Known number-formatting arguments lie outside the ECMAScript-supported range.",
"Validate or clamp the argument before calling the method.",
examples!("(42).toString(1);", "const safe: number = 1;")
),
rule!(
"BAMTS-W072",
"unsound-numeric-key-order-assumption",
Unsoundness,
Warn,
"Integer-like object keys are ordered before other keys, not purely by insertion.",
"Avoid insertion-order dependence or sort the keys explicitly.",
examples!("Object.keys({b: 1, \"2\": 2});", "const safe: number = 1;")
),
rule!(
"BAMTS-W073",
"json-stringify-unserializable-type",
Unsoundness,
Warn,
"JSON.stringify can throw for BigInt or return undefined for a top-level value.",
"Validate serializability and handle the undefined result.",
examples!("JSON.stringify(10n);", "const safe: number = 1;")
),
rule!(
"BAMTS-W074",
"unchecked-json-parse-any",
Unsoundness,
Warn,
"JSON.parse returns untrusted data that is consumed as a trusted type.",
"Parse to unknown and validate with a decoder.",
examples!(
"const u: User = JSON.parse(text);",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W075",
"numeric-array-default-sort",
Unsoundness,
Warn,
"Comparator-free sort coerces elements to strings rather than numeric order.",
"Pass an explicit numeric or domain comparator.",
examples!("[10, 2, 5].sort();", "const safe: number = 1;")
),
rule!(
"BAMTS-W076",
"loose-equality-coercion",
Unsoundness,
Warn,
"Loose equality can depend on implicit abstract coercion.",
"Use strict equality or an explicit conversion.",
examples!("\"0\" == false;", "const safe: number = 1;")
),
rule!(
"BAMTS-W077",
"object-implicit-toprimitive-coercion",
Unsoundness,
Warn,
"Implicit object-to-primitive conversion can call surprising coercion hooks.",
"Call String, Number, or an explicit conversion method.",
examples!("\"key_\" + Object.create(null);", "const safe: number = 1;")
),
rule!(
"BAMTS-W078",
"symbol-template-interpolation-throw",
Unsoundness,
Warn,
"Interpolating a symbol directly into a template literal throws.",
"Wrap it with String or use its description.",
examples!("`ID: ${Symbol(\"x\")}`", "const safe: number = 1;")
),
rule!(
"BAMTS-W079",
"nan-strict-comparison",
Unsoundness,
Warn,
"NaN is never strictly equal to itself, so a direct comparison is ineffective.",
"Use Number.isNaN.",
examples!("if (value === NaN) {}", "const safe: number = 1;")
),
rule!(
"BAMTS-W080",
"unsafe-tostringtag-override",
Unsoundness,
Warn,
"A toStringTag override is not a trustworthy runtime brand.",
"Use a string tag and validate the actual value shape.",
examples!(
"({ [Symbol.toStringTag]: 123 });",
"const safe: number = 1;"
)
),
rule!(
"BAMTS-W081",
"uninitialized-class-field-shadowing",
ClassSemantics,
Allow,
"An uninitialized derived field defines an own property that shadows an inherited accessor.",
"Use `declare`, initialize deliberately, or rename the field.",
examples!(
"class B { get data():number{return 1} } class D extends B { data:number; }",
"class B { get data():number{return 1} } class D extends B { declare data:number; }"
)
),
rule!(
"BAMTS-W082",
"preserve-const-enums-option",
NonErasable,
Warn,
"Preserving const enums retains runtime enum objects while inlining their uses.",
"Disable preserveConstEnums or replace the enum.",
RuleExamples::new(
RuleExampleCase::CompilerOptions(CompilerLintOptions {
preserve_const_enums: true,
..CompilerLintOptions::STANDARD
}),
RuleExampleCase::CompilerOptions(CompilerLintOptions::STANDARD)
)
),
rule!(
"BAMTS-W083",
"emit-decorator-metadata-option",
LegacySyntax,
Warn,
"Emitted decorator metadata couples runtime reflection to compiler type information.",
"Disable metadata emit and provide explicit metadata.",
RuleExamples::new(
RuleExampleCase::CompilerOptions(CompilerLintOptions {
emit_decorator_metadata: true,
..CompilerLintOptions::STANDARD
}),
RuleExampleCase::CompilerOptions(CompilerLintOptions::STANDARD)
)
),
rule!(
"BAMTS-W084",
"legacy-class-field-set-semantics",
ClassSemantics,
Allow,
"Legacy class-field set semantics invoke inherited setters instead of defining fields.",
"Enable standard define semantics.",
RuleExamples::new(
RuleExampleCase::CompilerOptions(CompilerLintOptions {
use_define_for_class_fields: false,
..CompilerLintOptions::STANDARD
}),
RuleExampleCase::CompilerOptions(CompilerLintOptions::STANDARD)
)
),
rule!(
"BAMTS-W085",
"javascript-syntax-rejection",
JavaScriptCompatibility,
Deny,
"TypeScript-only syntax in a JavaScript file violates that file's source dialect.",
"Rename the file to TypeScript or remove the type syntax.",
examples!(
JavaScript,
"interface Point { x: number }",
"const safe = 1;"
)
),
rule!(
"BAMTS-W086",
"cjs-esm-named-export-mismatch",
Modules,
Warn,
"An ESM named import from CommonJS may not exist in its statically detected exports.",
"Use the CommonJS default export or a declared named export.",
RuleExamples::new(
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"import { helper } from './legacy.js'; helper();"
)
.resolving_to(1),
RuleExampleSource::new(ScriptKind::JavaScript, "exports.other = () => 1;")
]),
RuleExampleCase::Program(&[
RuleExampleSource::new(
ScriptKind::TypeScript,
"import { helper } from './legacy.js'; helper();"
)
.resolving_to(1),
RuleExampleSource::new(
ScriptKind::JavaScript,
"function helper() {} module.exports = { helper };"
)
])
)
),
];
#[must_use]
pub fn rule_reference() -> String {
let mut reference = String::from(
"# BamTS strictness rules\n\nThis file is generated from `bamts_compiler::lint::RULES`; do not edit it manually.\n",
);
for rule in RULES {
writeln!(
reference,
"\n## `{}`: `{}`\n\n- Group: `{}`\n- Default level: `{}`\n- Rationale: {}\n- Sound alternative: {}\n- Silence: `{}`\n- Trigger: {}\n- Clean: {}",
rule.code(),
rule.slug(),
rule.group().slug(),
rule.default_level(),
rule.rationale(),
rule.sound_alternative(),
rule.silence_flag(),
render_example(rule.examples().trigger()),
render_example(rule.examples().clean()),
)
.expect("writing to a String cannot fail");
}
reference
}
fn render_example(example: RuleExampleCase) -> String {
match example {
RuleExampleCase::Source(source) => render_example_source(source),
RuleExampleCase::Program(sources) => sources
.iter()
.map(|source| render_example_source(*source))
.collect::<Vec<_>>()
.join("<br>"),
RuleExampleCase::CompilerOptions(options) => format!(
"<code>preserveConstEnums={}, emitDecoratorMetadata={}, useDefineForClassFields={}</code>",
options.preserve_const_enums,
options.emit_decorator_metadata,
options.use_define_for_class_fields,
),
}
}
fn render_example_source(source: RuleExampleSource) -> String {
let escaped = source
.text()
.replace('&', "&")
.replace('<', "<")
.replace('>', ">");
format!("<code>{:?}: {escaped}</code>", source.script_kind())
}
#[must_use]
pub fn rule_by_code(code: &str) -> Option<&'static RuleDefinition> {
RULES.iter().find(|rule| rule.code() == code)
}
#[must_use]
pub fn rule_by_slug(slug: &str) -> Option<&'static RuleDefinition> {
RULES.iter().find(|rule| rule.slug() == slug)
}
#[must_use]
pub fn rule_by_name(name: &str) -> Option<&'static RuleDefinition> {
rule_by_code(name)
.or_else(|| rule_by_slug(name))
.or_else(|| alias_by_name(name).and_then(|alias| rule_by_code(alias.target_code)))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RuleAlias {
alias: &'static str,
target_code: &'static str,
}
impl RuleAlias {
#[must_use]
pub const fn alias(self) -> &'static str {
self.alias
}
#[must_use]
pub const fn target_code(self) -> &'static str {
self.target_code
}
}
pub static RULE_ALIASES: [RuleAlias; 4] = [
RuleAlias {
alias: "any-downcast",
target_code: "BAMTS-W006",
},
RuleAlias {
alias: "excess-property-bypass",
target_code: "BAMTS-W003",
},
RuleAlias {
alias: "unchecked-catch-property-access",
target_code: "BAMTS-W005",
},
RuleAlias {
alias: "dynamic-tuple-out-of-bounds-indexing",
target_code: "BAMTS-W007",
},
];
fn alias_by_name(name: &str) -> Option<RuleAlias> {
RULE_ALIASES
.iter()
.copied()
.find(|alias| alias.alias == name)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RuleTombstone {
code: &'static str,
}
impl RuleTombstone {
#[must_use]
pub const fn code(self) -> &'static str {
self.code
}
}
pub static RULE_TOMBSTONES: [RuleTombstone; 1] = [RuleTombstone { code: "BAMTS-W000" }];
fn is_tombstone(name: &str) -> bool {
RULE_TOMBSTONES.iter().any(|entry| entry.code == name)
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum LintProfile {
#[default]
Default,
Strict,
Pedantic,
}
impl LintProfile {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Default => "default",
Self::Strict => "strict",
Self::Pedantic => "pedantic",
}
}
fn level(self, rule: &RuleDefinition) -> LintLevel {
let strict = match rule.group() {
RuleGroup::Unsoundness
| RuleGroup::EscapeHatches
| RuleGroup::NonErasable
| RuleGroup::LegacySyntax => LintLevel::Deny,
RuleGroup::ClassSemantics => LintLevel::Warn,
RuleGroup::JavaScriptCompatibility if rule.code() != "BAMTS-W085" => LintLevel::Warn,
RuleGroup::EnumSemantics if rule.code() != "BAMTS-W044" => LintLevel::Deny,
_ => rule.default_level(),
};
match self {
Self::Default => rule.default_level(),
Self::Strict => strict,
Self::Pedantic => match rule.group() {
RuleGroup::EscapeHatches => LintLevel::Forbid,
RuleGroup::Opinionated => LintLevel::Warn,
RuleGroup::ClassSemantics | RuleGroup::JavaScriptCompatibility => LintLevel::Deny,
_ => strict,
},
}
}
const fn unknown_level(self) -> LintLevel {
match self {
Self::Default => LintLevel::Warn,
Self::Strict | Self::Pedantic => LintLevel::Deny,
}
}
}
impl FromStr for LintProfile {
type Err = ParseLintProfileError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"default" => Ok(Self::Default),
"strict" => Ok(Self::Strict),
"pedantic" => Ok(Self::Pedantic),
_ => Err(ParseLintProfileError(Arc::from(value))),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseLintProfileError(Arc<str>);
impl fmt::Display for ParseLintProfileError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "unknown lint profile {:?}", self.0)
}
}
impl std::error::Error for ParseLintProfileError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LintSetting {
name: Arc<str>,
level: LintLevel,
source: Arc<str>,
}
impl LintSetting {
#[must_use]
pub fn new(name: impl Into<Arc<str>>, level: LintLevel, source: impl Into<Arc<str>>) -> Self {
Self {
name: name.into(),
level,
source: source.into(),
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn level(&self) -> LintLevel {
self.level
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LintConfig {
groups: Vec<LintSetting>,
rules: Vec<LintSetting>,
}
impl LintConfig {
#[must_use]
pub const fn new(groups: Vec<LintSetting>, rules: Vec<LintSetting>) -> Self {
Self { groups, rules }
}
#[must_use]
pub fn groups(&self) -> &[LintSetting] {
&self.groups
}
#[must_use]
pub fn rules(&self) -> &[LintSetting] {
&self.rules
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OverrideTargetKind {
Group,
Rule,
Either,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LintOverride {
setting: LintSetting,
target_kind: OverrideTargetKind,
}
impl LintOverride {
#[must_use]
pub fn new(name: impl Into<Arc<str>>, level: LintLevel, source: impl Into<Arc<str>>) -> Self {
Self {
setting: LintSetting::new(name, level, source),
target_kind: OverrideTargetKind::Either,
}
}
#[must_use]
pub fn group(group: RuleGroup, level: LintLevel, source: impl Into<Arc<str>>) -> Self {
Self {
setting: LintSetting::new(group.slug(), level, source),
target_kind: OverrideTargetKind::Group,
}
}
#[must_use]
pub fn rule(rule: RuleId, level: LintLevel, source: impl Into<Arc<str>>) -> Self {
Self {
setting: LintSetting::new(rule.code(), level, source),
target_kind: OverrideTargetKind::Rule,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)]
enum Specificity {
Profile,
Group,
Rule,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct AppliedLevel {
level: LintLevel,
source: Arc<str>,
specificity: Specificity,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct RuleState {
profile: AppliedLevel,
group: Option<AppliedLevel>,
rule: Option<AppliedLevel>,
}
impl RuleState {
fn effective(&self) -> &AppliedLevel {
self.rule
.as_ref()
.or(self.group.as_ref())
.unwrap_or(&self.profile)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LintIssueKind {
RenamedRule { canonical: &'static str },
RetiredCode,
UnknownName { suggestion: Option<Arc<str>> },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LintIssue {
name: Arc<str>,
level: LintLevel,
source: Arc<str>,
kind: LintIssueKind,
}
impl LintIssue {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn level(&self) -> LintLevel {
self.level
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
#[must_use]
pub const fn kind(&self) -> &LintIssueKind {
&self.kind
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ForbidOverrideError {
rule: RuleId,
forbidden_by: Arc<str>,
lowered_by: Arc<str>,
}
impl ForbidOverrideError {
#[must_use]
pub const fn rule(&self) -> RuleId {
self.rule
}
#[must_use]
pub fn forbidden_by(&self) -> &str {
&self.forbidden_by
}
#[must_use]
pub fn lowered_by(&self) -> &str {
&self.lowered_by
}
}
impl fmt::Display for ForbidOverrideError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"rule {} ({}) was forbidden by {}; {} cannot lower it",
self.rule.code(),
self.rule.slug(),
self.forbidden_by,
self.lowered_by
)
}
}
impl std::error::Error for ForbidOverrideError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SourceDialect {
TypeScript,
JavaScript,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LintTable {
profile: LintProfile,
states: Vec<RuleState>,
}
impl LintTable {
#[must_use]
pub fn new(profile: LintProfile) -> Self {
let profile_source: Arc<str> = Arc::from(format!("{} profile", profile.as_str()));
let states = RULES
.iter()
.map(|rule| RuleState {
profile: AppliedLevel {
level: profile.level(rule),
source: Arc::clone(&profile_source),
specificity: Specificity::Profile,
},
group: None,
rule: None,
})
.collect();
Self { profile, states }
}
#[must_use]
pub const fn profile(&self) -> LintProfile {
self.profile
}
#[must_use]
pub fn level(&self, rule: RuleId) -> LintLevel {
self.state(rule).effective().level
}
#[must_use]
pub fn source(&self, rule: RuleId) -> &str {
&self.state(rule).effective().source
}
#[must_use]
pub fn level_for_source(&self, rule: RuleId, dialect: SourceDialect) -> LintLevel {
if dialect == SourceDialect::TypeScript {
return self.level(rule);
}
let spec_footgun = matches!(
rule.code(),
"BAMTS-W071"
| "BAMTS-W072"
| "BAMTS-W073"
| "BAMTS-W074"
| "BAMTS-W075"
| "BAMTS-W076"
| "BAMTS-W077"
| "BAMTS-W078"
| "BAMTS-W079"
| "BAMTS-W080"
);
let control_flow = RULES[rule_index(rule)].group() == RuleGroup::ControlFlow;
let javascript_compatibility =
RULES[rule_index(rule)].group() == RuleGroup::JavaScriptCompatibility;
if spec_footgun || control_flow || javascript_compatibility {
let effective = self.level(rule);
return if effective == LintLevel::Allow {
LintLevel::Allow
} else {
LintLevel::Warn
};
}
LintLevel::Allow
}
pub fn apply_config(
&mut self,
config: &LintConfig,
) -> Result<Vec<LintIssue>, ForbidOverrideError> {
let mut issues = Vec::new();
for setting in config.groups() {
self.apply_setting(setting, OverrideTargetKind::Group, &mut issues)?;
}
for setting in config.rules() {
self.apply_setting(setting, OverrideTargetKind::Rule, &mut issues)?;
}
Ok(issues)
}
pub fn apply_cli(
&mut self,
overrides: impl IntoIterator<Item = LintOverride>,
) -> Result<Vec<LintIssue>, ForbidOverrideError> {
let mut issues = Vec::new();
for lint_override in overrides {
self.apply_setting(
&lint_override.setting,
lint_override.target_kind,
&mut issues,
)?;
}
Ok(issues)
}
fn apply_setting(
&mut self,
setting: &LintSetting,
kind: OverrideTargetKind,
issues: &mut Vec<LintIssue>,
) -> Result<(), ForbidOverrideError> {
if kind != OverrideTargetKind::Rule {
if let Ok(group) = RuleGroup::from_str(setting.name()) {
return self.apply_group(group, setting);
}
if kind == OverrideTargetKind::Group {
issues.push(self.unknown_issue(setting, group_suggestion(setting.name())));
return Ok(());
}
}
if is_tombstone(setting.name()) {
issues.push(LintIssue {
name: Arc::clone(&setting.name),
level: LintLevel::Deny,
source: Arc::clone(&setting.source),
kind: LintIssueKind::RetiredCode,
});
return Ok(());
}
if let Some(rule) = rule_by_code(setting.name()).or_else(|| rule_by_slug(setting.name())) {
return self.apply_rule(rule, setting);
}
if let Some(alias) = alias_by_name(setting.name()) {
let rule = rule_by_code(alias.target_code).expect("alias target must be registered");
issues.push(LintIssue {
name: Arc::clone(&setting.name),
level: LintLevel::Warn,
source: Arc::clone(&setting.source),
kind: LintIssueKind::RenamedRule {
canonical: rule.slug(),
},
});
return self.apply_rule(rule, setting);
}
issues.push(self.unknown_issue(setting, rule_suggestion(setting.name())));
Ok(())
}
fn apply_group(
&mut self,
group: RuleGroup,
setting: &LintSetting,
) -> Result<(), ForbidOverrideError> {
let targets: Vec<usize> = RULES
.iter()
.enumerate()
.filter_map(|(index, rule)| (rule.group() == group).then_some(index))
.collect();
self.check_forbid(&targets, setting, Specificity::Group)?;
for index in targets {
self.states[index].group = Some(AppliedLevel {
level: setting.level,
source: Arc::clone(&setting.source),
specificity: Specificity::Group,
});
}
Ok(())
}
fn apply_rule(
&mut self,
rule: &'static RuleDefinition,
setting: &LintSetting,
) -> Result<(), ForbidOverrideError> {
let index = rule_index(rule.id());
self.check_forbid(&[index], setting, Specificity::Rule)?;
self.states[index].rule = Some(AppliedLevel {
level: setting.level,
source: Arc::clone(&setting.source),
specificity: Specificity::Rule,
});
Ok(())
}
fn check_forbid(
&self,
indices: &[usize],
setting: &LintSetting,
specificity: Specificity,
) -> Result<(), ForbidOverrideError> {
if setting.level == LintLevel::Forbid {
return Ok(());
}
for &index in indices {
let active = self.states[index].effective();
if active.level == LintLevel::Forbid && specificity >= active.specificity {
return Err(ForbidOverrideError {
rule: RULES[index].id(),
forbidden_by: Arc::clone(&active.source),
lowered_by: Arc::clone(&setting.source),
});
}
}
Ok(())
}
fn unknown_issue(&self, setting: &LintSetting, suggestion: Option<Arc<str>>) -> LintIssue {
LintIssue {
name: Arc::clone(&setting.name),
level: self.profile.unknown_level(),
source: Arc::clone(&setting.source),
kind: LintIssueKind::UnknownName { suggestion },
}
}
fn state(&self, rule: RuleId) -> &RuleState {
&self.states[rule_index(rule)]
}
}
fn rule_index(id: RuleId) -> usize {
let code = id.code().as_bytes();
let number = usize::from(code[7] - b'0') * 100
+ usize::from(code[8] - b'0') * 10
+ usize::from(code[9] - b'0');
number - 1
}
fn group_suggestion(name: &str) -> Option<Arc<str>> {
nearest_name(name, RuleGroup::ALL.into_iter().map(RuleGroup::slug))
}
fn rule_suggestion(name: &str) -> Option<Arc<str>> {
nearest_name(
name,
RULES
.iter()
.flat_map(|rule| [rule.code(), rule.slug()])
.chain(RULE_ALIASES.iter().map(|alias| alias.alias)),
)
}
fn nearest_name<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<Arc<str>> {
candidates
.map(|candidate| (levenshtein(name, candidate), candidate))
.min_by_key(|(distance, candidate)| (*distance, *candidate))
.map(|(_, candidate)| Arc::from(candidate))
}
fn levenshtein(left: &str, right: &str) -> usize {
let right_chars: Vec<char> = right.chars().collect();
let mut previous: Vec<usize> = (0..=right_chars.len()).collect();
let mut current = vec![0; right_chars.len() + 1];
for (left_index, left_char) in left.chars().enumerate() {
current[0] = left_index + 1;
for (right_index, right_char) in right_chars.iter().enumerate() {
let substitution = previous[right_index] + usize::from(left_char != *right_char);
current[right_index + 1] = (current[right_index] + 1)
.min(previous[right_index + 1] + 1)
.min(substitution);
}
std::mem::swap(&mut previous, &mut current);
}
previous[right_chars.len()]
}
#[cfg(test)]
mod tests {
use super::*;
fn rule(slug: &str) -> RuleId {
rule_by_slug(slug).expect("test rule must exist").id()
}
#[test]
fn registry_is_complete_and_unique() {
assert_eq!(RULES.len(), 86);
for (index, rule) in RULES.iter().enumerate() {
assert!(rule.code().starts_with("BAMTS-W"));
assert!(!rule.slug().is_empty());
assert!(
!RULES[..index]
.iter()
.any(|other| other.code() == rule.code())
);
assert!(
!RULES[..index]
.iter()
.any(|other| other.slug() == rule.slug())
);
assert!(
!RULE_TOMBSTONES
.iter()
.any(|entry| entry.code() == rule.code())
);
}
}
#[test]
fn ordered_overrides_keep_rule_specificity_over_later_group() {
let target = rule("explicit-any");
let mut table = LintTable::new(LintProfile::Default);
table
.apply_cli([
LintOverride::group(
RuleGroup::EscapeHatches,
LintLevel::Deny,
"-D escape-hatches",
),
LintOverride::rule(target, LintLevel::Allow, "-A explicit-any"),
LintOverride::group(
RuleGroup::EscapeHatches,
LintLevel::Warn,
"-W escape-hatches",
),
])
.unwrap();
assert_eq!(table.level(target), LintLevel::Allow);
assert_eq!(table.source(target), "-A explicit-any");
assert_eq!(table.level(rule("implicit-any")), LintLevel::Warn);
}
#[test]
fn later_override_wins_within_the_same_specificity() {
let target = rule("unused-local");
let mut table = LintTable::new(LintProfile::Default);
table
.apply_cli([
LintOverride::rule(target, LintLevel::Deny, "first"),
LintOverride::rule(target, LintLevel::Warn, "second"),
])
.unwrap();
assert_eq!(table.level(target), LintLevel::Warn);
assert_eq!(table.source(target), "second");
}
#[test]
fn forbid_lock_reports_both_sources() {
let target = rule("explicit-any");
let mut table = LintTable::new(LintProfile::Default);
table
.apply_cli([LintOverride::rule(
target,
LintLevel::Forbid,
"security policy",
)])
.unwrap();
let error = table
.apply_cli([LintOverride::rule(
target,
LintLevel::Warn,
"developer flag",
)])
.unwrap_err();
assert_eq!(error.rule(), target);
assert_eq!(error.forbidden_by(), "security policy");
assert_eq!(error.lowered_by(), "developer flag");
}
#[test]
fn profiles_expand_the_settled_families() {
let escape = rule("explicit-any");
let opinionated = rule("prefer-type-alias");
let module_exception = rule("commonjs-in-esm");
let const_enum = rule("const-enum");
assert_eq!(
LintTable::new(LintProfile::Default).level(escape),
LintLevel::Warn
);
assert_eq!(
LintTable::new(LintProfile::Strict).level(escape),
LintLevel::Deny
);
assert_eq!(
LintTable::new(LintProfile::Pedantic).level(escape),
LintLevel::Forbid
);
assert_eq!(
LintTable::new(LintProfile::Default).level(opinionated),
LintLevel::Allow
);
assert_eq!(
LintTable::new(LintProfile::Strict).level(opinionated),
LintLevel::Allow
);
assert_eq!(
LintTable::new(LintProfile::Pedantic).level(opinionated),
LintLevel::Warn
);
assert_eq!(
LintTable::new(LintProfile::Strict).level(module_exception),
LintLevel::Allow
);
assert_eq!(
LintTable::new(LintProfile::Strict).level(const_enum),
LintLevel::Warn
);
assert_eq!(
LintTable::new(LintProfile::Strict).level(rule("runtime-enum")),
LintLevel::Deny
);
}
#[test]
fn aliases_resolve_and_warn_without_losing_the_setting() {
let mut table = LintTable::new(LintProfile::Default);
let issues = table
.apply_cli([LintOverride::new(
"any-downcast",
LintLevel::Deny,
"legacy config",
)])
.unwrap();
assert_eq!(table.level(rule("generic-any-downcast")), LintLevel::Deny);
assert!(matches!(
issues[0].kind(),
LintIssueKind::RenamedRule {
canonical: "generic-any-downcast"
}
));
}
#[test]
fn tombstones_are_rejected() {
let mut table = LintTable::new(LintProfile::Default);
let issues = table
.apply_cli([LintOverride::new("BAMTS-W000", LintLevel::Warn, "config")])
.unwrap();
assert_eq!(issues[0].level(), LintLevel::Deny);
assert_eq!(issues[0].kind(), &LintIssueKind::RetiredCode);
}
#[test]
fn unknown_names_warn_by_default_and_deny_in_stricter_profiles() {
for (profile, level) in [
(LintProfile::Default, LintLevel::Warn),
(LintProfile::Strict, LintLevel::Deny),
(LintProfile::Pedantic, LintLevel::Deny),
] {
let mut table = LintTable::new(profile);
let issues = table
.apply_cli([LintOverride::new("explicit-ang", LintLevel::Warn, "config")])
.unwrap();
assert_eq!(issues[0].level(), level);
assert!(matches!(
issues[0].kind(),
LintIssueKind::UnknownName { suggestion: Some(name) } if name.as_ref() == "explicit-any"
));
}
}
#[test]
fn javascript_dialect_preserves_allow_and_clamps_enabled_rules_to_warning() {
let table = LintTable::new(LintProfile::Pedantic);
assert_eq!(
table.level_for_source(
rule("invalid-number-formatting-options"),
SourceDialect::JavaScript
),
LintLevel::Warn
);
assert_eq!(
table.level_for_source(rule("unused-local"), SourceDialect::JavaScript),
LintLevel::Warn
);
assert_eq!(
table.level_for_source(rule("explicit-any"), SourceDialect::JavaScript),
LintLevel::Allow
);
assert_eq!(
table.level_for_source(
rule("javascript-syntax-rejection"),
SourceDialect::JavaScript
),
LintLevel::Warn
);
assert_eq!(
LintTable::new(LintProfile::Default)
.level_for_source(rule("javascript-input"), SourceDialect::JavaScript,),
LintLevel::Allow
);
}
}