mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
refactor(linter/config): Use serde::Deserialize for config parsing (#2325)
Fixes #2258 ### Overview - Re-implemented the config parser to use `serde::Deserialize` - In order to benefit from it as much as possible, avoided implementing custom deserializers and tried to use attributes as much as possible - This required some changes to the caller signatures... ➕ - Fixed a bug that did not support for abbreviations like `"rule-name": 1` - Fixed settings that should have been located in `settings.react` but were not
This commit is contained in:
parent
f3470163d9
commit
63b4741ff3
15 changed files with 479 additions and 468 deletions
|
|
@ -34,7 +34,7 @@ oxc_resolver = { version = "1.3.0" }
|
|||
rayon = { workspace = true }
|
||||
lazy_static = { workspace = true } # used in oxc_macros
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
phf = { workspace = true, features = ["macros"] }
|
||||
|
|
|
|||
|
|
@ -1,27 +1,58 @@
|
|||
use std::{self, ops::Deref};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Environment
|
||||
/// https://eslint.org/docs/latest/use/configure/language-options#using-configuration-files
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ESLintEnv(Vec<String>);
|
||||
///
|
||||
/// TS type is `Record<string, boolean>`
|
||||
/// https://github.com/eslint/eslint/blob/ce838adc3b673e52a151f36da0eedf5876977514/lib/shared/types.js#L40
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ESLintEnv(FxHashMap<String, bool>);
|
||||
|
||||
impl ESLintEnv {
|
||||
pub fn new(env: Vec<String>) -> Self {
|
||||
Self(env)
|
||||
pub fn from_vec(env: Vec<String>) -> Self {
|
||||
let map = env.into_iter().map(|key| (key, true)).collect();
|
||||
|
||||
Self(map)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &str> + '_ {
|
||||
// Filter out false values
|
||||
self.0.iter().filter(|(_, v)| **v).map(|(k, _)| k.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// The `env` field from ESLint config
|
||||
impl Default for ESLintEnv {
|
||||
fn default() -> Self {
|
||||
Self(vec!["builtin".to_string()])
|
||||
let mut map = FxHashMap::default();
|
||||
map.insert("builtin".to_string(), true);
|
||||
|
||||
Self(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ESLintEnv {
|
||||
type Target = Vec<String>;
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ESLintEnv;
|
||||
use itertools::Itertools;
|
||||
use serde::Deserialize;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
#[test]
|
||||
fn test_parse_env() {
|
||||
let env = ESLintEnv::deserialize(&serde_json::json!({
|
||||
"browser": true, "node": true, "es6": false
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(env.iter().count(), 2);
|
||||
assert!(env.iter().contains(&"browser"));
|
||||
assert!(env.iter().contains(&"node"));
|
||||
assert!(!env.iter().contains(&"es6"));
|
||||
assert!(!env.iter().contains(&"builtin"));
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_env_default() {
|
||||
let env = ESLintEnv::default();
|
||||
assert_eq!(env.iter().count(), 1);
|
||||
assert!(env.iter().contains(&"builtin"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ pub struct FailedToParseConfigJsonError(pub PathBuf, pub String);
|
|||
pub struct FailedToParseConfigError(#[related] pub Vec<Report>);
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("Failed to parse config at {0:?} with error {1:?}")]
|
||||
#[error("Failed to parse config with error {0:?}")]
|
||||
#[diagnostic()]
|
||||
pub struct FailedToParseConfigPropertyError(pub &'static str, pub &'static str);
|
||||
pub struct FailedToParseConfigPropertyError(pub String);
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("Failed to rule value {0:?} with error {1:?}")]
|
||||
|
|
|
|||
|
|
@ -1,44 +1,34 @@
|
|||
mod env;
|
||||
pub mod errors;
|
||||
mod rules;
|
||||
mod settings;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use oxc_diagnostics::{Error, FailedToOpenFileError, Report};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use serde_json::{Map, Value};
|
||||
use rustc_hash::FxHashSet;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{rules::RuleEnum, AllowWarnDeny};
|
||||
|
||||
pub use self::{
|
||||
env::ESLintEnv,
|
||||
settings::{ESLintSettings, JsxA11y, Nextjs},
|
||||
};
|
||||
use self::{
|
||||
errors::{
|
||||
FailedToParseConfigError, FailedToParseConfigJsonError, FailedToParseJsonc,
|
||||
FailedToParseRuleValueError,
|
||||
},
|
||||
settings::CustomComponents,
|
||||
use self::errors::{
|
||||
FailedToParseConfigError, FailedToParseConfigJsonError, FailedToParseConfigPropertyError,
|
||||
FailedToParseJsonc,
|
||||
};
|
||||
pub use self::{env::ESLintEnv, rules::ESLintRules, settings::ESLintSettings};
|
||||
|
||||
/// ESLint Config
|
||||
/// <https://eslint.org/docs/latest/use/configure/configuration-files-new#configuration-objects>
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ESLintConfig {
|
||||
rules: Vec<ESLintRuleConfig>,
|
||||
#[serde(default)]
|
||||
rules: ESLintRules,
|
||||
#[serde(default)]
|
||||
settings: ESLintSettings,
|
||||
#[serde(default)]
|
||||
env: ESLintEnv,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ESLintRuleConfig {
|
||||
plugin_name: String,
|
||||
rule_name: String,
|
||||
severity: AllowWarnDeny,
|
||||
config: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ESLintConfig {
|
||||
pub fn from_file(path: &Path) -> Result<Self, Report> {
|
||||
let mut string = std::fs::read_to_string(path).map_err(|e| {
|
||||
|
|
@ -67,18 +57,15 @@ impl ESLintConfig {
|
|||
))])
|
||||
})?;
|
||||
|
||||
let config = Self::from_value(&json)?;
|
||||
let config = Self::deserialize(&json).map_err(|err| {
|
||||
FailedToParseConfigError(vec![Error::new(FailedToParseConfigPropertyError(
|
||||
err.to_string(),
|
||||
))])
|
||||
})?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn from_value(value: &Value) -> Result<Self, Report> {
|
||||
let rules = parse_rules(value)?;
|
||||
let settings = parse_settings_from_root(value);
|
||||
let env = parse_env_from_root(value);
|
||||
|
||||
Ok(Self { rules, settings, env })
|
||||
}
|
||||
|
||||
pub fn properties(self) -> (ESLintSettings, ESLintEnv) {
|
||||
(self.settings, self.env)
|
||||
}
|
||||
|
|
@ -150,284 +137,37 @@ impl ESLintConfig {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_rules(root_json: &Value) -> Result<Vec<ESLintRuleConfig>, Error> {
|
||||
let Value::Object(rules_object) = root_json else { return Ok(Vec::default()) };
|
||||
|
||||
let Some(Value::Object(rules_object)) = rules_object.get("rules") else {
|
||||
return Ok(Vec::default());
|
||||
};
|
||||
|
||||
rules_object
|
||||
.into_iter()
|
||||
.map(|(key, value)| {
|
||||
let (plugin_name, rule_name) = parse_rule_name(key);
|
||||
let (severity, config) = resolve_rule_value(value)?;
|
||||
Ok(ESLintRuleConfig {
|
||||
plugin_name: plugin_name.to_string(),
|
||||
rule_name: rule_name.to_string(),
|
||||
severity,
|
||||
config,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()
|
||||
}
|
||||
|
||||
fn parse_settings_from_root(root_json: &Value) -> ESLintSettings {
|
||||
let Value::Object(root_object) = root_json else { return ESLintSettings::default() };
|
||||
|
||||
let Some(settings_value) = root_object.get("settings") else {
|
||||
return ESLintSettings::default();
|
||||
};
|
||||
|
||||
parse_settings(settings_value)
|
||||
}
|
||||
|
||||
pub fn parse_settings(setting_value: &Value) -> ESLintSettings {
|
||||
if let Value::Object(settings_object) = setting_value {
|
||||
let mut jsx_a11y_setting = JsxA11y::new(None, FxHashMap::default());
|
||||
let mut nextjs_setting = Nextjs::new(vec![]);
|
||||
if let Some(Value::Object(jsx_a11y)) = settings_object.get("jsx-a11y") {
|
||||
if let Some(Value::Object(components)) = jsx_a11y.get("components") {
|
||||
let components_map: FxHashMap<String, String> = components
|
||||
.iter()
|
||||
.map(|(key, value)| (String::from(key), String::from(value.as_str().unwrap())))
|
||||
.collect();
|
||||
|
||||
jsx_a11y_setting.set_components(components_map);
|
||||
}
|
||||
|
||||
if let Some(Value::String(polymorphic_prop_name)) = jsx_a11y.get("polymorphicPropName")
|
||||
{
|
||||
jsx_a11y_setting
|
||||
.set_polymorphic_prop_name(Some(String::from(polymorphic_prop_name)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Value::Object(nextjs)) = settings_object.get("next") {
|
||||
if let Some(Value::String(root_dir)) = nextjs.get("rootDir") {
|
||||
nextjs_setting.set_root_dir(vec![String::from(root_dir)]);
|
||||
}
|
||||
if let Some(Value::Array(root_dir)) = nextjs.get("rootDir") {
|
||||
nextjs_setting.set_root_dir(
|
||||
root_dir.iter().map(|v| v.as_str().unwrap().to_string()).collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let link_components_setting =
|
||||
parse_custom_components(settings_object, &CustomComponentEnum::LinkComponents);
|
||||
let form_components_setting =
|
||||
parse_custom_components(settings_object, &CustomComponentEnum::FormComponents);
|
||||
|
||||
return ESLintSettings::new(
|
||||
jsx_a11y_setting,
|
||||
nextjs_setting,
|
||||
// TODO: These should be inside of react_setting
|
||||
link_components_setting,
|
||||
form_components_setting,
|
||||
);
|
||||
}
|
||||
|
||||
ESLintSettings::default()
|
||||
}
|
||||
|
||||
enum CustomComponentEnum {
|
||||
LinkComponents,
|
||||
FormComponents,
|
||||
}
|
||||
|
||||
fn parse_custom_components(
|
||||
settings_object: &Map<String, Value>,
|
||||
components_type: &CustomComponentEnum,
|
||||
) -> CustomComponents {
|
||||
fn parse_obj(obj: &Map<String, Value>, attribute_name: &str, setting: &mut CustomComponents) {
|
||||
if let Some(Value::String(name)) = obj.get("name") {
|
||||
let mut arr: Vec<String> = vec![];
|
||||
if let Some(Value::String(attribute)) = obj.get(attribute_name) {
|
||||
arr.push(attribute.to_string());
|
||||
} else if let Some(Value::Array(attributes)) = obj.get(attribute_name) {
|
||||
for attribute in attributes {
|
||||
if let Value::String(attribute) = attribute {
|
||||
arr.push(attribute.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
setting.insert(name.to_string(), arr);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_component(
|
||||
settings_object: &Map<String, Value>,
|
||||
component_name: &str,
|
||||
attribute_name: &str,
|
||||
setting: &mut CustomComponents,
|
||||
) {
|
||||
match settings_object.get(component_name) {
|
||||
Some(Value::Array(component)) => {
|
||||
for component in component {
|
||||
if let Value::String(name) = component {
|
||||
setting.insert(name.to_string(), [].to_vec());
|
||||
continue;
|
||||
}
|
||||
if let Value::Object(obj) = component {
|
||||
parse_obj(obj, attribute_name, setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Value::Object(obj)) => {
|
||||
parse_obj(obj, attribute_name, setting);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
let mut setting: CustomComponents = FxHashMap::default();
|
||||
|
||||
match components_type {
|
||||
CustomComponentEnum::FormComponents => {
|
||||
parse_component(settings_object, "formComponents", "formAttribute", &mut setting);
|
||||
}
|
||||
CustomComponentEnum::LinkComponents => {
|
||||
parse_component(settings_object, "linkComponents", "linkAttribute", &mut setting);
|
||||
}
|
||||
}
|
||||
setting
|
||||
}
|
||||
|
||||
fn parse_env_from_root(root_json: &Value) -> ESLintEnv {
|
||||
let Value::Object(root_object) = root_json else { return ESLintEnv::default() };
|
||||
|
||||
let Some(env_value) = root_object.get("env") else { return ESLintEnv::default() };
|
||||
|
||||
let env_object = match env_value {
|
||||
Value::Object(env_object) => env_object,
|
||||
_ => return ESLintEnv::default(),
|
||||
};
|
||||
|
||||
let mut result = vec![];
|
||||
for (k, v) in env_object {
|
||||
if let Value::Bool(v) = v {
|
||||
if *v {
|
||||
result.push(String::from(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ESLintEnv::new(result)
|
||||
}
|
||||
|
||||
fn parse_rule_name(name: &str) -> (&str, &str) {
|
||||
if let Some((category, name)) = name.split_once('/') {
|
||||
let category = category.trim_start_matches('@');
|
||||
|
||||
let category = match category {
|
||||
// if it matches typescript-eslint, map it to typescript
|
||||
"typescript-eslint" => "typescript",
|
||||
// plugin name in RuleEnum is in snake_case
|
||||
"jsx-a11y" => "jsx_a11y",
|
||||
"next" => "nextjs",
|
||||
_ => category,
|
||||
};
|
||||
|
||||
// since next.js eslint rule starts with @next/next/
|
||||
let name = name.trim_start_matches("next/");
|
||||
|
||||
(category, name)
|
||||
} else {
|
||||
("eslint", name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the level of a rule and its config
|
||||
///
|
||||
/// Three cases here
|
||||
/// ```json
|
||||
/// {
|
||||
/// "rule": "off",
|
||||
/// "rule": ["off", "config"],
|
||||
/// "rule": ["off", "config1", "config2"],
|
||||
/// }
|
||||
/// ```
|
||||
fn resolve_rule_value(value: &serde_json::Value) -> Result<(AllowWarnDeny, Option<Value>), Error> {
|
||||
if let Some(v) = value.as_str() {
|
||||
return Ok((AllowWarnDeny::try_from(v)?, None));
|
||||
}
|
||||
|
||||
if let Some(v) = value.as_array() {
|
||||
let mut config = Vec::new();
|
||||
for item in v.iter().skip(1).take(2) {
|
||||
config.push(item.clone());
|
||||
}
|
||||
let config = if config.is_empty() { None } else { Some(Value::Array(config)) };
|
||||
if let Some(v_idx_0) = v.first() {
|
||||
return Ok((AllowWarnDeny::try_from(v_idx_0)?, config));
|
||||
}
|
||||
}
|
||||
|
||||
Err(FailedToParseRuleValueError(value.to_string(), "Invalid rule value").into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ESLintConfig;
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_file() {
|
||||
fn test_from_file() {
|
||||
let fixture_path = env::current_dir().unwrap().join("fixtures/eslint_config.json");
|
||||
let config = ESLintConfig::from_file(&fixture_path).unwrap();
|
||||
assert!(!config.rules.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_value() {
|
||||
let config = ESLintConfig::from_value(&serde_json::json!({
|
||||
"rules": { "no-console": "off" }
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(!config.rules.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rules() {
|
||||
// TODO: Should support `"xxx": 0` form(only `"xxx": [0]` is supported)
|
||||
let config = ESLintConfig::from_value(&serde_json::json!({
|
||||
fn test_deserialize() {
|
||||
let config = ESLintConfig::deserialize(&serde_json::json!({
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"foo/no-unused-vars": [1],
|
||||
"dummy": ["error", "arg1", "args2"],
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let mut rules = config.rules.iter();
|
||||
|
||||
let r1 = rules.next().unwrap();
|
||||
assert_eq!(r1.rule_name, "no-console");
|
||||
assert_eq!(r1.plugin_name, "eslint");
|
||||
assert!(r1.severity.is_allow());
|
||||
assert!(r1.config.is_none());
|
||||
|
||||
let r2 = rules.next().unwrap();
|
||||
assert_eq!(r2.rule_name, "no-unused-vars");
|
||||
assert_eq!(r2.plugin_name, "foo");
|
||||
assert!(r2.severity.is_warn_deny());
|
||||
assert!(r2.config.is_none());
|
||||
|
||||
let r3 = rules.next().unwrap();
|
||||
assert_eq!(r3.rule_name, "dummy");
|
||||
assert_eq!(r3.plugin_name, "eslint");
|
||||
assert!(r3.severity.is_warn_deny());
|
||||
assert_eq!(r3.config, Some(serde_json::json!(["arg1", "args2"])));
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_rules_default() {
|
||||
let config = ESLintConfig::from_value(&serde_json::json!({})).unwrap();
|
||||
assert!(config.rules.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_settings() {
|
||||
let config = ESLintConfig::from_value(&serde_json::json!({
|
||||
"no-debugger": 2,
|
||||
"no-bitwise": [
|
||||
"error",
|
||||
{ "allow": ["~"] }
|
||||
],
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"always", { "null": "ignore" }, "foo"
|
||||
],
|
||||
"@typescript-eslint/ban-types": "error",
|
||||
"jsx-a11y/alt-text": "warn",
|
||||
"@next/next/noop": [1]
|
||||
},
|
||||
"settings": {
|
||||
"jsx-a11y": {
|
||||
"polymorphicPropName": "role",
|
||||
|
|
@ -436,61 +176,14 @@ mod test {
|
|||
"Link2": "Anchor2"
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"rootDir": "app"
|
||||
},
|
||||
"formComponents": [
|
||||
"CustomForm",
|
||||
{"name": "SimpleForm", "formAttribute": "endpoint"},
|
||||
{"name": "Form", "formAttribute": ["registerEndpoint", "loginEndpoint"]},
|
||||
],
|
||||
"linkComponents": [
|
||||
"Hyperlink",
|
||||
{"name": "MyLink", "linkAttribute": "to"},
|
||||
{"name": "Link", "linkAttribute": ["to", "href"]},
|
||||
]
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(config.settings.jsx_a11y.polymorphic_prop_name, Some("role".to_string()));
|
||||
assert_eq!(config.settings.jsx_a11y.components.get("Link"), Some(&"Anchor".to_string()));
|
||||
assert!(config.settings.nextjs.root_dir.contains(&"app".to_string()));
|
||||
assert_eq!(config.settings.form_components.get("CustomForm"), Some(&vec![]));
|
||||
assert_eq!(
|
||||
config.settings.form_components.get("SimpleForm"),
|
||||
Some(&vec!["endpoint".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
config.settings.form_components.get("Form"),
|
||||
Some(&vec!["registerEndpoint".to_string(), "loginEndpoint".to_string()])
|
||||
);
|
||||
assert_eq!(config.settings.link_components.len(), 3);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_settings_default() {
|
||||
let config = ESLintConfig::from_value(&serde_json::json!({})).unwrap();
|
||||
assert!(config.settings.jsx_a11y.polymorphic_prop_name.is_none());
|
||||
assert!(config.settings.jsx_a11y.components.is_empty());
|
||||
assert!(config.settings.nextjs.root_dir.is_empty());
|
||||
assert!(config.settings.form_components.is_empty());
|
||||
assert!(config.settings.link_components.is_empty());
|
||||
}
|
||||
},
|
||||
"env": { "browser": true, }
|
||||
}));
|
||||
assert!(config.is_ok());
|
||||
|
||||
#[test]
|
||||
fn test_parse_env() {
|
||||
let config = ESLintConfig::from_value(&serde_json::json!({
|
||||
"env": { "browser": true, "node": true, "es6": false }
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(config.env.len(), 2);
|
||||
assert!(config.env.contains(&"browser".to_string()));
|
||||
assert!(config.env.contains(&"node".to_string()));
|
||||
assert!(!config.env.contains(&"es6".to_string()));
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_env_default() {
|
||||
let config = ESLintConfig::from_value(&serde_json::json!({})).unwrap();
|
||||
assert_eq!(config.env.len(), 1);
|
||||
assert_eq!(config.env.first(), Some(&"builtin".to_string()));
|
||||
let ESLintConfig { rules, settings, env } = config.unwrap();
|
||||
assert!(!rules.is_empty());
|
||||
assert_eq!(settings.jsx_a11y.polymorphic_prop_name, Some("role".to_string()));
|
||||
assert_eq!(env.iter().count(), 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
173
crates/oxc_linter/src/config/rules.rs
Normal file
173
crates/oxc_linter/src/config/rules.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
use super::errors::FailedToParseRuleValueError;
|
||||
use crate::AllowWarnDeny;
|
||||
use oxc_diagnostics::Error;
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// The `rules` field from ESLint config
|
||||
///
|
||||
/// TS type is `Record<string, RuleConf>`
|
||||
/// - type SeverityConf = 0 | 1 | 2 | "off" | "warn" | "error";
|
||||
/// - type RuleConf = SeverityConf | [SeverityConf, ...any[]];
|
||||
/// https://github.com/eslint/eslint/blob/ce838adc3b673e52a151f36da0eedf5876977514/lib/shared/types.js#L12
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ESLintRules(Vec<ESLintRule>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ESLintRule {
|
||||
pub plugin_name: String,
|
||||
pub rule_name: String,
|
||||
pub severity: AllowWarnDeny,
|
||||
pub config: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
// Manually implement Deserialize because the type is a bit complex...
|
||||
// - Handle single value form and array form
|
||||
// - SeverityConf into AllowWarnDeny
|
||||
// - Align plugin names
|
||||
impl<'de> Deserialize<'de> for ESLintRules {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ESLintRulesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ESLintRulesVisitor {
|
||||
type Value = ESLintRules;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("Record<string, SeverityConf | [SeverityConf, ...any[]]>")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: de::MapAccess<'de>,
|
||||
{
|
||||
let mut rules = vec![];
|
||||
while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
|
||||
let (plugin_name, rule_name) = parse_rule_key(&key);
|
||||
let (severity, config) = parse_rule_value(&value).map_err(de::Error::custom)?;
|
||||
rules.push(ESLintRule { plugin_name, rule_name, severity, config });
|
||||
}
|
||||
|
||||
Ok(ESLintRules(rules))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ESLintRulesVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_rule_key(name: &str) -> (String, String) {
|
||||
let Some((plugin_name, rule_name)) = name.split_once('/') else {
|
||||
return ("eslint".to_string(), name.to_string());
|
||||
};
|
||||
|
||||
let (oxlint_plugin_name, rule_name) = match plugin_name {
|
||||
"@typescript-eslint" => ("typescript", rule_name),
|
||||
"jsx-a11y" => ("jsx_a11y", rule_name),
|
||||
"react-perf" => ("react_perf", rule_name),
|
||||
// e.g. "@next/next/google-font-display"
|
||||
"@next" => ("nextjs", rule_name.trim_start_matches("next/")),
|
||||
_ => (plugin_name, rule_name),
|
||||
};
|
||||
|
||||
(oxlint_plugin_name.to_string(), rule_name.to_string())
|
||||
}
|
||||
|
||||
fn parse_rule_value(
|
||||
value: &serde_json::Value,
|
||||
) -> Result<(AllowWarnDeny, Option<serde_json::Value>), Error> {
|
||||
match value {
|
||||
serde_json::Value::String(_) | serde_json::Value::Number(_) => {
|
||||
let severity = AllowWarnDeny::try_from(value)?;
|
||||
Ok((severity, None))
|
||||
}
|
||||
|
||||
serde_json::Value::Array(v) => {
|
||||
if v.is_empty() {
|
||||
return Err(FailedToParseRuleValueError(
|
||||
value.to_string(),
|
||||
"Type should be `[SeverityConf, ...any[]`",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// The first item should be SeverityConf
|
||||
let severity = AllowWarnDeny::try_from(v.first().unwrap())?;
|
||||
// e.g. ["warn"], [0]
|
||||
let config = if v.len() == 1 {
|
||||
None
|
||||
// e.g. ["error", "args", { type: "whatever" }, ["len", "also"]]
|
||||
} else {
|
||||
Some(serde_json::Value::Array(v.iter().skip(1).cloned().collect::<Vec<_>>()))
|
||||
};
|
||||
|
||||
Ok((severity, config))
|
||||
}
|
||||
|
||||
_ => Err(FailedToParseRuleValueError(
|
||||
value.to_string(),
|
||||
"Type should be `SeverityConf | [SeverityConf, ...any[]]`",
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ESLintRules {
|
||||
type Target = Vec<ESLintRule>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ESLintRules;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[test]
|
||||
fn test_parse_rules() {
|
||||
let rules = ESLintRules::deserialize(&serde_json::json!({
|
||||
"no-console": "off",
|
||||
"foo/no-unused-vars": [1],
|
||||
"dummy": ["error", "arg1", "args2"],
|
||||
"@next/next/noop": 2,
|
||||
}))
|
||||
.unwrap();
|
||||
let mut rules = rules.iter();
|
||||
|
||||
let r1 = rules.next().unwrap();
|
||||
assert_eq!(r1.rule_name, "no-console");
|
||||
assert_eq!(r1.plugin_name, "eslint");
|
||||
assert!(r1.severity.is_allow());
|
||||
assert!(r1.config.is_none());
|
||||
|
||||
let r2 = rules.next().unwrap();
|
||||
assert_eq!(r2.rule_name, "no-unused-vars");
|
||||
assert_eq!(r2.plugin_name, "foo");
|
||||
assert!(r2.severity.is_warn_deny());
|
||||
assert!(r2.config.is_none());
|
||||
|
||||
let r3 = rules.next().unwrap();
|
||||
assert_eq!(r3.rule_name, "dummy");
|
||||
assert_eq!(r3.plugin_name, "eslint");
|
||||
assert!(r3.severity.is_warn_deny());
|
||||
assert_eq!(r3.config, Some(serde_json::json!(["arg1", "args2"])));
|
||||
|
||||
let r4 = rules.next().unwrap();
|
||||
assert_eq!(r4.rule_name, "noop");
|
||||
assert_eq!(r4.plugin_name, "nextjs");
|
||||
assert!(r4.severity.is_warn_deny());
|
||||
assert!(r4.config.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_rules_default() {
|
||||
let rules = ESLintRules::default();
|
||||
assert!(rules.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
use rustc_hash::FxHashMap;
|
||||
|
||||
/// The `settings` field from ESLint config
|
||||
///
|
||||
/// An object containing name-value pairs of information that should be available to all rules
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ESLintSettings {
|
||||
pub jsx_a11y: JsxA11y,
|
||||
pub nextjs: Nextjs,
|
||||
pub link_components: CustomComponents,
|
||||
pub form_components: CustomComponents,
|
||||
}
|
||||
|
||||
impl Default for ESLintSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
jsx_a11y: JsxA11y { polymorphic_prop_name: None, components: FxHashMap::default() },
|
||||
nextjs: Nextjs { root_dir: vec![] },
|
||||
link_components: FxHashMap::default(),
|
||||
form_components: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ESLintSettings {
|
||||
pub fn new(
|
||||
jsx_a11y: JsxA11y,
|
||||
nextjs: Nextjs,
|
||||
link_components: CustomComponents,
|
||||
form_components: CustomComponents,
|
||||
) -> Self {
|
||||
Self { jsx_a11y, nextjs, link_components, form_components }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JsxA11y {
|
||||
pub polymorphic_prop_name: Option<String>,
|
||||
pub components: FxHashMap<String, String>,
|
||||
}
|
||||
|
||||
impl JsxA11y {
|
||||
pub fn new(
|
||||
polymorphic_prop_name: Option<String>,
|
||||
components: FxHashMap<String, String>,
|
||||
) -> Self {
|
||||
Self { polymorphic_prop_name, components }
|
||||
}
|
||||
|
||||
pub fn set_components(&mut self, components: FxHashMap<String, String>) {
|
||||
self.components = components;
|
||||
}
|
||||
|
||||
pub fn set_polymorphic_prop_name(&mut self, name: Option<String>) {
|
||||
self.polymorphic_prop_name = name;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Nextjs {
|
||||
pub root_dir: Vec<String>,
|
||||
}
|
||||
|
||||
impl Nextjs {
|
||||
pub fn new(root_dir: Vec<String>) -> Self {
|
||||
Self { root_dir }
|
||||
}
|
||||
|
||||
pub fn set_root_dir(&mut self, root_dir: Vec<String>) {
|
||||
self.root_dir = root_dir;
|
||||
}
|
||||
}
|
||||
|
||||
pub type CustomComponents = FxHashMap<String, Vec<String>>;
|
||||
11
crates/oxc_linter/src/config/settings/jsx_a11y.rs
Normal file
11
crates/oxc_linter/src/config/settings/jsx_a11y.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#configurations
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct ESLintSettingsJSXA11y {
|
||||
#[serde(rename = "polymorphicPropName")]
|
||||
pub polymorphic_prop_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub components: FxHashMap<String, String>,
|
||||
}
|
||||
83
crates/oxc_linter/src/config/settings/mod.rs
Normal file
83
crates/oxc_linter/src/config/settings/mod.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use self::{jsx_a11y::ESLintSettingsJSXA11y, next::ESLintSettingsNext, react::ESLintSettingsReact};
|
||||
use serde::Deserialize;
|
||||
|
||||
mod jsx_a11y;
|
||||
mod next;
|
||||
mod react;
|
||||
|
||||
/// The `settings` field from ESLint config
|
||||
/// An object containing name-value pairs of information that should be available to all rules
|
||||
///
|
||||
/// TS type is `Object`
|
||||
/// https://github.com/eslint/eslint/blob/ce838adc3b673e52a151f36da0eedf5876977514/lib/shared/types.js#L53
|
||||
/// But each plugin extends this with their own properties.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct ESLintSettings {
|
||||
#[serde(default)]
|
||||
#[serde(rename = "jsx-a11y")]
|
||||
pub jsx_a11y: ESLintSettingsJSXA11y,
|
||||
#[serde(default)]
|
||||
pub next: ESLintSettingsNext,
|
||||
#[serde(default)]
|
||||
pub react: ESLintSettingsReact,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ESLintSettings;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[test]
|
||||
fn test_parse_settings() {
|
||||
let settings = ESLintSettings::deserialize(&serde_json::json!({
|
||||
"jsx-a11y": {
|
||||
"polymorphicPropName": "role",
|
||||
"components": {
|
||||
"Link": "Anchor",
|
||||
"Link2": "Anchor2"
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"rootDir": "app"
|
||||
},
|
||||
"react": {
|
||||
"formComponents": [
|
||||
"CustomForm",
|
||||
{"name": "SimpleForm", "formAttribute": "endpoint"},
|
||||
{"name": "Form", "formAttribute": ["registerEndpoint", "loginEndpoint"]},
|
||||
],
|
||||
"linkComponents": [
|
||||
"Hyperlink",
|
||||
{"name": "MyLink", "linkAttribute": "to"},
|
||||
{"name": "Link", "linkAttribute": ["to", "href"]},
|
||||
]
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(settings.jsx_a11y.polymorphic_prop_name, Some("role".to_string()));
|
||||
assert_eq!(settings.jsx_a11y.components.get("Link"), Some(&"Anchor".to_string()));
|
||||
assert!(settings.next.get_root_dirs().contains(&"app".to_string()));
|
||||
assert_eq!(settings.react.get_form_component_attrs("CustomForm"), Some(vec![]));
|
||||
assert_eq!(
|
||||
settings.react.get_form_component_attrs("SimpleForm"),
|
||||
Some(vec!["endpoint".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
settings.react.get_form_component_attrs("Form"),
|
||||
Some(vec!["registerEndpoint".to_string(), "loginEndpoint".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
settings.react.get_link_component_attrs("Link"),
|
||||
Some(vec!["to".to_string(), "href".to_string()])
|
||||
);
|
||||
assert_eq!(settings.react.get_link_component_attrs("Noop"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_settings_default() {
|
||||
let settings = ESLintSettings::default();
|
||||
assert!(settings.jsx_a11y.polymorphic_prop_name.is_none());
|
||||
assert!(settings.jsx_a11y.components.is_empty());
|
||||
}
|
||||
}
|
||||
32
crates/oxc_linter/src/config/settings/next.rs
Normal file
32
crates/oxc_linter/src/config/settings/next.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
/// https://nextjs.org/docs/pages/building-your-application/configuring/eslint#eslint-plugin
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct ESLintSettingsNext {
|
||||
#[serde(default)]
|
||||
#[serde(rename = "rootDir")]
|
||||
root_dir: OneOrMany<String>,
|
||||
}
|
||||
|
||||
impl ESLintSettingsNext {
|
||||
pub fn get_root_dirs(&self) -> Vec<String> {
|
||||
match &self.root_dir {
|
||||
OneOrMany::One(val) => vec![val.clone()],
|
||||
OneOrMany::Many(vec) => vec.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize helper types
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
enum OneOrMany<T> {
|
||||
One(T),
|
||||
Many(Vec<T>),
|
||||
}
|
||||
impl<T> Default for OneOrMany<T> {
|
||||
fn default() -> Self {
|
||||
OneOrMany::Many(Vec::new())
|
||||
}
|
||||
}
|
||||
62
crates/oxc_linter/src/config/settings/react.rs
Normal file
62
crates/oxc_linter/src/config/settings/react.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
/// https://github.com/jsx-eslint/eslint-plugin-react#configuration-legacy-eslintrc-
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct ESLintSettingsReact {
|
||||
#[serde(default)]
|
||||
#[serde(rename = "formComponents")]
|
||||
form_components: Vec<CustomComponent>,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "linkComponents")]
|
||||
link_components: Vec<CustomComponent>,
|
||||
// TODO: More properties should be added
|
||||
}
|
||||
|
||||
impl ESLintSettingsReact {
|
||||
pub fn get_form_component_attrs(&self, name: &str) -> Option<Vec<String>> {
|
||||
get_component_attrs_by_name(&self.form_components, name)
|
||||
}
|
||||
|
||||
pub fn get_link_component_attrs(&self, name: &str) -> Option<Vec<String>> {
|
||||
get_component_attrs_by_name(&self.link_components, name)
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize helper types
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum CustomComponent {
|
||||
NameOnly(String),
|
||||
ObjectWithOneAttr {
|
||||
name: String,
|
||||
#[serde(alias = "formAttribute", alias = "linkAttribute")]
|
||||
attribute: String,
|
||||
},
|
||||
ObjectWithManyAttrs {
|
||||
name: String,
|
||||
#[serde(alias = "formAttribute", alias = "linkAttribute")]
|
||||
attributes: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn get_component_attrs_by_name(
|
||||
components: &Vec<CustomComponent>,
|
||||
name: &str,
|
||||
) -> Option<Vec<String>> {
|
||||
for item in components {
|
||||
let comp = match item {
|
||||
CustomComponent::NameOnly(name) => (name, vec![]),
|
||||
CustomComponent::ObjectWithOneAttr { name, attribute } => {
|
||||
(name, vec![attribute.to_string()])
|
||||
}
|
||||
CustomComponent::ObjectWithManyAttrs { name, attributes } => (name, attributes.clone()),
|
||||
};
|
||||
|
||||
if comp.0 == name {
|
||||
return Some(comp.1);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ use std::{io::Write, rc::Rc, sync::Arc};
|
|||
use oxc_diagnostics::Report;
|
||||
|
||||
use crate::{
|
||||
config::{ESLintEnv, ESLintSettings, JsxA11y},
|
||||
config::{ESLintEnv, ESLintSettings},
|
||||
fixer::Fix,
|
||||
fixer::{Fixer, Message},
|
||||
rule::RuleCategory,
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ impl LintOptions {
|
|||
|
||||
#[must_use]
|
||||
pub fn with_env(mut self, env: Vec<String>) -> Self {
|
||||
self.env = ESLintEnv::new(env);
|
||||
self.env = ESLintEnv::from_vec(env);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ impl JsxNoTargetBlank {
|
|||
if tag_name == "a" {
|
||||
return true;
|
||||
}
|
||||
return ctx.settings().link_components.get(tag_name).is_some();
|
||||
return ctx.settings().react.get_link_component_attrs(tag_name).is_some();
|
||||
}
|
||||
fn check_is_forms(&self, tag_name: &str, ctx: &LintContext) -> bool {
|
||||
if !self.forms {
|
||||
|
|
@ -81,7 +81,7 @@ impl JsxNoTargetBlank {
|
|||
if tag_name == "form" {
|
||||
return true;
|
||||
}
|
||||
return ctx.settings().form_components.get(tag_name).is_some();
|
||||
return ctx.settings().react.get_form_component_attrs(tag_name).is_some();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,18 +142,20 @@ impl Rule for JsxNoTargetBlank {
|
|||
}
|
||||
} else if attribute_name == "href"
|
||||
|| attribute_name == "action"
|
||||
|| ctx.settings().link_components.get(tag_name).map_or(
|
||||
false,
|
||||
|link_attribute| {
|
||||
|| ctx
|
||||
.settings()
|
||||
.react
|
||||
.get_link_component_attrs(tag_name)
|
||||
.map_or(false, |link_attribute| {
|
||||
link_attribute.contains(&attribute_name.to_string())
|
||||
},
|
||||
)
|
||||
|| ctx.settings().form_components.get(tag_name).map_or(
|
||||
false,
|
||||
|link_attribute| {
|
||||
link_attribute.contains(&attribute_name.to_string())
|
||||
},
|
||||
)
|
||||
})
|
||||
|| ctx
|
||||
.settings()
|
||||
.react
|
||||
.get_form_component_attrs(tag_name)
|
||||
.map_or(false, |form_attribute| {
|
||||
form_attribute.contains(&attribute_name.to_string())
|
||||
})
|
||||
{
|
||||
if let Some(val) = attribute.value.as_ref() {
|
||||
has_href_value = true;
|
||||
|
|
@ -487,20 +489,20 @@ fn test() {
|
|||
(
|
||||
r#"<Link target="_blank" href={ dynamicLink }></Link>"#,
|
||||
Some(serde_json::json!([{ "enforceDynamicLinks": "never" }])),
|
||||
Some(serde_json::json!({ "linkComponents": ["Link"] })),
|
||||
Some(serde_json::json!({ "react": { "linkComponents": ["Link"] } })),
|
||||
),
|
||||
(
|
||||
r#"<Link target="_blank" to={ dynamicLink }></Link>"#,
|
||||
Some(serde_json::json!([{ "enforceDynamicLinks": "never" }])),
|
||||
Some(
|
||||
serde_json::json!({ "linkComponents": { "name": "Link", "linkAttribute": "to" } }),
|
||||
serde_json::json!({"react": { "linkComponents": [{ "name": "Link", "linkAttribute": "to" }] }}),
|
||||
),
|
||||
),
|
||||
(
|
||||
r#"<Link target="_blank" to={ dynamicLink }></Link>"#,
|
||||
Some(serde_json::json!([{ "enforceDynamicLinks": "never" }])),
|
||||
Some(
|
||||
serde_json::json!({ "linkComponents": { "name": "Link", "linkAttribute": ["to"] } }),
|
||||
serde_json::json!({ "react": { "linkComponents": [{ "name": "Link", "linkAttribute": ["to"] }] }}),
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -681,13 +683,13 @@ fn test() {
|
|||
(
|
||||
r#"<Link target="_blank" href={ dynamicLink }></Link>"#,
|
||||
Some(serde_json::json!([{ "enforceDynamicLinks": "always"}])),
|
||||
Some(serde_json::json!({ "linkComponents": ["Link"] })),
|
||||
Some(serde_json::json!({ "react": { "linkComponents": ["Link"] } })),
|
||||
),
|
||||
(
|
||||
r#"<Link target="_blank" to={ dynamicLink }></Link>"#,
|
||||
Some(serde_json::json!([{ "enforceDynamicLinks": "always" }])),
|
||||
Some(
|
||||
serde_json::json!({ "linkComponents": { "name": "Link", "linkAttribute": "to" } }),
|
||||
serde_json::json!({ "react": { "linkComponents": [{ "name": "Link", "linkAttribute": "to" }] } }),
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@ use std::{
|
|||
use oxc_allocator::Allocator;
|
||||
use oxc_diagnostics::miette::NamedSource;
|
||||
use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler, GraphicalTheme};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
config::parse_settings, rules::RULES, ESLintSettings, Fixer, LintOptions, LintService, Linter,
|
||||
RuleEnum,
|
||||
};
|
||||
use crate::{rules::RULES, ESLintSettings, Fixer, LintOptions, LintService, Linter, RuleEnum};
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum TestResult {
|
||||
|
|
@ -188,8 +186,9 @@ impl Tester {
|
|||
) -> TestResult {
|
||||
let allocator = Allocator::default();
|
||||
let rule = self.find_rule().read_json(config);
|
||||
let lint_settings: ESLintSettings =
|
||||
settings.as_ref().map_or_else(ESLintSettings::default, parse_settings);
|
||||
let lint_settings: ESLintSettings = settings
|
||||
.as_ref()
|
||||
.map_or_else(ESLintSettings::default, |v| ESLintSettings::deserialize(v).unwrap());
|
||||
let options = LintOptions::default()
|
||||
.with_fix(is_fix)
|
||||
.with_import_plugin(self.import_plugin)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use oxc_ast::{
|
|||
};
|
||||
use oxc_semantic::{AstNode, SymbolFlags};
|
||||
|
||||
use crate::{ESLintSettings, JsxA11y, LintContext};
|
||||
use crate::{ESLintSettings, LintContext};
|
||||
|
||||
pub fn is_create_element_call(call_expr: &CallExpression) -> bool {
|
||||
if let Some(member_expr) = call_expr.callee.get_member_expr() {
|
||||
|
|
@ -225,9 +225,8 @@ pub fn get_element_type(context: &LintContext, element: &JSXOpeningElement) -> O
|
|||
};
|
||||
|
||||
let ESLintSettings { jsx_a11y, .. } = context.settings();
|
||||
let JsxA11y { polymorphic_prop_name, components } = jsx_a11y;
|
||||
|
||||
if let Some(polymorphic_prop_name_value) = polymorphic_prop_name {
|
||||
if let Some(polymorphic_prop_name_value) = &jsx_a11y.polymorphic_prop_name {
|
||||
if let Some(as_tag) = has_jsx_prop_lowercase(element, polymorphic_prop_name_value) {
|
||||
if let Some(JSXAttributeValue::StringLiteral(str)) = get_prop_value(as_tag) {
|
||||
return Some(String::from(str.value.as_str()));
|
||||
|
|
@ -236,7 +235,7 @@ pub fn get_element_type(context: &LintContext, element: &JSXOpeningElement) -> O
|
|||
}
|
||||
|
||||
let element_type = ident.name.as_str();
|
||||
if let Some(val) = components.get(element_type) {
|
||||
if let Some(val) = jsx_a11y.components.get(element_type) {
|
||||
return Some(String::from(val));
|
||||
}
|
||||
Some(String::from(element_type))
|
||||
|
|
|
|||
Loading…
Reference in a new issue