Skip to content

Commit

Permalink
feat(linter): add jsx-a11y settings (#1668)
Browse files Browse the repository at this point in the history
When we developed linter for #1141 , we needed to configure some
settings for `jsx-a11y`, which was not supported before, but I am trying
to support it now.
like this:
```
fn config() -> serde_json::Value {
    serde_json::json!([2,{
        "ignoreNonDOM": true
    }])
}

fn settings() -> serde_json::Value {
    serde_json::json!({
        "jsx-a11y": {
            "components": {
                "Button": "button",
            }
        }
    })
}

let pass = vec![
    ("<Button />", Some(config()), Some(settings())),
];
```
  • Loading branch information
msdlisper authored Dec 16, 2023
1 parent cf91379 commit 6a90cd4
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 77 deletions.
53 changes: 48 additions & 5 deletions crates/oxc_linter/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{collections::HashMap, path::PathBuf};

pub mod errors;
use oxc_diagnostics::{Error, FailedToOpenFileError, Report};
Expand All @@ -7,7 +7,7 @@ use serde_json::Value;

use crate::{
rules::{RuleEnum, RULES},
AllowWarnDeny,
AllowWarnDeny, JsxA11y, LintSettings,
};

use self::errors::{
Expand All @@ -17,6 +17,7 @@ use self::errors::{

pub struct ESLintConfig {
rules: std::vec::Vec<RuleEnum>,
settings: LintSettings,
}

impl ESLintConfig {
Expand Down Expand Up @@ -66,6 +67,8 @@ impl ESLintConfig {
}
};

let settings = parse_settings_from_root(&file);

// `extends` provides the defaults
// `rules` provides the overrides
let rules = RULES.clone().into_iter().filter_map(|rule| {
Expand All @@ -91,12 +94,16 @@ impl ESLintConfig {
}
});

Ok(Self { rules: rules.collect::<Vec<_>>() })
Ok(Self { rules: rules.collect::<Vec<_>>(), settings })
}

pub fn into_rules(mut self) -> Vec<RuleEnum> {
pub fn into_rules(mut self) -> Self {
self.rules.sort_unstable_by_key(RuleEnum::name);
self.rules
self
}

pub fn get_config(self) -> (std::vec::Vec<RuleEnum>, LintSettings) {
(self.rules, self.settings)
}
}

Expand Down Expand Up @@ -151,6 +158,42 @@ fn parse_rules(
.collect::<Result<Vec<_>, Error>>()
}

fn parse_settings_from_root(root_json: &Value) -> LintSettings {
let Value::Object(root_object) = root_json else { return LintSettings::default() };

let Some(settings_value) = root_object.get("settings") else { return LintSettings::default() };

parse_settings(settings_value)
}

pub fn parse_settings(setting_value: &Value) -> LintSettings {
if let Value::Object(settings_object) = setting_value {
if let Some(Value::Object(jsx_a11y)) = settings_object.get("jsx-a11y") {
let mut jsx_a11y_setting =
JsxA11y { polymorphic_prop_name: None, components: HashMap::new() };

if let Some(Value::Object(components)) = jsx_a11y.get("components") {
let components_map: HashMap<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)));
}

return LintSettings { jsx_a11y: jsx_a11y_setting };
}
}

LintSettings::default()
}

pub const EXTENDS_MAP: Map<&'static str, &'static str> = phf_map! {
"eslint:recommended" => "eslint",
"plugin:react/recommended" => "react",
Expand Down
11 changes: 9 additions & 2 deletions crates/oxc_linter/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use oxc_span::SourceType;
use crate::{
disable_directives::{DisableDirectives, DisableDirectivesBuilder},
fixer::{Fix, Message},
AstNode,
AstNode, LintSettings,
};

pub struct LintContext<'a> {
Expand All @@ -24,10 +24,12 @@ pub struct LintContext<'a> {
current_rule_name: &'static str,

file_path: Box<Path>,

settings: LintSettings,
}

impl<'a> LintContext<'a> {
pub fn new(file_path: Box<Path>, semantic: &Rc<Semantic<'a>>) -> Self {
pub fn new(file_path: Box<Path>, semantic: &Rc<Semantic<'a>>, settings: LintSettings) -> Self {
let disable_directives =
DisableDirectivesBuilder::new(semantic.source_text(), semantic.trivias()).build();
Self {
Expand All @@ -37,6 +39,7 @@ impl<'a> LintContext<'a> {
fix: false,
current_rule_name: "",
file_path,
settings,
}
}

Expand All @@ -54,6 +57,10 @@ impl<'a> LintContext<'a> {
&self.disable_directives
}

pub fn settings(&self) -> LintSettings {
self.settings.clone()
}

pub fn source_text(&self) -> &'a str {
self.semantic().source_text()
}
Expand Down
51 changes: 47 additions & 4 deletions crates/oxc_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mod rules;
mod service;
mod utils;

use std::{self, fs, io::Write, rc::Rc, time::Duration};
use std::{self, collections::HashMap, fs, io::Write, rc::Rc, time::Duration};

use oxc_diagnostics::Report;
pub(crate) use oxc_semantic::AstNode;
Expand All @@ -33,10 +33,38 @@ pub use crate::{
};
pub(crate) use rules::{RuleEnum, RULES};

#[derive(Debug, Clone)]
pub struct LintSettings {
jsx_a11y: JsxA11y,
}

impl Default for LintSettings {
fn default() -> Self {
Self { jsx_a11y: JsxA11y { polymorphic_prop_name: None, components: HashMap::new() } }
}
}

#[derive(Debug, Clone)]
pub struct JsxA11y {
polymorphic_prop_name: Option<String>,
components: HashMap<String, String>,
}

impl JsxA11y {
pub fn set_components(&mut self, components: HashMap<String, String>) {
self.components = components;
}

pub fn set_polymorphic_prop_name(&mut self, name: Option<String>) {
self.polymorphic_prop_name = name;
}
}

#[derive(Debug)]
pub struct Linter {
rules: Vec<RuleEnum>,
options: LintOptions,
settings: LintSettings,
}

impl Default for Linter {
Expand All @@ -52,15 +80,21 @@ impl Linter {
.filter(|&rule| rule.category() == RuleCategory::Correctness)
.cloned()
.collect::<Vec<_>>();
Self { rules, options: LintOptions::default() }
Self {
rules,
options: LintOptions::default(),
settings: LintSettings {
jsx_a11y: JsxA11y { polymorphic_prop_name: None, components: HashMap::new() },
},
}
}

/// # Errors
///
/// Returns `Err` if there are any errors parsing the configuration file.
pub fn from_options(options: LintOptions) -> Result<Self, Report> {
let rules = options.derive_rules()?;
Ok(Self { rules, options })
let (rules, settings) = options.derive_rules_and_settings()?;
Ok(Self { rules, options, settings })
}

#[must_use]
Expand All @@ -69,6 +103,12 @@ impl Linter {
self
}

#[must_use]
pub fn with_settings(mut self, settings: LintSettings) -> Self {
self.settings = settings;
self
}

pub fn rules(&self) -> &Vec<RuleEnum> {
&self.rules
}
Expand Down Expand Up @@ -120,6 +160,9 @@ impl Linter {
ctx.into_message()
}

pub fn get_settings(&self) -> LintSettings {
self.settings.clone()
}
#[allow(unused)]
fn read_rules_configuration() -> Option<serde_json::Map<String, serde_json::Value>> {
fs::read_to_string(".eslintrc.json")
Expand Down
10 changes: 5 additions & 5 deletions crates/oxc_linter/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
ESLintConfig,
},
rules::RULES,
RuleCategory, RuleEnum,
LintSettings, RuleCategory, RuleEnum,
};
use oxc_diagnostics::{Error, Report};
use rustc_hash::FxHashSet;
Expand Down Expand Up @@ -145,12 +145,12 @@ const JSX_A11Y_PLUGIN_NAME: &str = "jsx_a11y";
impl LintOptions {
/// # Errors
/// Returns `Err` if there are any errors parsing the configuration file.
pub fn derive_rules(&self) -> Result<Vec<RuleEnum>, Report> {
pub fn derive_rules_and_settings(&self) -> Result<(Vec<RuleEnum>, LintSettings), Report> {
let mut rules: FxHashSet<RuleEnum> = FxHashSet::default();

if let Some(path) = &self.config_path {
let rules = ESLintConfig::new(path)?.into_rules();
return Ok(rules);
let (rules, settings) = ESLintConfig::new(path)?.into_rules().get_config();
return Ok((rules, settings));
}

let all_rules = self.get_filtered_rules();
Expand Down Expand Up @@ -195,7 +195,7 @@ impl LintOptions {
let mut rules = rules.into_iter().collect::<Vec<_>>();
// for stable diagnostics output ordering
rules.sort_unstable_by_key(RuleEnum::name);
Ok(rules)
Ok((rules, LintSettings::default()))
}

// get final filtered rules by reading `self.jest_plugin` and `self.jsx_a11y_plugin`
Expand Down
72 changes: 42 additions & 30 deletions crates/oxc_linter/src/rules/jsx_a11y/no_autofocus.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
use phf::phf_set;

use oxc_ast::{ast::JSXElementName, AstKind};
use oxc_ast::AstKind;
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop, AstNode};
use crate::{
context::LintContext,
rule::Rule,
utils::{get_element_type, has_jsx_prop},
AstNode,
};

#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-jsx-a11y(no-autofocus): The `autofocus` attribute is found here, which can cause usability issues for sighted and non-sighted users")]
Expand Down Expand Up @@ -86,12 +91,11 @@ impl Rule for NoAutofocus {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::JSXElement(jsx_el) = node.kind() {
if let Option::Some(autofocus) = has_jsx_prop(&jsx_el.opening_element, "autoFocus") {
let Some(element_type) = get_element_type(ctx, &jsx_el.opening_element) else {
return;
};
if self.ignore_non_dom {
let JSXElementName::Identifier(ident) = &jsx_el.opening_element.name else {
return;
};
let name = ident.name.as_str();
if HTML_TAG.contains(name) {
if HTML_TAG.contains(&element_type) {
if let oxc_ast::ast::JSXAttributeItem::Attribute(attr) = autofocus {
ctx.diagnostic(NoAutofocusDiagnostic(attr.span));
}
Expand Down Expand Up @@ -262,38 +266,46 @@ const HTML_TAG: phf::Set<&'static str> = phf_set! {
#[test]
fn test() {
use crate::tester::Tester;
fn array() -> serde_json::Value {
fn config() -> serde_json::Value {
serde_json::json!([2,{
"ignoreNonDOM": true
}])
}

fn settings() -> serde_json::Value {
serde_json::json!({
"jsx-a11y": {
"components": {
"Button": "button",
}
}
})
}

let pass = vec![
("<div />;", None),
("<div autofocus />;", None),
("<input autofocus='true' />;", None),
("<Foo bar />", None),
("<Button />", None),
("<Foo autoFocus />", Some(array())),
("<div><div autofocus /></div>", Some(array())),
// TODO we need components_settings to test this
// ("<Button />", Some(serde_json::json!(ignoreNonDOMSchema))),
// ("<Button />", Some(serde_json::json!(ignoreNonDOMSchema)), setting),
("<div />;", None, None),
("<div autofocus />;", None, None),
("<input autofocus='true' />;", None, None),
("<Foo bar />", None, None),
("<Button />", None, None),
("<Foo autoFocus />", Some(config()), None),
("<div><div autofocus /></div>", Some(config()), None),
("<Button />", None, Some(settings())),
("<Button />", Some(config()), Some(settings())),
];

let fail = vec![
("<div autoFocus />", None),
("<div autoFocus={true} />", None),
("<div autoFocus={false} />", None),
("<div autoFocus={undefined} />", None),
("<div autoFocus='true' />", None),
("<div autoFocus='false' />", None),
("<input autoFocus />", None),
("<Foo autoFocus />", None),
("<Button autoFocus />", None),
// TODO we need components_settings to test this
// ("<Button autoFocus />", Some(array())),
("<div autoFocus />", None, None),
("<div autoFocus={true} />", None, None),
("<div autoFocus={false} />", None, None),
("<div autoFocus={undefined} />", None, None),
("<div autoFocus='true' />", None, None),
("<div autoFocus='false' />", None, None),
("<input autoFocus />", None, None),
("<Foo autoFocus />", None, None),
("<Button autoFocus />", None, None),
("<Button autoFocus />", Some(config()), Some(settings())),
];

Tester::new(NoAutofocus::NAME, pass, fail).test_and_snapshot();
Tester::new_with_settings(NoAutofocus::NAME, pass, fail).test_and_snapshot();
}
8 changes: 6 additions & 2 deletions crates/oxc_linter/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl Runtime {
}
}

#[allow(clippy::too_many_arguments)]
fn process_source<'a>(
&self,
path: &Path,
Expand Down Expand Up @@ -233,8 +234,11 @@ impl Runtime {
return semantic_ret.errors.into_iter().map(|err| Message::new(err, None)).collect();
};

let lint_ctx =
LintContext::new(path.to_path_buf().into_boxed_path(), &Rc::new(semantic_ret.semantic));
let lint_ctx = LintContext::new(
path.to_path_buf().into_boxed_path(),
&Rc::new(semantic_ret.semantic),
self.linter.get_settings(),
);
self.linter.run(lint_ctx)
}

Expand Down
Loading

0 comments on commit 6a90cd4

Please sign in to comment.