mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
9019d26c3f
Share non-generic array/null/error normalization for `DefaultRuleConfig` and route all 279 call sites through the direct `Value` path. This reduced the local release binary by 66,784 bytes. Validated with `just ready`. Prepared with AI assistance.
975 lines
34 KiB
Rust
975 lines
34 KiB
Rust
#[cfg(feature = "ruledocs")]
|
|
use std::borrow::Cow;
|
|
use std::{fmt, hash::Hash};
|
|
|
|
use schemars::{
|
|
JsonSchema, SchemaGenerator,
|
|
schema::{InstanceType, Schema, SchemaObject},
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use oxc_semantic::AstTypesBitset;
|
|
|
|
use crate::{
|
|
AstNode, FixKind,
|
|
context::{ContextHost, LintContext},
|
|
utils::PossibleJestNode,
|
|
};
|
|
|
|
pub trait Rule: Sized + Default + fmt::Debug {
|
|
/// Initialize from eslint json configuration
|
|
fn from_configuration(_value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
|
|
Ok(Self::default())
|
|
}
|
|
|
|
/// Serialize rule configuration to JSON. Only used for sending rule configurations
|
|
/// to another linter. This allows oxlint to handle the parsing and error handling.
|
|
/// Type-aware rules implemented in tsgolint will need to override this method.
|
|
///
|
|
/// - Returns `None` if no configuration should be serialized (default)
|
|
/// - Returns `Some(Err(_))` if serialization fails
|
|
/// - Returns `Some(Ok(_))` if serialization succeeds
|
|
fn to_configuration(&self) -> Option<Result<serde_json::Value, serde_json::Error>> {
|
|
None
|
|
}
|
|
|
|
#[expect(unused_variables)]
|
|
#[cfg(feature = "ruledocs")]
|
|
fn schema(generator: &mut SchemaGenerator) -> Option<Schema> {
|
|
None
|
|
}
|
|
|
|
/// Visit each AST Node
|
|
#[expect(unused_variables)]
|
|
#[inline]
|
|
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {}
|
|
|
|
/// Run only once. Useful for inspecting scopes and trivias etc.
|
|
#[expect(unused_variables)]
|
|
#[inline]
|
|
fn run_once(&self, ctx: &LintContext) {}
|
|
|
|
/// Run on each Jest node (e.g. `it`, `describe`, `test`, `expect`, etc.).
|
|
/// This is only called if the Jest plugin is enabled and the file is a test file.
|
|
/// It should be used to run rules that are specific to Jest or Vitest.
|
|
#[expect(unused_variables)]
|
|
#[inline]
|
|
fn run_on_jest_node<'a, 'c>(
|
|
&self,
|
|
jest_node: &PossibleJestNode<'a, 'c>,
|
|
ctx: &'c LintContext<'a>,
|
|
) {
|
|
}
|
|
|
|
/// Check if a rule should be run at all.
|
|
///
|
|
/// You usually do not need to implement this function. If you do, use it to
|
|
/// enable rules on a file-by-file basis. Do not check if plugins are
|
|
/// enabled/disabled; this is handled by the [`linter`].
|
|
///
|
|
/// [`linter`]: crate::Linter
|
|
#[expect(unused_variables)]
|
|
#[inline]
|
|
fn should_run(&self, ctx: &ContextHost) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Pretty-print a JSON value and collapse all whitespace runs to single spaces,
|
|
/// for embedding a "received config" snippet in a rule-config deserialization error.
|
|
///
|
|
/// Kept non-generic (and out of the generic `Deserialize` impls below) so the heavy
|
|
/// `serde_json::to_string_pretty` machinery is compiled once instead of being
|
|
/// monomorphized into every rule-config `deserialize` instantiation. Returns `None`
|
|
/// when serialization fails, so callers fall back to the bare error.
|
|
fn compact_json_for_error(value: &serde_json::Value) -> Option<String> {
|
|
serde_json::to_string_pretty(value)
|
|
.ok()
|
|
.map(|value_str| value_str.split_whitespace().collect::<Vec<_>>().join(" "))
|
|
}
|
|
|
|
/// Extract the first item from an ESLint-style rule configuration.
|
|
///
|
|
/// `None` represents a missing configuration (`null` or an empty array). Keeping this helper
|
|
/// non-generic avoids duplicating the array and error handling for every rule configuration type.
|
|
#[inline(never)]
|
|
fn normalize_default_rule_config(
|
|
value: serde_json::Value,
|
|
) -> Result<Option<serde_json::Value>, serde_json::Error> {
|
|
match value {
|
|
serde_json::Value::Array(arr) => Ok(arr.into_iter().next()),
|
|
serde_json::Value::Null => Ok(None),
|
|
_ => Err(<serde_json::Error as serde::de::Error>::custom(
|
|
"Expected array for rule configuration",
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Add the received configuration to a deserialization error.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn default_rule_config_error(
|
|
error: serde_json::Error,
|
|
value: &serde_json::Value,
|
|
) -> serde_json::Error {
|
|
match compact_json_for_error(value) {
|
|
Some(compact) => <serde_json::Error as serde::de::Error>::custom(format!(
|
|
"{error}\n received config: `{compact}`"
|
|
)),
|
|
None => error,
|
|
}
|
|
}
|
|
|
|
/// A wrapper type for deserializing ESLint-style rule configurations.
|
|
///
|
|
/// ESLint configurations are typically arrays where the first element contains
|
|
/// the actual rule configuration. This type automatically extracts and deserializes
|
|
/// that first element. If the array is empty, it uses the default value.
|
|
///
|
|
/// If the configuration is invalid (e.g. does not match the expected type, has a
|
|
/// field that does not exist), it will return a deserialization error that is
|
|
/// handled by the linter and reported to the user.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```ignore
|
|
/// impl Rule for MyRule {
|
|
/// fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
|
|
/// DefaultRuleConfig::<Self>::from_value(value).map(DefaultRuleConfig::into_inner)
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// For rules that take a tuple configuration object, e.g. `["foobar", { "param": true, "other_param": false }]`,
|
|
/// use [`TupleRuleConfig`] instead.
|
|
#[derive(Debug, Clone)]
|
|
pub struct DefaultRuleConfig<T>(T);
|
|
|
|
impl<T> DefaultRuleConfig<T> {
|
|
/// Unwraps the inner configuration value.
|
|
pub fn into_inner(self) -> T {
|
|
self.0
|
|
}
|
|
|
|
/// Deserialize an ESLint-style rule configuration from a JSON value.
|
|
pub(crate) fn from_value(value: serde_json::Value) -> Result<Self, serde_json::Error>
|
|
where
|
|
T: serde::de::DeserializeOwned + Default,
|
|
{
|
|
let config = match normalize_default_rule_config(value)? {
|
|
Some(value) => {
|
|
T::deserialize(&value).map_err(|error| default_rule_config_error(error, &value))?
|
|
}
|
|
None => T::default(),
|
|
};
|
|
|
|
Ok(Self(config))
|
|
}
|
|
}
|
|
|
|
impl<T: Default> Default for DefaultRuleConfig<T> {
|
|
fn default() -> Self {
|
|
Self(T::default())
|
|
}
|
|
}
|
|
|
|
impl<'de, T> serde::Deserialize<'de> for DefaultRuleConfig<T>
|
|
where
|
|
T: serde::de::DeserializeOwned + Default,
|
|
{
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let value = serde_json::Value::deserialize(deserializer)?;
|
|
Self::from_value(value).map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
|
|
/// A wrapper type for deserializing ESLint-style tuple rule configurations.
|
|
///
|
|
/// Some ESLint rules take configurations in tuple form, e.g. `["foo", { "bar": "baz" }]`
|
|
/// where different array elements correspond to different config fields. This type
|
|
/// deserializes the entire array as `T`, which should be a tuple struct with
|
|
/// `#[serde(default)]` to handle partial configurations.
|
|
///
|
|
/// If the configuration is invalid, it will return a deserialization error that is
|
|
/// handled by the linter and reported to the user.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```ignore
|
|
/// #[derive(Debug, Default, Clone, Deserialize)]
|
|
/// #[serde(default)]
|
|
/// pub struct MyTupleRule(EnumOption, ObjectConfig);
|
|
///
|
|
/// impl Rule for MyTupleRule {
|
|
/// fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
|
|
/// serde_json::from_value::<TupleRuleConfig<Self>>(value).map(TupleRuleConfig::into_inner)
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, Clone)]
|
|
pub struct TupleRuleConfig<T>(T);
|
|
|
|
impl<T> TupleRuleConfig<T> {
|
|
/// Unwraps the inner configuration value.
|
|
pub fn into_inner(self) -> T {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl<T: Default> Default for TupleRuleConfig<T> {
|
|
fn default() -> Self {
|
|
Self(T::default())
|
|
}
|
|
}
|
|
|
|
impl<'de, T> serde::Deserialize<'de> for TupleRuleConfig<T>
|
|
where
|
|
T: serde::de::DeserializeOwned + Default,
|
|
{
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
use serde::de::Error;
|
|
|
|
let value = serde_json::Value::deserialize(deserializer)?;
|
|
|
|
if let serde_json::Value::Array(arr) = value {
|
|
let config = if arr.is_empty() {
|
|
T::default()
|
|
} else {
|
|
// Parse the entire array as the tuple configuration.
|
|
let arr_value = serde_json::Value::Array(arr);
|
|
T::deserialize(&arr_value).map_err(|e| {
|
|
match compact_json_for_error(&arr_value) {
|
|
Some(compact) => D::Error::custom(format!("{e}, received `{compact}`")),
|
|
None => D::Error::custom(e),
|
|
}
|
|
})?
|
|
};
|
|
|
|
Ok(TupleRuleConfig(config))
|
|
} else if value == serde_json::Value::Null {
|
|
// Missing configuration (null) is treated as default (no rule options provided)
|
|
Ok(TupleRuleConfig(T::default()))
|
|
} else {
|
|
Err(D::Error::custom("Expected array for rule configuration"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A wrapper type for deserializing ESLint-style mixed tuple rule configurations, where the first configuration object can be skipped.
|
|
///
|
|
/// Some ESLint rules take configurations in tuple form, e.g. `["foo", "qux", { "bar": "baz" }]`
|
|
/// where the first argument is optional and the second argument is the actual configuration object.
|
|
/// This type deserializes the array as a tuple `(Option<T1>, Option<T2>) | (Option<T2>)`.
|
|
#[derive(Debug, Clone)]
|
|
pub struct MixedTupleRuleConfig<T1, T2>(pub T1, pub T2);
|
|
|
|
impl<T1: Default, T2: Default> Default for MixedTupleRuleConfig<T1, T2> {
|
|
fn default() -> Self {
|
|
Self(T1::default(), T2::default())
|
|
}
|
|
}
|
|
|
|
impl<'de, T1, T2> serde::Deserialize<'de> for MixedTupleRuleConfig<T1, T2>
|
|
where
|
|
T1: serde::de::DeserializeOwned + Default,
|
|
T2: serde::de::DeserializeOwned + Default,
|
|
{
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
use serde::de::Error;
|
|
let value = serde_json::Value::deserialize(deserializer)?;
|
|
|
|
if let serde_json::Value::Array(arr) = value {
|
|
let config = match arr.len() {
|
|
0 => Self(T1::default(), T2::default()),
|
|
1 => {
|
|
// If there's only one element, we need to determine whether it's T1 or T2.
|
|
// We can try deserializing it as T2 first, and if that fails, try T1.
|
|
let first_value = &arr[0];
|
|
if let Ok(t2) = T2::deserialize(first_value) {
|
|
Self(T1::default(), t2)
|
|
} else if let Ok(t1) = T1::deserialize(first_value) {
|
|
Self(t1, T2::default())
|
|
} else {
|
|
return Err(D::Error::custom(
|
|
"Expected configuration to match either the first or second tuple type",
|
|
));
|
|
}
|
|
}
|
|
2 => {
|
|
let t1 = T1::deserialize(&arr[0]).map_err(|e| {
|
|
D::Error::custom(format!("Error deserializing first tuple element: {e}"))
|
|
})?;
|
|
let t2 = T2::deserialize(&arr[1]).map_err(|e| {
|
|
D::Error::custom(format!("Error deserializing second tuple element: {e}"))
|
|
})?;
|
|
Self(t1, t2)
|
|
}
|
|
_ => {
|
|
return Err(D::Error::custom(
|
|
"Expected at most 2 elements in the configuration array",
|
|
));
|
|
}
|
|
};
|
|
|
|
Ok(config)
|
|
} else if value == serde_json::Value::Null {
|
|
// Missing configuration (null) is treated as default (no rule options provided)
|
|
Ok(Self(T1::default(), T2::default()))
|
|
} else {
|
|
Err(D::Error::custom("Expected array for rule configuration"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T1, T2> JsonSchema for MixedTupleRuleConfig<T1, T2>
|
|
where
|
|
T1: JsonSchema,
|
|
T2: JsonSchema,
|
|
{
|
|
fn schema_name() -> String {
|
|
"MixedTupleRuleConfig".to_string()
|
|
}
|
|
|
|
fn is_referenceable() -> bool {
|
|
false
|
|
}
|
|
|
|
fn json_schema(r#gen: &mut SchemaGenerator) -> Schema {
|
|
Schema::Object(SchemaObject {
|
|
subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
|
|
any_of: Some(vec![
|
|
SchemaObject {
|
|
instance_type: Some(InstanceType::Array.into()),
|
|
array: Some(Box::new(schemars::schema::ArrayValidation {
|
|
items: Some(schemars::schema::SingleOrVec::Vec(vec![
|
|
r#gen.subschema_for::<T1>(),
|
|
r#gen.subschema_for::<T2>(),
|
|
])),
|
|
min_items: Some(2),
|
|
max_items: Some(2),
|
|
..Default::default()
|
|
})),
|
|
..Default::default()
|
|
}
|
|
.into(),
|
|
r#gen.subschema_for::<T1>(),
|
|
r#gen.subschema_for::<T2>(),
|
|
]),
|
|
..Default::default()
|
|
})),
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
pub trait RuleRunner: Rule {
|
|
/// `AstType`s that this rule acts on, or `None` if the codegen
|
|
/// can't figure it out and the linter should call `run` on every node.
|
|
const NODE_TYPES: Option<&AstTypesBitset>;
|
|
|
|
/// What `Rule` functions are implemented by this `Rule`. For example, if a rule only
|
|
/// implements `run_once`, then the linter can skip calling `run`, so
|
|
/// this value would be tagged as [`RuleRunFunctionsImplemented::RunOnce`].
|
|
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown;
|
|
|
|
fn types_info(&self) -> Option<&'static AstTypesBitset> {
|
|
Self::NODE_TYPES
|
|
}
|
|
|
|
fn run_info(&self) -> RuleRunFunctionsImplemented {
|
|
Self::RUN_FUNCTIONS
|
|
}
|
|
}
|
|
|
|
/// Enum approximating a bitset of which `Rule` functions are implemented.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum RuleRunFunctionsImplemented {
|
|
/// Unknown which functions are implemented.
|
|
Unknown,
|
|
/// Only `run` is implemented
|
|
Run,
|
|
/// Only `run_once` is implemented
|
|
RunOnce,
|
|
/// Only `run_on_jest_node` is implemented
|
|
RunOnJestNode,
|
|
}
|
|
|
|
impl RuleRunFunctionsImplemented {
|
|
pub fn is_run_implemented(self) -> bool {
|
|
matches!(self, Self::Run | Self::Unknown)
|
|
}
|
|
|
|
pub fn is_run_once_implemented(self) -> bool {
|
|
matches!(self, Self::RunOnce | Self::Unknown)
|
|
}
|
|
|
|
pub fn is_run_on_jest_node_implemented(self) -> bool {
|
|
matches!(self, Self::RunOnJestNode | Self::Unknown)
|
|
}
|
|
}
|
|
|
|
pub trait RuleMeta {
|
|
const NAME: &'static str;
|
|
|
|
const PLUGIN: &'static str;
|
|
|
|
const CATEGORY: RuleCategory;
|
|
|
|
const IS_TSGOLINT_RULE: bool = false;
|
|
|
|
/// What kind of auto-fixing can this rule do?
|
|
const FIX: RuleFixMeta = RuleFixMeta::None;
|
|
|
|
fn documentation() -> Option<&'static str> {
|
|
None
|
|
}
|
|
|
|
#[expect(unused_variables)]
|
|
fn config_schema(generator: &mut SchemaGenerator) -> Option<Schema> {
|
|
None
|
|
}
|
|
|
|
/// Whether this rule declares a configuration struct in `declare_oxc_lint!`.
|
|
///
|
|
/// Defaults to `false`. Rules that accept configuration options will have
|
|
/// this set to `true` by the macro-generated impl.
|
|
const HAS_CONFIG: bool = false;
|
|
|
|
/// The version of oxlint in which this rule was first available.
|
|
///
|
|
/// Set via `version = "x.y.z"` or `version = "next"` in `declare_oxc_lint!`.
|
|
const VERSION: &'static str;
|
|
|
|
/// Additional information about the rule.
|
|
///
|
|
/// Set via `short_description = "..."` in `declare_oxc_lint!`.
|
|
const INFO: RuleInfo = RuleInfo { short_description: "" };
|
|
}
|
|
|
|
/// Additional information describing a lint rule.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct RuleInfo {
|
|
/// A short, one-line summary of what the rule does.
|
|
pub short_description: &'static str,
|
|
}
|
|
|
|
/// Rule categories defined by rust-clippy
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum RuleCategory {
|
|
/// Code that is outright wrong or useless
|
|
Correctness,
|
|
/// Code that is most likely wrong or useless
|
|
Suspicious,
|
|
/// Lints which are rather strict or have occasional false positives
|
|
Pedantic,
|
|
/// Code that can be written to run faster
|
|
Perf,
|
|
/// Code that should be written in a more idiomatic way
|
|
Style,
|
|
/// Lints which prevent the use of language and library features
|
|
/// The restriction category should, emphatically, not be enabled as a whole.
|
|
/// The contained lints may lint against perfectly reasonable code, may not have an alternative suggestion,
|
|
/// and may contradict any other lints (including other categories).
|
|
/// Lints should be considered on a case-by-case basis before enabling.
|
|
Restriction,
|
|
/// New lints that are still under development
|
|
Nursery,
|
|
}
|
|
|
|
impl RuleCategory {
|
|
pub fn description(self) -> &'static str {
|
|
match self {
|
|
Self::Correctness => "Code that is outright wrong or useless.",
|
|
Self::Suspicious => "code that is most likely wrong or useless.",
|
|
Self::Pedantic => "Lints which are rather strict or have occasional false positives.",
|
|
Self::Perf => "Code that can be written to run faster.",
|
|
Self::Style => "Code that should be written in a more idiomatic way.",
|
|
Self::Restriction => {
|
|
"Lints which prevent the use of language and library features. Must not be enabled as a whole, should be considered on a case-by-case basis before enabling."
|
|
}
|
|
Self::Nursery => "New lints that are still under development.",
|
|
}
|
|
}
|
|
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Correctness => "correctness",
|
|
Self::Suspicious => "suspicious",
|
|
Self::Pedantic => "pedantic",
|
|
Self::Perf => "perf",
|
|
Self::Style => "style",
|
|
Self::Restriction => "restriction",
|
|
Self::Nursery => "nursery",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for RuleCategory {
|
|
type Error = ();
|
|
|
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
match value {
|
|
"correctness" => Ok(Self::Correctness),
|
|
"suspicious" => Ok(Self::Suspicious),
|
|
"pedantic" => Ok(Self::Pedantic),
|
|
"perf" => Ok(Self::Perf),
|
|
"style" => Ok(Self::Style),
|
|
"restriction" => Ok(Self::Restriction),
|
|
"nursery" => Ok(Self::Nursery),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for RuleCategory {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let category_name = match self {
|
|
Self::Correctness => "Correctness",
|
|
Self::Suspicious => "Suspicious",
|
|
Self::Pedantic => "Pedantic",
|
|
Self::Perf => "Perf",
|
|
Self::Style => "Style",
|
|
Self::Restriction => "Restriction",
|
|
Self::Nursery => "Nursery",
|
|
};
|
|
f.write_str(category_name)
|
|
}
|
|
}
|
|
|
|
// NOTE: this could be packed into a single byte if we wanted. I don't think
|
|
// this is needed, but we could do it if it would have a performance impact.
|
|
/// Describes the auto-fixing capabilities of a `Rule`.
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum RuleFixMeta {
|
|
/// An auto-fix is not available.
|
|
#[default]
|
|
None,
|
|
/// An auto-fix could be implemented, but it has not been yet.
|
|
FixPending,
|
|
/// An auto-fix is available for some violations, but not all.
|
|
Conditional(FixKind),
|
|
/// An auto-fix is available.
|
|
Fixable(FixKind),
|
|
}
|
|
|
|
impl RuleFixMeta {
|
|
#[inline]
|
|
pub fn is_none(self) -> bool {
|
|
matches!(self, Self::None)
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn fix_kind(self) -> FixKind {
|
|
match self {
|
|
Self::Conditional(kind) | Self::Fixable(kind) => {
|
|
debug_assert!(
|
|
!kind.is_none(),
|
|
"This lint rule indicates that it provides an auto-fix but its FixKind is None. This is a bug. If this rule does not provide a fix, please use RuleFixMeta::None. Otherwise, please provide a valid FixKind"
|
|
);
|
|
kind
|
|
}
|
|
RuleFixMeta::None | RuleFixMeta::FixPending => FixKind::None,
|
|
}
|
|
}
|
|
|
|
/// Does this `Rule` have some kind of auto-fix available?
|
|
///
|
|
/// Also returns `true` for suggestions.
|
|
#[inline]
|
|
pub fn has_fix(self) -> bool {
|
|
matches!(self, Self::Fixable(_) | Self::Conditional(_))
|
|
}
|
|
|
|
pub fn supports_fix(self, kind: FixKind) -> bool {
|
|
matches!(self, Self::Fixable(fix_kind) | Self::Conditional(fix_kind) if fix_kind.can_apply(kind))
|
|
}
|
|
|
|
#[cfg(feature = "ruledocs")]
|
|
pub fn description(self) -> Cow<'static, str> {
|
|
match self {
|
|
Self::None => Cow::Borrowed("No auto-fix is available for this rule."),
|
|
Self::FixPending => Cow::Borrowed(
|
|
"An auto-fix is planned for this rule, but not implemented at this time.",
|
|
),
|
|
Self::Fixable(kind) | Self::Conditional(kind) => {
|
|
// e.g. an auto-fix is available for this rule
|
|
// e.g. a suggestion is available for this rule
|
|
// e.g. a dangerous auto-fix is available for this rule
|
|
// e.g. an auto-fix is available for this rule for some violations
|
|
// e.g. an auto-fix and a suggestion are available for this rule
|
|
let noun = match (kind.contains(FixKind::Fix), kind.contains(FixKind::Suggestion)) {
|
|
(true, true) => "auto-fix and a suggestion are available for this rule",
|
|
(true, false) => "auto-fix is available for this rule",
|
|
(false, true) => "suggestion is available for this rule",
|
|
_ => unreachable!(
|
|
"Fix kinds must contain Fix and/or Suggestion, but {self:?} has neither."
|
|
),
|
|
};
|
|
let mut message =
|
|
if kind.is_dangerous() { format!("dangerous {noun}") } else { noun.into() };
|
|
|
|
let article = match message.chars().next() {
|
|
Some('a' | 'e' | 'i' | 'o' | 'u') => "An",
|
|
Some(_) => "A",
|
|
None => unreachable!(),
|
|
};
|
|
|
|
if matches!(self, Self::Conditional(_)) {
|
|
message += " for some violations";
|
|
}
|
|
|
|
Cow::Owned(format!("{article} {message}."))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn emoji(self) -> Option<&'static str> {
|
|
match self {
|
|
Self::None => None,
|
|
Self::Conditional(kind) | Self::Fixable(kind) => Some(kind.emoji()),
|
|
Self::FixPending => Some("🚧"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for RuleFixMeta {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::None => write!(f, "none"),
|
|
Self::FixPending => write!(f, "pending"),
|
|
Self::Conditional(kind) => write!(f, "conditional_{}", kind.to_string()),
|
|
Self::Fixable(kind) => write!(f, "fixable_{}", kind.to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RuleFixMeta> for FixKind {
|
|
fn from(value: RuleFixMeta) -> Self {
|
|
value.fix_kind()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::{
|
|
RuleMeta, RuleRunner,
|
|
rule::{DefaultRuleConfig, TupleRuleConfig},
|
|
};
|
|
|
|
use super::RuleCategory;
|
|
use rustc_hash::FxHashMap;
|
|
|
|
#[test]
|
|
#[cfg(feature = "ruledocs")]
|
|
fn ensure_documentation() {
|
|
use crate::rules::RULES;
|
|
use markdown::{Options, to_html_with_options};
|
|
|
|
assert!(!RULES.is_empty());
|
|
let options = Options::gfm();
|
|
|
|
for rule in RULES.iter() {
|
|
let name = rule.name();
|
|
assert!(
|
|
rule.documentation().is_some_and(|s| !s.is_empty()),
|
|
"Rule '{name}' is missing documentation."
|
|
);
|
|
// will panic if provided invalid markdown
|
|
let html = to_html_with_options(rule.documentation().unwrap(), &options).unwrap();
|
|
assert!(!html.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_rule_category() {
|
|
let tests = [
|
|
("correctness", RuleCategory::Correctness),
|
|
("suspicious", RuleCategory::Suspicious),
|
|
("restriction", RuleCategory::Restriction),
|
|
("perf", RuleCategory::Perf),
|
|
("pedantic", RuleCategory::Pedantic),
|
|
("style", RuleCategory::Style),
|
|
("nursery", RuleCategory::Nursery),
|
|
];
|
|
|
|
for (input, expected) in tests {
|
|
let de: RuleCategory = serde_json::from_str(&format!("{input:?}")).unwrap();
|
|
// deserializes to expected value
|
|
assert_eq!(de, expected, "{input}");
|
|
// try_from on a str produces the same value as deserializing
|
|
assert_eq!(de, RuleCategory::try_from(input).unwrap(), "{input}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_rule_runner_impls() {
|
|
use crate::rules::*;
|
|
use oxc_ast::AstType::*;
|
|
|
|
// The RuleRunner code is automatically generated by the `oxc_linter_codegen` crate.
|
|
// This is set of manually verified test cases to ensure that the generated code
|
|
// is working as expected and is not skipping rules for nodes that actually should be linted.
|
|
assert_rule_runs_on_node_types(&eslint::no_debugger::NoDebugger, &[DebuggerStatement]);
|
|
assert_rule_runs_on_node_types(&eslint::no_with::NoWith, &[WithStatement]);
|
|
assert_rule_runs_on_node_types(
|
|
&eslint::arrow_body_style::ArrowBodyStyle::default(),
|
|
&[ArrowFunctionExpression],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&eslint::no_else_return::NoElseReturn::default(),
|
|
&[IfStatement],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&eslint::max_params::MaxParams::default(),
|
|
&[Function, ArrowFunctionExpression, TSFunctionType],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&import::no_dynamic_require::NoDynamicRequire::default(),
|
|
&[ImportExpression, CallExpression],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&jest::prefer_jest_mocked::PreferJestMocked,
|
|
&[TSAsExpression, TSTypeAssertion],
|
|
);
|
|
assert_rule_runs_on_node_types(&jest::prefer_spy_on::PreferSpyOn, &[AssignmentExpression]);
|
|
assert_rule_runs_on_node_types(
|
|
&jsx_a11y::anchor_is_valid::AnchorIsValid::default(),
|
|
&[JSXElement],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&jsx_a11y::aria_activedescendant_has_tabindex::AriaActivedescendantHasTabindex,
|
|
&[JSXOpeningElement],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&nextjs::no_head_element::NoHeadElement,
|
|
&[JSXOpeningElement],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&nextjs::google_font_display::GoogleFontDisplay,
|
|
&[JSXOpeningElement],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&unicorn::consistent_assert::ConsistentAssert,
|
|
&[ImportDeclaration],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&promise::always_return::AlwaysReturn::default(),
|
|
&[Function, ArrowFunctionExpression],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&vue::max_props::MaxProps::default(),
|
|
&[CallExpression, ExportDefaultDeclaration],
|
|
);
|
|
assert_rule_runs_on_node_types(
|
|
&eslint::prefer_named_capture_group::PreferNamedCaptureGroup,
|
|
&[RegExpLiteral, CallExpression, NewExpression],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_default_rule_config_single() {
|
|
// single element present
|
|
assert_default_rule_config("[123]", &123u32);
|
|
assert_default_rule_config("[true]", &true);
|
|
assert_default_rule_config("[false]", &false);
|
|
|
|
// empty array should use defaults
|
|
assert_default_rule_config("[]", &String::default());
|
|
}
|
|
|
|
#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
|
|
#[serde(default)]
|
|
struct Obj {
|
|
foo: String,
|
|
}
|
|
|
|
impl Default for Obj {
|
|
fn default() -> Self {
|
|
Self { foo: "defaultval".to_string() }
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
|
|
#[serde(default)]
|
|
struct Pair(u32, Obj);
|
|
|
|
impl Default for Pair {
|
|
fn default() -> Self {
|
|
Self(123u32, Obj::default())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_tuple_rule_config() {
|
|
// both elements present
|
|
assert_tuple_rule_config(
|
|
r#"[42, { "foo": "abc" }]"#,
|
|
&Pair(42u32, Obj { foo: "abc".to_string() }),
|
|
);
|
|
|
|
// only first element present -> Since Pair has #[serde(default)],
|
|
// serde will use the default value for the missing second field.
|
|
assert_tuple_rule_config("[10]", &Pair(10u32, Obj { foo: "defaultval".to_string() }));
|
|
|
|
// Should also be able to handle both elements if they are both passed
|
|
assert_tuple_rule_config(
|
|
r#"[10, { "foo": "bar" }]"#,
|
|
&Pair(10u32, Obj { foo: "bar".to_string() }),
|
|
);
|
|
|
|
// empty array -> both default
|
|
assert_tuple_rule_config("[]", &Pair(123u32, Obj { foo: "defaultval".to_string() }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_default_rule_config_object_in_array() {
|
|
// Single-element array containing an object should parse into the object
|
|
// configuration (fallback behavior, not the "entire-array as T" path).
|
|
assert_default_rule_config(r#"[{ "foo": "xyz" }]"#, &Obj { foo: "xyz".to_string() });
|
|
|
|
// Empty array -> default
|
|
assert_default_rule_config("[]", &Obj { foo: "defaultval".to_string() });
|
|
}
|
|
|
|
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Default)]
|
|
#[serde(default)]
|
|
struct ComplexConfig {
|
|
foo: FxHashMap<String, String>,
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_default_rule_config_with_complex_shape() {
|
|
// A complex object shape for the rule config, like
|
|
// `[ { "foo": { "obj": "value" } } ]`.
|
|
assert_default_rule_config(
|
|
r#"[ { "foo": { "obj": "value" } } ]"#,
|
|
&ComplexConfig {
|
|
foo: std::iter::once(("obj".to_string(), "value".to_string())).collect(),
|
|
},
|
|
);
|
|
}
|
|
|
|
#[derive(serde::Deserialize, Debug, PartialEq, Eq, Default)]
|
|
#[serde(rename_all = "camelCase")]
|
|
enum EnumOptions {
|
|
#[default]
|
|
OptionA,
|
|
OptionB,
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_default_rule_config_with_enum_config() {
|
|
// A basic enum config option.
|
|
assert_default_rule_config(r#"["optionA"]"#, &EnumOptions::OptionA);
|
|
|
|
// Works with non-default value as well.
|
|
assert_default_rule_config(r#"["optionB"]"#, &EnumOptions::OptionB);
|
|
|
|
// Works with an empty array.
|
|
assert_default_rule_config(r"[]", &EnumOptions::OptionA);
|
|
}
|
|
|
|
#[derive(serde::Deserialize, Default, Debug, PartialEq, Eq)]
|
|
#[serde(default)]
|
|
struct TupleWithEnumAndObjectConfig(EnumOptions, Obj);
|
|
|
|
#[test]
|
|
fn test_deserialize_tuple_rule_config_with_enum_and_object() {
|
|
// A basic enum config option with an object.
|
|
assert_tuple_rule_config(
|
|
r#"["optionA", { "foo": "bar" }]"#,
|
|
&TupleWithEnumAndObjectConfig(EnumOptions::OptionA, Obj { foo: "bar".to_string() }),
|
|
);
|
|
|
|
// Ensure that we can pass just one value and it'll provide the default for the second.
|
|
assert_tuple_rule_config(
|
|
r#"["optionB"]"#,
|
|
&TupleWithEnumAndObjectConfig(
|
|
EnumOptions::OptionB,
|
|
Obj { foo: "defaultval".to_string() },
|
|
),
|
|
);
|
|
}
|
|
|
|
#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
|
|
#[serde(default)]
|
|
struct ExampleObjConfig {
|
|
baz: String,
|
|
qux: bool,
|
|
}
|
|
|
|
impl Default for ExampleObjConfig {
|
|
fn default() -> Self {
|
|
Self { baz: "defaultbaz".to_string(), qux: false }
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_default_rule_with_object_with_multiple_fields() {
|
|
// Test a rule config that is a simple object with multiple fields.
|
|
assert_default_rule_config(
|
|
r#"[{ "baz": "fooval", "qux": true }]"#,
|
|
&ExampleObjConfig { baz: "fooval".to_string(), qux: true },
|
|
);
|
|
|
|
// Ensure that missing fields get their default values.
|
|
assert_default_rule_config(
|
|
r#"[{ "qux": true }]"#,
|
|
&ExampleObjConfig { baz: "defaultbaz".to_string(), qux: true },
|
|
);
|
|
}
|
|
|
|
// Ensure that the provided JSON deserializes into the expected value with DefaultRuleConfig.
|
|
fn assert_default_rule_config<T>(json: &str, expected: &T)
|
|
where
|
|
T: serde::de::DeserializeOwned + Default + PartialEq + std::fmt::Debug,
|
|
{
|
|
let de: DefaultRuleConfig<T> = serde_json::from_str(json).unwrap();
|
|
assert_eq!(de.into_inner(), *expected);
|
|
}
|
|
|
|
// Ensure that the provided JSON deserializes into the expected value with TupleRuleConfig.
|
|
fn assert_tuple_rule_config<T>(json: &str, expected: &T)
|
|
where
|
|
T: serde::de::DeserializeOwned + Default + PartialEq + std::fmt::Debug,
|
|
{
|
|
let de: TupleRuleConfig<T> = serde_json::from_str(json).unwrap();
|
|
assert_eq!(de.into_inner(), *expected);
|
|
}
|
|
|
|
fn assert_rule_runs_on_node_types<R: RuleMeta + RuleRunner>(
|
|
rule: &R,
|
|
node_types: &[oxc_ast::AstType],
|
|
) {
|
|
let types = rule.types_info();
|
|
assert!(types.is_some(), "{}: NODE_TYPES is None", R::NAME);
|
|
let types = types.unwrap();
|
|
for node_type in node_types {
|
|
assert!(
|
|
types.has(*node_type),
|
|
"{}: missing {:?} in its NODE_TYPES (this means it will incorrectly skip nodes it needs to lint)",
|
|
R::NAME,
|
|
node_type
|
|
);
|
|
}
|
|
for node_type in types {
|
|
assert!(
|
|
node_types.contains(&node_type),
|
|
"{}: has {:?} in its NODE_TYPES but it should not (this means it will lint nodes it does not need to)",
|
|
R::NAME,
|
|
node_type
|
|
);
|
|
}
|
|
}
|
|
}
|