From 7b65492893ff5e3a4e00897652c6cbc3d38a2db8 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Sat, 10 Jun 2023 10:57:05 +0200 Subject: [PATCH] refactor(rome_analyze): deserialize options early (#4541) * refactor(rome_analyze): deserialize options early * chore: add test case * chore: code generation * changelog * fix file name in diagnostic * chore: ignore deps * chore: remove deps --- CHANGELOG.md | 54 ++++++ Cargo.lock | 6 +- Cargo.toml | 3 + crates/rome_analyze/Cargo.toml | 2 - crates/rome_analyze/src/context.rs | 12 +- crates/rome_analyze/src/lib.rs | 20 +- crates/rome_analyze/src/matcher.rs | 29 ++- crates/rome_analyze/src/options.rs | 50 +++-- crates/rome_analyze/src/registry.rs | 44 ++--- crates/rome_analyze/src/rule.rs | 23 +-- crates/rome_analyze/src/signals.rs | 28 +-- crates/rome_analyze/src/syntax.rs | 9 +- crates/rome_analyze/src/visitor.rs | 16 +- crates/rome_deserialize/src/json.rs | 69 +++++++ crates/rome_deserialize/src/visitor.rs | 11 ++ crates/rome_js_analyze/Cargo.toml | 7 + crates/rome_js_analyze/src/analyzers.rs | 14 +- crates/rome_js_analyze/src/analyzers/a11y.rs | 30 +-- .../src/analyzers/complexity.rs | 26 +-- .../src/analyzers/correctness.rs | 36 ++-- .../rome_js_analyze/src/analyzers/nursery.rs | 22 +-- .../src/analyzers/performance.rs | 2 +- crates/rome_js_analyze/src/analyzers/style.rs | 38 ++-- .../src/analyzers/suspicious.rs | 46 ++--- crates/rome_js_analyze/src/aria_analyzers.rs | 4 +- .../src/aria_analyzers/a11y.rs | 8 +- .../src/aria_analyzers/nursery.rs | 8 +- crates/rome_js_analyze/src/assists.rs | 2 +- .../src/assists/correctness.rs | 6 +- crates/rome_js_analyze/src/lib.rs | 37 ++-- crates/rome_js_analyze/src/options.rs | 83 +++++++++ .../rome_js_analyze/src/semantic_analyzers.rs | 14 +- .../src/semantic_analyzers/a11y.rs | 4 +- .../src/semantic_analyzers/complexity.rs | 2 +- .../src/semantic_analyzers/correctness.rs | 14 +- .../src/semantic_analyzers/nursery.rs | 12 +- .../nursery/use_exhaustive_dependencies.rs | 175 +++++++++++++----- .../nursery/use_hook_at_top_level.rs | 29 ++- .../src/semantic_analyzers/security.rs | 4 +- .../src/semantic_analyzers/style.rs | 14 +- .../src/semantic_analyzers/suspicious.rs | 16 +- crates/rome_js_analyze/src/syntax.rs | 2 +- crates/rome_js_analyze/src/syntax/nursery.rs | 4 +- crates/rome_js_analyze/tests/spec_tests.rs | 49 +++-- .../customHook.options.json | 28 ++- .../malformedOptions.js.snap | 13 +- .../malformedOptions.options.json | 28 ++- .../useHookAtTopLevel/customHook.options.json | 20 +- crates/rome_migrate/Cargo.toml | 5 +- crates/rome_migrate/src/lib.rs | 18 +- crates/rome_service/Cargo.toml | 2 +- .../src/configuration/generated.rs | 40 ++-- .../src/configuration/linter/mod.rs | 20 +- .../configuration/parse/json/configuration.rs | 2 +- .../src/configuration/parse/json/linter.rs | 11 +- .../tests/invalid/hooks_missing_name.json | 20 ++ .../invalid/hooks_missing_name.json.snap | 21 +++ editors/vscode/configuration_schema.json | 51 ++++- npm/backend-jsonrpc/src/workspace.ts | 28 ++- npm/rome/configuration_schema.json | 51 ++++- .../lint/rules/useExhaustiveDependencies.md | 10 +- .../src/pages/lint/rules/useHookAtTopLevel.md | 25 +++ xtask/codegen/src/generate_analyzer.rs | 4 +- xtask/codegen/src/generate_configuration.rs | 5 +- 64 files changed, 1016 insertions(+), 470 deletions(-) create mode 100644 crates/rome_js_analyze/src/options.rs create mode 100644 crates/rome_service/tests/invalid/hooks_missing_name.json create mode 100644 crates/rome_service/tests/invalid/hooks_missing_name.json.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index cdaecea0538..45774896b92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,60 @@ ### Editors ### Formatter ### Linter + +#### Other changes + +- The rules [`useExhaustiveDependencies`](https://docs.rome.tools/lint/rules/useexhaustivedependencies/) and [`useHookAtTopLevel`](https://docs.rome.tools/lint/rules/usehookattoplevel/) accept a different + shape of options + +Old configuration + +```json +{ + "linter": { + "rules": { + "nursery": { + "useExhaustiveDependencies": { + "level": "error", + "options": { + "hooks": [ + ["useMyEffect", 0, 1] + ] + } + } + } + } + } +} +``` + +New configuration + +```json +{ + "linter": { + "rules": { + "nursery": { + "useExhaustiveDependencies": { + "level": "error", + "options": { + "hooks": [ + { + "name": "useMyEffect", + "closureIndex": 0, + "dependenciesIndex": 1 + } + ] + } + } + } + } + } +} +``` + + + ### Parser ### VSCode ### JavaScript APIs diff --git a/Cargo.lock b/Cargo.lock index 5edd5e2a90a..589f4fe3ff0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,9 +1652,7 @@ version = "0.0.0" dependencies = [ "bitflags 2.2.1", "rome_console", - "rome_deserialize", "rome_diagnostics", - "rome_json_parser", "rome_rowan", "rustc-hash", "schemars", @@ -1874,6 +1872,7 @@ dependencies = [ name = "rome_js_analyze" version = "0.0.0" dependencies = [ + "bpaf", "countme", "insta", "json_comments", @@ -1892,10 +1891,13 @@ dependencies = [ "rome_js_syntax", "rome_js_unicode_table", "rome_json_factory", + "rome_json_parser", "rome_json_syntax", "rome_rowan", + "rome_service", "rome_text_edit", "rustc-hash", + "schemars", "serde", "serde_json", "similar", diff --git a/Cargo.toml b/Cargo.toml index 2f3ec685280..ef04471881c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,6 @@ rome_json_factory = { version = "0.0.1", path = "./crates/rome_json_factory" } tests_macros = { path = "./crates/tests_macros" } rome_formatter_test = { path = "./crates/rome_formatter_test" } rome_js_analyze = { path = "./crates/rome_js_analyze" } +schemars = { version = "0.8.10" } + + diff --git a/crates/rome_analyze/Cargo.toml b/crates/rome_analyze/Cargo.toml index ee17a361a0f..9500acf4e8f 100644 --- a/crates/rome_analyze/Cargo.toml +++ b/crates/rome_analyze/Cargo.toml @@ -12,8 +12,6 @@ license.workspace = true rome_rowan = { workspace = true } rome_console = { workspace = true } rome_diagnostics = { workspace = true } -rome_json_parser = { workspace = true } -rome_deserialize = { workspace = true} bitflags.workspace = true rustc-hash = { workspace = true } serde = { version = "1.0.136", features = ["derive"] } diff --git a/crates/rome_analyze/src/context.rs b/crates/rome_analyze/src/context.rs index 800715b0dba..ca0da75e2e1 100644 --- a/crates/rome_analyze/src/context.rs +++ b/crates/rome_analyze/src/context.rs @@ -6,9 +6,6 @@ use std::path::Path; type RuleQueryResult = <::Query as Queryable>::Output; type RuleServiceBag = <::Query as Queryable>::Services; -#[derive(Clone)] -pub struct ServiceBagRuleOptionsWrapper(pub R::Options); - pub struct RuleContext<'a, R> where R: ?Sized + Rule, @@ -19,6 +16,7 @@ where services: RuleServiceBag, globals: &'a [&'a str], file_path: &'a Path, + options: &'a R::Options, } impl<'a, R> RuleContext<'a, R> @@ -31,6 +29,7 @@ where services: &'a ServiceBag, globals: &'a [&'a str], file_path: &'a Path, + options: &'a R::Options, ) -> Result { let rule_key = RuleKey::rule::(); Ok(Self { @@ -40,6 +39,7 @@ where services: FromServices::from_services(&rule_key, services)?, globals, file_path, + options, }) } @@ -90,11 +90,7 @@ where /// } /// ``` pub fn options(&self) -> &R::Options { - let ServiceBagRuleOptionsWrapper(options) = self - .bag - .get_service::>() - .unwrap(); - options + self.options } /// Checks whether the provided text belongs to globals diff --git a/crates/rome_analyze/src/lib.rs b/crates/rome_analyze/src/lib.rs index 4f29eeed0da..9df55ad1135 100644 --- a/crates/rome_analyze/src/lib.rs +++ b/crates/rome_analyze/src/lib.rs @@ -5,7 +5,6 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BinaryHeap}; use std::fmt::{Debug, Display, Formatter}; use std::ops; -use std::path::Path; mod categories; pub mod context; @@ -43,7 +42,6 @@ pub use crate::services::{FromServices, MissingServicesDiagnostic, ServiceBag}; pub use crate::signals::{AnalyzerAction, AnalyzerSignal, DiagnosticSignal}; pub use crate::syntax::{Ast, SyntaxVisitor}; pub use crate::visitor::{NodeVisitor, Visitor, VisitorContext, VisitorFinishContext}; -pub use rule::DeserializableRuleOptions; use rome_console::markup; use rome_diagnostics::{category, Applicability, Diagnostic, DiagnosticExt, DiagnosticTags}; @@ -77,8 +75,7 @@ pub struct AnalyzerContext<'a, L: Language> { pub root: LanguageRoot, pub services: ServiceBag, pub range: Option, - pub globals: &'a [&'a str], - pub file_path: &'a Path, + pub options: &'a AnalyzerOptions, } impl<'analyzer, L, Matcher, Break, Diag> Analyzer<'analyzer, L, Matcher, Break, Diag> @@ -142,9 +139,8 @@ where root: &ctx.root, services: &ctx.services, range: ctx.range, - globals: ctx.globals, apply_suppression_comment, - file_path: ctx.file_path, + options: ctx.options, }; // The first phase being run will inspect the tokens and parse the @@ -221,10 +217,8 @@ struct PhaseRunner<'analyzer, 'phase, L: Language, Matcher, Break, Diag> { services: &'phase ServiceBag, /// Optional text range to restrict the analysis to range: Option, - /// Options passed to the analyzer - globals: &'phase [&'phase str], - /// The [Path] of the current file - file_path: &'phase Path, + /// Analyzer options + options: &'phase AnalyzerOptions, } /// Single entry for a suppression comment in the `line_suppressions` buffer @@ -283,8 +277,7 @@ where query_matcher: self.query_matcher, signal_queue: &mut self.signal_queue, apply_suppression_comment: self.apply_suppression_comment, - globals: self.globals, - file_path: self.file_path, + options: self.options, }; visitor.visit(&node_event, ctx); @@ -309,8 +302,7 @@ where query_matcher: self.query_matcher, signal_queue: &mut self.signal_queue, apply_suppression_comment: self.apply_suppression_comment, - globals: self.globals, - file_path: self.file_path, + options: self.options, }; visitor.visit(&event, ctx); diff --git a/crates/rome_analyze/src/matcher.rs b/crates/rome_analyze/src/matcher.rs index 69f06dcabd4..a2e3df20fe6 100644 --- a/crates/rome_analyze/src/matcher.rs +++ b/crates/rome_analyze/src/matcher.rs @@ -1,9 +1,8 @@ use crate::{ - AnalyzerSignal, Phases, QueryMatch, Rule, RuleFilter, RuleGroup, ServiceBag, + AnalyzerOptions, AnalyzerSignal, Phases, QueryMatch, Rule, RuleFilter, RuleGroup, ServiceBag, SuppressionCommentEmitter, }; use rome_rowan::{Language, TextRange}; -use std::path::Path; use std::{ any::{Any, TypeId}, cmp::Ordering, @@ -27,8 +26,7 @@ pub struct MatchQueryParams<'phase, 'query, L: Language> { pub services: &'phase ServiceBag, pub signal_queue: &'query mut BinaryHeap>, pub apply_suppression_comment: SuppressionCommentEmitter, - pub globals: &'phase [&'phase str], - pub file_path: &'phase Path, + pub options: &'phase AnalyzerOptions, } /// Wrapper type for a [QueryMatch] @@ -198,24 +196,20 @@ where #[cfg(test)] mod tests { - use std::convert::Infallible; - use std::path::Path; - + use super::MatchQueryParams; + use crate::{ + signals::DiagnosticSignal, Analyzer, AnalyzerContext, AnalyzerSignal, ControlFlow, + MetadataRegistry, Never, Phases, QueryMatcher, RuleKey, ServiceBag, SignalEntry, + SyntaxVisitor, + }; + use crate::{AnalyzerOptions, SuppressionKind}; use rome_diagnostics::{category, DiagnosticExt}; use rome_diagnostics::{Diagnostic, Severity}; use rome_rowan::{ raw_language::{RawLanguage, RawLanguageKind, RawLanguageRoot, RawSyntaxTreeBuilder}, AstNode, SyntaxNode, TextRange, TextSize, TriviaPiece, TriviaPieceKind, }; - - use crate::SuppressionKind; - use crate::{ - signals::DiagnosticSignal, Analyzer, AnalyzerContext, AnalyzerSignal, ControlFlow, - MetadataRegistry, Never, Phases, QueryMatcher, RuleKey, ServiceBag, SignalEntry, - SyntaxVisitor, - }; - - use super::MatchQueryParams; + use std::convert::Infallible; struct SuppressionMatcher; @@ -383,8 +377,7 @@ mod tests { root, range: None, services: ServiceBag::default(), - globals: &[], - file_path: Path::new(""), + options: &AnalyzerOptions::default(), }; let result: Option = analyzer.run(ctx); diff --git a/crates/rome_analyze/src/options.rs b/crates/rome_analyze/src/options.rs index 3f897c12f24..d899f74c592 100644 --- a/crates/rome_analyze/src/options.rs +++ b/crates/rome_analyze/src/options.rs @@ -1,22 +1,28 @@ -use crate::RuleKey; -use serde::Deserialize; +use crate::{Rule, RuleKey}; +use std::any::{Any, TypeId}; use std::collections::HashMap; use std::fmt::Debug; use std::path::PathBuf; /// A convenient new type data structure to store the options that belong to a rule -#[derive(Debug, Default, Deserialize)] -pub struct RuleOptions(String); +#[derive(Debug)] +pub struct RuleOptions((TypeId, Box)); impl RuleOptions { /// It returns the deserialized rule option - pub fn value(&self) -> &String { - &self.0 + pub fn value(&self) -> &O { + let (type_id, value) = &self.0; + let current_id = TypeId::of::(); + debug_assert_eq!(type_id, ¤t_id); + // SAFETY: the code should fail when asserting the types. + // If the code throws an error here, it means that the developer didn't test + // the rule with the options + value.downcast_ref::().unwrap() } /// Creates a new [RuleOptions] - pub fn new(options: String) -> Self { - Self(options) + pub fn new(options: O) -> Self { + Self((TypeId::of::(), Box::new(options))) } } @@ -26,13 +32,13 @@ pub struct AnalyzerRules(HashMap); impl AnalyzerRules { /// It tracks the options of a specific rule - pub fn push_rule(&mut self, rule_key: RuleKey, options: String) { - self.0.insert(rule_key, RuleOptions::new(options)); + pub fn push_rule(&mut self, rule_key: RuleKey, options: RuleOptions) { + self.0.insert(rule_key, options); } /// It retrieves the options of a stored rule, given its name - pub fn get_rule(&self, rule_key: &RuleKey) -> Option<&RuleOptions> { - self.0.get(rule_key) + pub fn get_rule_options(&self, rule_key: &RuleKey) -> Option<&O> { + self.0.get(rule_key).map(|o| o.value::()) } } @@ -57,3 +63,23 @@ pub struct AnalyzerOptions { /// The file that is being analyzed pub file_path: PathBuf, } +impl AnalyzerOptions { + pub fn globals(&self) -> Vec<&str> { + self.configuration + .globals + .iter() + .map(|global| global.as_str()) + .collect() + } + + pub fn rule_options(&self) -> Option + where + R: Rule, + R::Options: Clone, + { + self.configuration + .rules + .get_rule_options::(&RuleKey::rule::()) + .map(R::Options::clone) + } +} diff --git a/crates/rome_analyze/src/registry.rs b/crates/rome_analyze/src/registry.rs index 4479410844a..80ccc4b551f 100644 --- a/crates/rome_analyze/src/registry.rs +++ b/crates/rome_analyze/src/registry.rs @@ -1,12 +1,11 @@ use crate::{ - context::{RuleContext, ServiceBagRuleOptionsWrapper}, + context::RuleContext, matcher::{GroupKey, MatchQueryParams}, query::{QueryKey, Queryable}, signals::RuleSignal, - AddVisitor, AnalysisFilter, AnalyzerOptions, DeserializableRuleOptions, GroupCategory, - QueryMatcher, Rule, RuleGroup, RuleKey, RuleMetadata, ServiceBag, SignalEntry, Visitor, + AddVisitor, AnalysisFilter, GroupCategory, QueryMatcher, Rule, RuleGroup, RuleKey, + RuleMetadata, ServiceBag, SignalEntry, Visitor, }; -use rome_deserialize::Deserialized; use rome_diagnostics::Error; use rome_rowan::{AstNode, Language, RawSyntaxKind, SyntaxKind, SyntaxNode}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -109,12 +108,10 @@ pub struct RuleRegistry { impl RuleRegistry { pub fn builder<'a>( filter: &'a AnalysisFilter<'a>, - options: &'a AnalyzerOptions, root: &'a L::Root, ) -> RuleRegistryBuilder<'a, L> { RuleRegistryBuilder { filter, - options, root, registry: RuleRegistry { phase_rules: Default::default(), @@ -142,7 +139,6 @@ enum TypeRules { pub struct RuleRegistryBuilder<'a, L: Language> { filter: &'a AnalysisFilter<'a>, - options: &'a AnalyzerOptions, root: &'a L::Root, // Rule Registry registry: RuleRegistry, @@ -170,6 +166,7 @@ impl RegistryVisitor for RuleRegistryBuilder fn record_rule(&mut self) where R: Rule + 'static, + ::Options: Default, R::Query: Queryable, ::Output: Clone, { @@ -222,27 +219,6 @@ impl RegistryVisitor for RuleRegistryBuilder phase.rule_states.push(RuleState::default()); - let rule_key = RuleKey::rule::(); - - let deserialized = - if let Some(options) = self.options.configuration.rules.get_rule(&rule_key) { - let value = options.value(); - ::from(value.to_string()) - } else { - Deserialized::new(::default(), vec![]) - }; - - if deserialized.has_errors() { - for error in deserialized.into_diagnostics() { - self.diagnostics.push(error) - } - } else { - self.services - .insert_service(ServiceBagRuleOptionsWrapper::( - deserialized.into_deserialized(), - )) - } - ::build_visitor(&mut self.visitors, self.root); } } @@ -403,6 +379,7 @@ impl RegistryRule { fn new(state_index: usize) -> Self where R: Rule + 'static, + ::Options: Default, R::Query: Queryable + 'static, ::Output: Clone, { @@ -415,6 +392,7 @@ impl RegistryRule { R: Rule + 'static, R::Query: 'static, ::Output: Clone, + ::Options: Default, { if let Some(node) = params.query.downcast_ref::>>() { if state.suppressions.inner.contains(node) { @@ -426,12 +404,15 @@ impl RegistryRule { // if the query doesn't match let query_result = params.query.downcast_ref().unwrap(); let query_result = ::unwrap_match(params.services, query_result); + let globals = params.options.globals(); + let options = params.options.rule_options::().unwrap_or_default(); let ctx = match RuleContext::new( &query_result, params.root, params.services, - params.globals, - params.file_path, + &globals, + ¶ms.options.file_path, + &options, ) { Ok(ctx) => ctx, Err(error) => return Err(error), @@ -449,8 +430,7 @@ impl RegistryRule { result, params.services, params.apply_suppression_comment, - params.globals, - params.file_path, + params.options, )); params.signal_queue.push(SignalEntry { diff --git a/crates/rome_analyze/src/rule.rs b/crates/rome_analyze/src/rule.rs index 6dbd26250fe..1275cce81e5 100644 --- a/crates/rome_analyze/src/rule.rs +++ b/crates/rome_analyze/src/rule.rs @@ -6,8 +6,6 @@ use crate::{ }; use rome_console::fmt::Display; use rome_console::{markup, MarkupBuf}; -use rome_deserialize::json::{deserialize_from_json_str, JsonDeserialize, VisitJsonNode}; -use rome_deserialize::Deserialized; use rome_diagnostics::advice::CodeSuggestionAdvice; use rome_diagnostics::location::AsSpan; use rome_diagnostics::Applicability; @@ -16,6 +14,7 @@ use rome_diagnostics::{ Visit, }; use rome_rowan::{AstNode, BatchMutation, BatchMutationExt, Language, TextRange}; +use std::fmt::Debug; /// Static metadata containing information about a rule pub struct RuleMetadata { @@ -236,24 +235,6 @@ impl_group_language!( T76, T77, T78, T79, T80, T81, T82, T83, T84, T85, T86, T87, T88, T89 ); -// pub trait DeserializableRuleOptions: Default + DeserializeOwned + Sized { -// fn try_from(value: String) -> Result { -// // parse_json(); -// } -// } - -pub trait DeserializableRuleOptions: Default + Sized + JsonDeserialize + VisitJsonNode { - fn from(value: String) -> Deserialized { - deserialize_from_json_str(&value) - } -} - -impl DeserializableRuleOptions for () { - fn from(_value: String) -> Deserialized { - Deserialized::new((), vec![]) - } -} - /// Trait implemented by all analysis rules: declares interest to a certain AstNode type, /// and a callback function to be executed on all nodes matching the query to possibly /// raise an analysis event @@ -269,7 +250,7 @@ pub trait Rule: RuleMeta + Sized { /// analyzer type Signals: IntoIterator; /// The options that belong to a rule - type Options: DeserializableRuleOptions; + type Options: Default + Clone + Debug; fn phase() -> Phases { <<::Query as Queryable>::Services as Phase>::phase() diff --git a/crates/rome_analyze/src/signals.rs b/crates/rome_analyze/src/signals.rs index ad322826f37..c4bcba546b5 100644 --- a/crates/rome_analyze/src/signals.rs +++ b/crates/rome_analyze/src/signals.rs @@ -4,7 +4,8 @@ use crate::{ context::RuleContext, registry::{RuleLanguage, RuleRoot}, rule::Rule, - AnalyzerDiagnostic, Queryable, RuleGroup, ServiceBag, SuppressionCommentEmitter, + AnalyzerDiagnostic, AnalyzerOptions, Queryable, RuleGroup, ServiceBag, + SuppressionCommentEmitter, }; use rome_console::MarkupBuf; use rome_diagnostics::{advice::CodeSuggestionAdvice, Applicability, CodeSuggestion, Error}; @@ -12,7 +13,6 @@ use rome_rowan::{BatchMutation, Language}; use std::borrow::Cow; use std::iter::FusedIterator; use std::marker::PhantomData; -use std::path::Path; use std::vec::IntoIter; /// Event raised by the analyzer when a [Rule](crate::Rule) @@ -245,8 +245,7 @@ pub(crate) struct RuleSignal<'phase, R: Rule> { /// An optional action to suppress the rule. apply_suppression_comment: SuppressionCommentEmitter>, /// A list of strings that are considered "globals" inside the analyzer - globals: &'phase [&'phase str], - file_path: &'phase Path, + options: &'phase AnalyzerOptions, } impl<'phase, R> RuleSignal<'phase, R> @@ -261,8 +260,7 @@ where apply_suppression_comment: SuppressionCommentEmitter< <::Query as Queryable>::Language, >, - globals: &'phase [&'phase str], - file_path: &'phase Path, + options: &'phase AnalyzerOptions, ) -> Self { Self { root, @@ -270,8 +268,7 @@ where state, services, apply_suppression_comment, - globals, - file_path, + options, } } } @@ -279,14 +276,18 @@ where impl<'bag, R> AnalyzerSignal> for RuleSignal<'bag, R> where R: Rule + 'static, + ::Options: Default, { fn diagnostic(&self) -> Option { + let globals = self.options.globals(); + let options = self.options.rule_options::().unwrap_or_default(); let ctx = RuleContext::new( &self.query_result, self.root, self.services, - self.globals, - self.file_path, + &globals, + &self.options.file_path, + &options, ) .ok()?; @@ -294,12 +295,15 @@ where } fn actions(&self) -> AnalyzerActionIter> { + let globals = self.options.globals(); + let options = self.options.rule_options::().unwrap_or_default(); let ctx = RuleContext::new( &self.query_result, self.root, self.services, - self.globals, - self.file_path, + &globals, + &self.options.file_path, + &options, ) .ok(); if let Some(ctx) = ctx { diff --git a/crates/rome_analyze/src/syntax.rs b/crates/rome_analyze/src/syntax.rs index d8abcad08e1..2f8176517f0 100644 --- a/crates/rome_analyze/src/syntax.rs +++ b/crates/rome_analyze/src/syntax.rs @@ -98,11 +98,11 @@ mod tests { AstNode, SyntaxNode, }; use std::convert::Infallible; - use std::path::Path; use crate::{ - matcher::MatchQueryParams, registry::Phases, Analyzer, AnalyzerContext, AnalyzerSignal, - ControlFlow, MetadataRegistry, Never, QueryMatcher, ServiceBag, SyntaxVisitor, + matcher::MatchQueryParams, registry::Phases, Analyzer, AnalyzerContext, AnalyzerOptions, + AnalyzerSignal, ControlFlow, MetadataRegistry, Never, QueryMatcher, ServiceBag, + SyntaxVisitor, }; #[derive(Default)] @@ -165,8 +165,7 @@ mod tests { root, range: None, services: ServiceBag::default(), - globals: &[], - file_path: Path::new(""), + options: &AnalyzerOptions::default(), }; let result: Option = analyzer.run(ctx); diff --git a/crates/rome_analyze/src/visitor.rs b/crates/rome_analyze/src/visitor.rs index e08b2bd7e76..871057d039a 100644 --- a/crates/rome_analyze/src/visitor.rs +++ b/crates/rome_analyze/src/visitor.rs @@ -1,13 +1,11 @@ -use std::collections::BinaryHeap; -use std::path::Path; - -use rome_rowan::{AstNode, Language, SyntaxNode, TextRange, WalkEvent}; - use crate::{ matcher::{MatchQueryParams, Query}, registry::{NodeLanguage, Phases}, - LanguageRoot, QueryMatch, QueryMatcher, ServiceBag, SignalEntry, SuppressionCommentEmitter, + AnalyzerOptions, LanguageRoot, QueryMatch, QueryMatcher, ServiceBag, SignalEntry, + SuppressionCommentEmitter, }; +use rome_rowan::{AstNode, Language, SyntaxNode, TextRange, WalkEvent}; +use std::collections::BinaryHeap; /// Mutable context objects shared by all visitors pub struct VisitorContext<'phase, 'query, L: Language> { @@ -18,8 +16,7 @@ pub struct VisitorContext<'phase, 'query, L: Language> { pub(crate) query_matcher: &'query mut dyn QueryMatcher, pub(crate) signal_queue: &'query mut BinaryHeap>, pub apply_suppression_comment: SuppressionCommentEmitter, - pub globals: &'phase [&'phase str], - pub file_path: &'phase Path, + pub options: &'phase AnalyzerOptions, } impl<'phase, 'query, L: Language> VisitorContext<'phase, 'query, L> { @@ -31,8 +28,7 @@ impl<'phase, 'query, L: Language> VisitorContext<'phase, 'query, L> { services: self.services, signal_queue: self.signal_queue, apply_suppression_comment: self.apply_suppression_comment, - globals: self.globals, - file_path: self.file_path, + options: self.options, }) } } diff --git a/crates/rome_deserialize/src/json.rs b/crates/rome_deserialize/src/json.rs index b050ba54089..af5dac500df 100644 --- a/crates/rome_deserialize/src/json.rs +++ b/crates/rome_deserialize/src/json.rs @@ -151,6 +151,46 @@ pub trait VisitJsonNode: VisitNode { } } + /// It attempts to map a [AnyJsonValue] to a [usize]. + /// + /// ## Errors + /// + /// It will fail if: + /// - `value` can't be cast to [JsonNumberValue] + /// - the value of the node can't be parsed to [usize] + fn map_to_usize( + &self, + value: &AnyJsonValue, + name: &str, + maximum: usize, + diagnostics: &mut Vec, + ) -> Option { + let value = JsonNumberValue::cast_ref(value.syntax()).or_else(|| { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( + name, + "number", + value.range(), + )); + None + })?; + let value = value.value_token().ok()?; + let result = value.text_trimmed().parse::().map_err(|err| { + emit_diagnostic_form_number( + err, + value.text_trimmed(), + value.text_trimmed_range(), + maximum, + ) + }); + match result { + Ok(number) => Some(number), + Err(err) => { + diagnostics.push(err); + None + } + } + } + /// It attempts to map a [AnyJsonValue] to a [u16]. /// /// ## Errors @@ -341,6 +381,35 @@ pub trait VisitJsonNode: VisitNode { } Some(()) } + + fn map_to_array( + &mut self, + value: &AnyJsonValue, + name: &str, + visitor: &mut V, + diagnostics: &mut Vec, + ) -> Option<()> + where + V: VisitNode, + { + let array = JsonArrayValue::cast_ref(value.syntax()).or_else(|| { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( + name, + "array", + value.range(), + )); + None + })?; + if array.elements().is_empty() { + return None; + } + for element in array.elements() { + let element = element.ok()?; + visitor.visit_array_member(element.syntax(), diagnostics); + } + + Some(()) + } } impl VisitJsonNode for () {} diff --git a/crates/rome_deserialize/src/visitor.rs b/crates/rome_deserialize/src/visitor.rs index 7cdcb375878..80c186f386c 100644 --- a/crates/rome_deserialize/src/visitor.rs +++ b/crates/rome_deserialize/src/visitor.rs @@ -32,6 +32,17 @@ pub trait VisitNode: Sized { ) -> Option<()> { unimplemented!("you should implement visit_map") } + + /// Called when visiting a list of elements. + /// + /// The implementor should loop through the list and call this function by passing the encountered nodes. + fn visit_array_member( + &mut self, + _element: &SyntaxNode, + _diagnostics: &mut Vec, + ) -> Option<()> { + unimplemented!("you should implement visit_array_member") + } } impl VisitNode for () { diff --git a/crates/rome_js_analyze/Cargo.toml b/crates/rome_js_analyze/Cargo.toml index 4927001552c..935f34abb30 100644 --- a/crates/rome_js_analyze/Cargo.toml +++ b/crates/rome_js_analyze/Cargo.toml @@ -26,6 +26,8 @@ serde = { version = "1.0.136", features = ["derive"] } serde_json = { version = "1.0.74", features = ["raw_value"] } lazy_static = { workspace = true } natord = "1.0.9" +bpaf.workspace = true +schemars = { workspace = true, optional = true } [dev-dependencies] tests_macros = { workspace = true } @@ -35,3 +37,8 @@ insta = { workspace = true, features = ["glob"] } countme = { workspace = true, features = ["enable"] } similar = "2.1.0" json_comments = "0.2.1" +rome_service = { path = "../rome_service"} +rome_json_parser = { path = "../rome_json_parser"} + +[features] +schemars = ["dep:schemars"] diff --git a/crates/rome_js_analyze/src/analyzers.rs b/crates/rome_js_analyze/src/analyzers.rs index 1dbf8367fac..be6415aa7f8 100644 --- a/crates/rome_js_analyze/src/analyzers.rs +++ b/crates/rome_js_analyze/src/analyzers.rs @@ -1,10 +1,10 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -mod a11y; -mod complexity; -mod correctness; -mod nursery; -mod performance; -mod style; -mod suspicious; +pub(crate) mod a11y; +pub(crate) mod complexity; +pub(crate) mod correctness; +pub(crate) mod nursery; +pub(crate) mod performance; +pub(crate) mod style; +pub(crate) mod suspicious; ::rome_analyze::declare_category! { pub (crate) Analyzers { kind : Lint , groups : [self :: a11y :: A11y , self :: complexity :: Complexity , self :: correctness :: Correctness , self :: nursery :: Nursery , self :: performance :: Performance , self :: style :: Style , self :: suspicious :: Suspicious ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/a11y.rs b/crates/rome_js_analyze/src/analyzers/a11y.rs index d488edb7cf6..07402f46ccf 100644 --- a/crates/rome_js_analyze/src/analyzers/a11y.rs +++ b/crates/rome_js_analyze/src/analyzers/a11y.rs @@ -1,19 +1,19 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_access_key; -mod no_auto_focus; -mod no_blank_target; -mod no_distracting_elements; -mod no_header_scope; -mod no_redundant_alt; -mod no_svg_without_title; -mod use_alt_text; -mod use_anchor_content; -mod use_html_lang; -mod use_iframe_title; -mod use_key_with_click_events; -mod use_key_with_mouse_events; -mod use_media_caption; -mod use_valid_anchor; +pub(crate) mod no_access_key; +pub(crate) mod no_auto_focus; +pub(crate) mod no_blank_target; +pub(crate) mod no_distracting_elements; +pub(crate) mod no_header_scope; +pub(crate) mod no_redundant_alt; +pub(crate) mod no_svg_without_title; +pub(crate) mod use_alt_text; +pub(crate) mod use_anchor_content; +pub(crate) mod use_html_lang; +pub(crate) mod use_iframe_title; +pub(crate) mod use_key_with_click_events; +pub(crate) mod use_key_with_mouse_events; +pub(crate) mod use_media_caption; +pub(crate) mod use_valid_anchor; declare_group! { pub (crate) A11y { name : "a11y" , rules : [self :: no_access_key :: NoAccessKey , self :: no_auto_focus :: NoAutoFocus , self :: no_blank_target :: NoBlankTarget , self :: no_distracting_elements :: NoDistractingElements , self :: no_header_scope :: NoHeaderScope , self :: no_redundant_alt :: NoRedundantAlt , self :: no_svg_without_title :: NoSvgWithoutTitle , self :: use_alt_text :: UseAltText , self :: use_anchor_content :: UseAnchorContent , self :: use_html_lang :: UseHtmlLang , self :: use_iframe_title :: UseIframeTitle , self :: use_key_with_click_events :: UseKeyWithClickEvents , self :: use_key_with_mouse_events :: UseKeyWithMouseEvents , self :: use_media_caption :: UseMediaCaption , self :: use_valid_anchor :: UseValidAnchor ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/complexity.rs b/crates/rome_js_analyze/src/analyzers/complexity.rs index a22cfba7e4b..21d76fd9e62 100644 --- a/crates/rome_js_analyze/src/analyzers/complexity.rs +++ b/crates/rome_js_analyze/src/analyzers/complexity.rs @@ -1,17 +1,17 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_extra_boolean_cast; -mod no_extra_semicolon; -mod no_multiple_spaces_in_regular_expression_literals; -mod no_useless_catch; -mod no_useless_constructor; -mod no_useless_label; -mod no_useless_rename; -mod no_useless_switch_case; -mod no_useless_type_constraint; -mod no_with; -mod use_flat_map; -mod use_optional_chain; -mod use_simplified_logic_expression; +pub(crate) mod no_extra_boolean_cast; +pub(crate) mod no_extra_semicolon; +pub(crate) mod no_multiple_spaces_in_regular_expression_literals; +pub(crate) mod no_useless_catch; +pub(crate) mod no_useless_constructor; +pub(crate) mod no_useless_label; +pub(crate) mod no_useless_rename; +pub(crate) mod no_useless_switch_case; +pub(crate) mod no_useless_type_constraint; +pub(crate) mod no_with; +pub(crate) mod use_flat_map; +pub(crate) mod use_optional_chain; +pub(crate) mod use_simplified_logic_expression; declare_group! { pub (crate) Complexity { name : "complexity" , rules : [self :: no_extra_boolean_cast :: NoExtraBooleanCast , self :: no_extra_semicolon :: NoExtraSemicolon , self :: no_multiple_spaces_in_regular_expression_literals :: NoMultipleSpacesInRegularExpressionLiterals , self :: no_useless_catch :: NoUselessCatch , self :: no_useless_constructor :: NoUselessConstructor , self :: no_useless_label :: NoUselessLabel , self :: no_useless_rename :: NoUselessRename , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_useless_type_constraint :: NoUselessTypeConstraint , self :: no_with :: NoWith , self :: use_flat_map :: UseFlatMap , self :: use_optional_chain :: UseOptionalChain , self :: use_simplified_logic_expression :: UseSimplifiedLogicExpression ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/correctness.rs b/crates/rome_js_analyze/src/analyzers/correctness.rs index ec5a06e4a21..216e1d10f75 100644 --- a/crates/rome_js_analyze/src/analyzers/correctness.rs +++ b/crates/rome_js_analyze/src/analyzers/correctness.rs @@ -1,22 +1,22 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_constructor_return; -mod no_empty_pattern; -mod no_inner_declarations; -mod no_invalid_constructor_super; -mod no_new_symbol; -mod no_precision_loss; -mod no_setter_return; -mod no_string_case_mismatch; -mod no_switch_declarations; -mod no_unnecessary_continue; -mod no_unreachable; -mod no_unreachable_super; -mod no_unsafe_finally; -mod no_unsafe_optional_chaining; -mod no_unused_labels; -mod no_void_type_return; -mod use_valid_for_direction; -mod use_yield; +pub(crate) mod no_constructor_return; +pub(crate) mod no_empty_pattern; +pub(crate) mod no_inner_declarations; +pub(crate) mod no_invalid_constructor_super; +pub(crate) mod no_new_symbol; +pub(crate) mod no_precision_loss; +pub(crate) mod no_setter_return; +pub(crate) mod no_string_case_mismatch; +pub(crate) mod no_switch_declarations; +pub(crate) mod no_unnecessary_continue; +pub(crate) mod no_unreachable; +pub(crate) mod no_unreachable_super; +pub(crate) mod no_unsafe_finally; +pub(crate) mod no_unsafe_optional_chaining; +pub(crate) mod no_unused_labels; +pub(crate) mod no_void_type_return; +pub(crate) mod use_valid_for_direction; +pub(crate) mod use_yield; declare_group! { pub (crate) Correctness { name : "correctness" , rules : [self :: no_constructor_return :: NoConstructorReturn , self :: no_empty_pattern :: NoEmptyPattern , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_new_symbol :: NoNewSymbol , self :: no_precision_loss :: NoPrecisionLoss , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_switch_declarations :: NoSwitchDeclarations , self :: no_unnecessary_continue :: NoUnnecessaryContinue , self :: no_unreachable :: NoUnreachable , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unsafe_optional_chaining :: NoUnsafeOptionalChaining , self :: no_unused_labels :: NoUnusedLabels , self :: no_void_type_return :: NoVoidTypeReturn , self :: use_valid_for_direction :: UseValidForDirection , self :: use_yield :: UseYield ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index d778633a50c..3e5149cabe9 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -1,15 +1,15 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_banned_types; -mod no_confusing_arrow; -mod no_duplicate_jsx_props; -mod no_for_each; -mod no_self_assign; -mod use_grouped_type_import; -mod use_heading_content; -mod use_is_nan; -mod use_literal_enum_members; -mod use_literal_keys; -mod use_simple_number_keys; +pub(crate) mod no_banned_types; +pub(crate) mod no_confusing_arrow; +pub(crate) mod no_duplicate_jsx_props; +pub(crate) mod no_for_each; +pub(crate) mod no_self_assign; +pub(crate) mod use_grouped_type_import; +pub(crate) mod use_heading_content; +pub(crate) mod use_is_nan; +pub(crate) mod use_literal_enum_members; +pub(crate) mod use_literal_keys; +pub(crate) mod use_simple_number_keys; declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_banned_types :: NoBannedTypes , self :: no_confusing_arrow :: NoConfusingArrow , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_for_each :: NoForEach , self :: no_self_assign :: NoSelfAssign , self :: use_grouped_type_import :: UseGroupedTypeImport , self :: use_heading_content :: UseHeadingContent , self :: use_is_nan :: UseIsNan , self :: use_literal_enum_members :: UseLiteralEnumMembers , self :: use_literal_keys :: UseLiteralKeys , self :: use_simple_number_keys :: UseSimpleNumberKeys ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/performance.rs b/crates/rome_js_analyze/src/analyzers/performance.rs index f28ba878261..d342c52e31f 100644 --- a/crates/rome_js_analyze/src/analyzers/performance.rs +++ b/crates/rome_js_analyze/src/analyzers/performance.rs @@ -1,5 +1,5 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_delete; +pub(crate) mod no_delete; declare_group! { pub (crate) Performance { name : "performance" , rules : [self :: no_delete :: NoDelete ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/style.rs b/crates/rome_js_analyze/src/analyzers/style.rs index 62098170af3..fe25257f310 100644 --- a/crates/rome_js_analyze/src/analyzers/style.rs +++ b/crates/rome_js_analyze/src/analyzers/style.rs @@ -1,23 +1,23 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_comma_operator; -mod no_implicit_boolean; -mod no_inferrable_types; -mod no_namespace; -mod no_negation_else; -mod no_non_null_assertion; -mod no_parameter_properties; -mod no_unused_template_literal; -mod use_block_statements; -mod use_default_parameter_last; -mod use_enum_initializers; -mod use_exponentiation_operator; -mod use_numeric_literals; -mod use_self_closing_elements; -mod use_shorthand_array_type; -mod use_single_case_statement; -mod use_single_var_declarator; -mod use_template; -mod use_while; +pub(crate) mod no_comma_operator; +pub(crate) mod no_implicit_boolean; +pub(crate) mod no_inferrable_types; +pub(crate) mod no_namespace; +pub(crate) mod no_negation_else; +pub(crate) mod no_non_null_assertion; +pub(crate) mod no_parameter_properties; +pub(crate) mod no_unused_template_literal; +pub(crate) mod use_block_statements; +pub(crate) mod use_default_parameter_last; +pub(crate) mod use_enum_initializers; +pub(crate) mod use_exponentiation_operator; +pub(crate) mod use_numeric_literals; +pub(crate) mod use_self_closing_elements; +pub(crate) mod use_shorthand_array_type; +pub(crate) mod use_single_case_statement; +pub(crate) mod use_single_var_declarator; +pub(crate) mod use_template; +pub(crate) mod use_while; declare_group! { pub (crate) Style { name : "style" , rules : [self :: no_comma_operator :: NoCommaOperator , self :: no_implicit_boolean :: NoImplicitBoolean , self :: no_inferrable_types :: NoInferrableTypes , self :: no_namespace :: NoNamespace , self :: no_negation_else :: NoNegationElse , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_parameter_properties :: NoParameterProperties , self :: no_unused_template_literal :: NoUnusedTemplateLiteral , self :: use_block_statements :: UseBlockStatements , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_numeric_literals :: UseNumericLiterals , self :: use_self_closing_elements :: UseSelfClosingElements , self :: use_shorthand_array_type :: UseShorthandArrayType , self :: use_single_case_statement :: UseSingleCaseStatement , self :: use_single_var_declarator :: UseSingleVarDeclarator , self :: use_template :: UseTemplate , self :: use_while :: UseWhile ,] } } diff --git a/crates/rome_js_analyze/src/analyzers/suspicious.rs b/crates/rome_js_analyze/src/analyzers/suspicious.rs index aa519ee401f..80e87bc284d 100644 --- a/crates/rome_js_analyze/src/analyzers/suspicious.rs +++ b/crates/rome_js_analyze/src/analyzers/suspicious.rs @@ -1,27 +1,27 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_assign_in_expressions; -mod no_async_promise_executor; -mod no_comment_text; -mod no_compare_neg_zero; -mod no_confusing_labels; -mod no_const_enum; -mod no_debugger; -mod no_double_equals; -mod no_duplicate_case; -mod no_duplicate_class_members; -mod no_duplicate_object_keys; -mod no_empty_interface; -mod no_explicit_any; -mod no_extra_non_null_assertion; -mod no_prototype_builtins; -mod no_redundant_use_strict; -mod no_self_compare; -mod no_shadow_restricted_names; -mod no_sparse_array; -mod no_unsafe_negation; -mod use_default_switch_clause_last; -mod use_namespace_keyword; -mod use_valid_typeof; +pub(crate) mod no_assign_in_expressions; +pub(crate) mod no_async_promise_executor; +pub(crate) mod no_comment_text; +pub(crate) mod no_compare_neg_zero; +pub(crate) mod no_confusing_labels; +pub(crate) mod no_const_enum; +pub(crate) mod no_debugger; +pub(crate) mod no_double_equals; +pub(crate) mod no_duplicate_case; +pub(crate) mod no_duplicate_class_members; +pub(crate) mod no_duplicate_object_keys; +pub(crate) mod no_empty_interface; +pub(crate) mod no_explicit_any; +pub(crate) mod no_extra_non_null_assertion; +pub(crate) mod no_prototype_builtins; +pub(crate) mod no_redundant_use_strict; +pub(crate) mod no_self_compare; +pub(crate) mod no_shadow_restricted_names; +pub(crate) mod no_sparse_array; +pub(crate) mod no_unsafe_negation; +pub(crate) mod use_default_switch_clause_last; +pub(crate) mod use_namespace_keyword; +pub(crate) mod use_valid_typeof; declare_group! { pub (crate) Suspicious { name : "suspicious" , rules : [self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_async_promise_executor :: NoAsyncPromiseExecutor , self :: no_comment_text :: NoCommentText , self :: no_compare_neg_zero :: NoCompareNegZero , self :: no_confusing_labels :: NoConfusingLabels , self :: no_const_enum :: NoConstEnum , self :: no_debugger :: NoDebugger , self :: no_double_equals :: NoDoubleEquals , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_class_members :: NoDuplicateClassMembers , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_explicit_any :: NoExplicitAny , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_shadow_restricted_names :: NoShadowRestrictedNames , self :: no_sparse_array :: NoSparseArray , self :: no_unsafe_negation :: NoUnsafeNegation , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_namespace_keyword :: UseNamespaceKeyword , self :: use_valid_typeof :: UseValidTypeof ,] } } diff --git a/crates/rome_js_analyze/src/aria_analyzers.rs b/crates/rome_js_analyze/src/aria_analyzers.rs index b116233f52b..1244ed1b5ac 100644 --- a/crates/rome_js_analyze/src/aria_analyzers.rs +++ b/crates/rome_js_analyze/src/aria_analyzers.rs @@ -1,5 +1,5 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -mod a11y; -mod nursery; +pub(crate) mod a11y; +pub(crate) mod nursery; ::rome_analyze::declare_category! { pub (crate) AriaAnalyzers { kind : Lint , groups : [self :: a11y :: A11y , self :: nursery :: Nursery ,] } } diff --git a/crates/rome_js_analyze/src/aria_analyzers/a11y.rs b/crates/rome_js_analyze/src/aria_analyzers/a11y.rs index e85a7ac2c87..5765952a8fd 100644 --- a/crates/rome_js_analyze/src/aria_analyzers/a11y.rs +++ b/crates/rome_js_analyze/src/aria_analyzers/a11y.rs @@ -1,8 +1,8 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_noninteractive_element_to_interactive_role; -mod use_aria_props_for_role; -mod use_valid_aria_props; -mod use_valid_lang; +pub(crate) mod no_noninteractive_element_to_interactive_role; +pub(crate) mod use_aria_props_for_role; +pub(crate) mod use_valid_aria_props; +pub(crate) mod use_valid_lang; declare_group! { pub (crate) A11y { name : "a11y" , rules : [self :: no_noninteractive_element_to_interactive_role :: NoNoninteractiveElementToInteractiveRole , self :: use_aria_props_for_role :: UseAriaPropsForRole , self :: use_valid_aria_props :: UseValidAriaProps , self :: use_valid_lang :: UseValidLang ,] } } diff --git a/crates/rome_js_analyze/src/aria_analyzers/nursery.rs b/crates/rome_js_analyze/src/aria_analyzers/nursery.rs index 5d10fd49299..4bc2db45d04 100644 --- a/crates/rome_js_analyze/src/aria_analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/aria_analyzers/nursery.rs @@ -1,8 +1,8 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_aria_unsupported_elements; -mod no_noninteractive_tabindex; -mod no_redundant_roles; -mod use_aria_prop_types; +pub(crate) mod no_aria_unsupported_elements; +pub(crate) mod no_noninteractive_tabindex; +pub(crate) mod no_redundant_roles; +pub(crate) mod use_aria_prop_types; declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_aria_unsupported_elements :: NoAriaUnsupportedElements , self :: no_noninteractive_tabindex :: NoNoninteractiveTabindex , self :: no_redundant_roles :: NoRedundantRoles , self :: use_aria_prop_types :: UseAriaPropTypes ,] } } diff --git a/crates/rome_js_analyze/src/assists.rs b/crates/rome_js_analyze/src/assists.rs index 19fda5f0b56..66e071e3463 100644 --- a/crates/rome_js_analyze/src/assists.rs +++ b/crates/rome_js_analyze/src/assists.rs @@ -1,4 +1,4 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -mod correctness; +pub(crate) mod correctness; ::rome_analyze::declare_category! { pub (crate) Assists { kind : Action , groups : [self :: correctness :: Correctness ,] } } diff --git a/crates/rome_js_analyze/src/assists/correctness.rs b/crates/rome_js_analyze/src/assists/correctness.rs index 37dd41f8b65..965a10b098b 100644 --- a/crates/rome_js_analyze/src/assists/correctness.rs +++ b/crates/rome_js_analyze/src/assists/correctness.rs @@ -1,7 +1,7 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod flip_bin_exp; -mod inline_variable; -mod organize_imports; +pub(crate) mod flip_bin_exp; +pub(crate) mod inline_variable; +pub(crate) mod organize_imports; declare_group! { pub (crate) Correctness { name : "correctness" , rules : [self :: flip_bin_exp :: FlipBinExp , self :: inline_variable :: InlineVariable , self :: organize_imports :: OrganizeImports ,] } } diff --git a/crates/rome_js_analyze/src/lib.rs b/crates/rome_js_analyze/src/lib.rs index f9edf370502..fb57ac0ac96 100644 --- a/crates/rome_js_analyze/src/lib.rs +++ b/crates/rome_js_analyze/src/lib.rs @@ -19,6 +19,7 @@ mod assists; mod ast_utils; mod control_flow; pub mod globals; +pub mod options; mod react; mod registry; mod semantic_analyzers; @@ -97,7 +98,7 @@ where result } - let mut registry = RuleRegistry::builder(&filter, options, root); + let mut registry = RuleRegistry::builder(&filter, root); visit_registry(&mut registry); let (registry, mut services, diagnostics, visitors) = registry.build(); @@ -122,19 +123,12 @@ where services.insert_service(Arc::new(AriaRoles::default())); services.insert_service(Arc::new(AriaProperties::default())); services.insert_service(source_type); - let globals: Vec<_> = options - .configuration - .globals - .iter() - .map(|global| global.as_str()) - .collect(); ( analyzer.run(AnalyzerContext { root: root.clone(), range: filter.range, services, - globals: globals.as_slice(), - file_path: options.file_path.as_path(), + options, }), diagnostics, ) @@ -159,7 +153,8 @@ where #[cfg(test)] mod tests { - use rome_analyze::{AnalyzerOptions, Never, RuleCategories, RuleFilter}; + use rome_analyze::options::RuleOptions; + use rome_analyze::{AnalyzerOptions, Never, RuleCategories, RuleFilter, RuleKey}; use rome_console::fmt::{Formatter, Termcolor}; use rome_console::{markup, Markup}; use rome_diagnostics::category; @@ -169,6 +164,7 @@ mod tests { use rome_js_syntax::{JsFileSource, TextRange, TextSize}; use std::slice; + use crate::semantic_analyzers::nursery::use_exhaustive_dependencies::{Hooks, HooksOptions}; use crate::{analyze, AnalysisFilter, ControlFlow}; #[ignore] @@ -183,13 +179,28 @@ mod tests { String::from_utf8(buffer).unwrap() } - const SOURCE: &str = r#"a.b["c"];"#; + const SOURCE: &str = r#"function f() { + if (true){ + useMyEffect(() => {}, []); + + } + }"#; let parsed = parse(SOURCE, JsFileSource::tsx()); let mut error_ranges: Vec = Vec::new(); - let options = AnalyzerOptions::default(); - let rule_filter = RuleFilter::Rule("nursery", "useLiteralKeys"); + let mut options = AnalyzerOptions::default(); + let hook = Hooks { + name: "myEffect".to_string(), + closure_index: Some(0), + dependencies_index: Some(1), + }; + let rule_filter = RuleFilter::Rule("nursery", "useHookAtTopLevel"); + options.configuration.rules.push_rule( + RuleKey::new("nursery", "useHookAtTopLevel"), + RuleOptions::new(HooksOptions { hooks: vec![hook] }), + ); + analyze( &parsed.tree(), AnalysisFilter { diff --git a/crates/rome_js_analyze/src/options.rs b/crates/rome_js_analyze/src/options.rs new file mode 100644 index 00000000000..6ed938707df --- /dev/null +++ b/crates/rome_js_analyze/src/options.rs @@ -0,0 +1,83 @@ +//! This module contains the rules that have options + +use crate::semantic_analyzers::nursery::use_exhaustive_dependencies::{ + hooks_options, HooksOptions, +}; +use bpaf::Bpaf; +use rome_analyze::options::RuleOptions; +use rome_analyze::RuleKey; +use rome_deserialize::json::{has_only_known_keys, VisitJsonNode}; +use rome_deserialize::{DeserializationDiagnostic, VisitNode}; +use rome_json_syntax::{JsonLanguage, JsonSyntaxNode}; +use rome_rowan::SyntaxNode; +#[cfg(feature = "schemars")] +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Default, Deserialize, Serialize, Debug, Clone, Bpaf)] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, untagged)] +pub enum PossibleOptions { + /// Options for `useExhaustiveDependencies` and `useHookAtTopLevel` rule + Hooks(#[bpaf(external(hooks_options), hide)] HooksOptions), + /// No options available + #[default] + NoOptions, +} + +impl FromStr for PossibleOptions { + type Err = (); + + fn from_str(_s: &str) -> Result { + Ok(Self::NoOptions) + } +} + +impl PossibleOptions { + const KNOWN_KEYS: &'static [&'static str] = &["hooks"]; + + pub fn extract_option(&self, rule_key: &RuleKey) -> RuleOptions { + match rule_key.rule_name() { + "useExhaustiveDependencies" | "useHookAtTopLevel" => { + let options = match self { + PossibleOptions::Hooks(hooks) => hooks.clone(), + _ => HooksOptions::default(), + }; + RuleOptions::new(options) + } + + // TODO: review error + _ => panic!("This rule {:?} doesn't have options", rule_key), + } + } +} + +impl VisitJsonNode for PossibleOptions {} +impl VisitNode for PossibleOptions { + fn visit_member_name( + &mut self, + node: &JsonSyntaxNode, + diagnostics: &mut Vec, + ) -> Option<()> { + has_only_known_keys(node, PossibleOptions::KNOWN_KEYS, diagnostics) + } + + fn visit_map( + &mut self, + key: &SyntaxNode, + value: &SyntaxNode, + diagnostics: &mut Vec, + ) -> Option<()> { + let (name, value) = self.get_key_and_value(key, value, diagnostics)?; + let name_text = name.text(); + + if name_text == "hooks" { + let mut options = HooksOptions::default(); + self.map_to_array(&value, &name, &mut options, diagnostics); + *self = PossibleOptions::Hooks(options); + } + + Some(()) + } +} diff --git a/crates/rome_js_analyze/src/semantic_analyzers.rs b/crates/rome_js_analyze/src/semantic_analyzers.rs index d2e02c63131..495eb0e1a05 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers.rs @@ -1,10 +1,10 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -mod a11y; -mod complexity; -mod correctness; -mod nursery; -mod security; -mod style; -mod suspicious; +pub(crate) mod a11y; +pub(crate) mod complexity; +pub(crate) mod correctness; +pub(crate) mod nursery; +pub(crate) mod security; +pub(crate) mod style; +pub(crate) mod suspicious; ::rome_analyze::declare_category! { pub (crate) SemanticAnalyzers { kind : Lint , groups : [self :: a11y :: A11y , self :: complexity :: Complexity , self :: correctness :: Correctness , self :: nursery :: Nursery , self :: security :: Security , self :: style :: Style , self :: suspicious :: Suspicious ,] } } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/a11y.rs b/crates/rome_js_analyze/src/semantic_analyzers/a11y.rs index f000972b065..f3b15e8a5dc 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/a11y.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/a11y.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_positive_tabindex; -mod use_button_type; +pub(crate) mod no_positive_tabindex; +pub(crate) mod use_button_type; declare_group! { pub (crate) A11y { name : "a11y" , rules : [self :: no_positive_tabindex :: NoPositiveTabindex , self :: use_button_type :: UseButtonType ,] } } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/complexity.rs b/crates/rome_js_analyze/src/semantic_analyzers/complexity.rs index 44c787d0c92..2837a78efa2 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/complexity.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/complexity.rs @@ -1,5 +1,5 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_useless_fragments; +pub(crate) mod no_useless_fragments; declare_group! { pub (crate) Complexity { name : "complexity" , rules : [self :: no_useless_fragments :: NoUselessFragments ,] } } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/correctness.rs b/crates/rome_js_analyze/src/semantic_analyzers/correctness.rs index 0b80e51023f..66fea861b45 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/correctness.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/correctness.rs @@ -1,11 +1,11 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_children_prop; -mod no_const_assign; -mod no_global_object_calls; -mod no_render_return_value; -mod no_undeclared_variables; -mod no_unused_variables; -mod no_void_elements_with_children; +pub(crate) mod no_children_prop; +pub(crate) mod no_const_assign; +pub(crate) mod no_global_object_calls; +pub(crate) mod no_render_return_value; +pub(crate) mod no_undeclared_variables; +pub(crate) mod no_unused_variables; +pub(crate) mod no_void_elements_with_children; declare_group! { pub (crate) Correctness { name : "correctness" , rules : [self :: no_children_prop :: NoChildrenProp , self :: no_const_assign :: NoConstAssign , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_render_return_value :: NoRenderReturnValue , self :: no_undeclared_variables :: NoUndeclaredVariables , self :: no_unused_variables :: NoUnusedVariables , self :: no_void_elements_with_children :: NoVoidElementsWithChildren ,] } } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs b/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs index 4b681b226fc..129f6c222d0 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs @@ -1,10 +1,10 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_accumulating_spread; -mod no_console_log; -mod no_constant_condition; -mod use_camel_case; -mod use_exhaustive_dependencies; -mod use_hook_at_top_level; +pub(crate) mod no_accumulating_spread; +pub(crate) mod no_console_log; +pub(crate) mod no_constant_condition; +pub(crate) mod use_camel_case; +pub(crate) mod use_exhaustive_dependencies; +pub(crate) mod use_hook_at_top_level; declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_accumulating_spread :: NoAccumulatingSpread , self :: no_console_log :: NoConsoleLog , self :: no_constant_condition :: NoConstantCondition , self :: use_camel_case :: UseCamelCase , self :: use_exhaustive_dependencies :: UseExhaustiveDependencies , self :: use_hook_at_top_level :: UseHookAtTopLevel ,] } } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_exhaustive_dependencies.rs b/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_exhaustive_dependencies.rs index 357d085cf0a..b90a750d451 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_exhaustive_dependencies.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_exhaustive_dependencies.rs @@ -1,21 +1,23 @@ use crate::react::hooks::*; use crate::semantic_services::Semantic; -use rome_analyze::{ - context::RuleContext, declare_rule, DeserializableRuleOptions, Rule, RuleDiagnostic, -}; +use bpaf::Bpaf; +use rome_analyze::{context::RuleContext, declare_rule, Rule, RuleDiagnostic}; use rome_console::markup; -use rome_deserialize::json::{ - deserialize_from_json_str, has_only_known_keys, JsonDeserialize, VisitJsonNode, -}; -use rome_deserialize::{DeserializationDiagnostic, Deserialized, VisitNode}; +use rome_deserialize::json::{has_only_known_keys, VisitJsonNode}; +use rome_deserialize::{DeserializationDiagnostic, VisitNode}; use rome_js_semantic::{Capture, SemanticModel}; use rome_js_syntax::{ binding_ext::AnyJsBindingDeclaration, JsCallExpression, JsStaticMemberExpression, JsSyntaxKind, JsSyntaxNode, JsVariableDeclaration, TextRange, }; -use rome_json_syntax::{JsonLanguage, JsonRoot, JsonSyntaxNode}; -use rome_rowan::{AstNode, AstSeparatedList, SyntaxNodeCast}; +use rome_json_syntax::{AnyJsonValue, JsonLanguage, JsonSyntaxNode}; +use rome_rowan::{AstNode, AstSeparatedList, SyntaxNode, SyntaxNodeCast}; +use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap, HashSet}; +use std::str::FromStr; + +#[cfg(feature = "schemars")] +use schemars::JsonSchema; declare_rule! { /// Enforce all dependencies are correctly specified. @@ -100,18 +102,13 @@ declare_rule! { /// "//": "...", /// "options": { /// "hooks": [ - /// ["useLocation", 0, 1], - /// ["useQuery", 1, 0] + /// { "name": "useLocation", "closureIndex": 0, "dependenciesIndex": 1}, + /// { "name": "useQuery", "closureIndex": 1, "dependenciesIndex": 0} /// ] /// } /// } /// ``` /// - /// The following items mean: - /// 1. the name of the hook - /// 2. the index of the closure - /// 3. the index of the array of dependencies - /// /// Given the previous example, your hooks be used like this: /// /// ```js @@ -183,8 +180,88 @@ impl Default for ReactExtensiveDependenciesOptions { } } -#[derive(Debug, Default, Clone)] -pub struct HooksOptions(Vec<(String, Option, Option)>); +/// Options for the rule `useExhaustiveDependencies` and `useHookAtTopLevel` +#[derive(Default, Deserialize, Serialize, Debug, Clone, Bpaf)] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct HooksOptions { + #[bpaf(external, hide, many)] + /// List of safe hooks + pub hooks: Vec, +} + +impl FromStr for HooksOptions { + type Err = (); + + fn from_str(_s: &str) -> Result { + Ok(HooksOptions::default()) + } +} + +#[derive(Default, Deserialize, Serialize, Debug, Clone, Bpaf)] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Hooks { + #[bpaf(hide)] + /// The name of the hook + pub name: String, + #[bpaf(hide)] + /// The "position" of the closure function, starting from zero. + /// + /// ### Example + pub closure_index: Option, + #[bpaf(hide)] + /// The "position" of the array of dependencies, starting from zero. + pub dependencies_index: Option, +} + +impl Hooks { + const KNOWN_KEYS: &'static [&'static str] = &["name", "closureIndex", "dependenciesIndex"]; +} + +impl VisitJsonNode for Hooks {} +impl VisitNode for Hooks { + fn visit_member_name( + &mut self, + node: &JsonSyntaxNode, + diagnostics: &mut Vec, + ) -> Option<()> { + has_only_known_keys(node, Hooks::KNOWN_KEYS, diagnostics) + } + + fn visit_map( + &mut self, + key: &JsonSyntaxNode, + value: &JsonSyntaxNode, + diagnostics: &mut Vec, + ) -> Option<()> { + let (name, value) = self.get_key_and_value(key, value, diagnostics)?; + let name_text = name.text(); + match name_text { + "name" => { + self.name = self.map_to_string(&value, name_text, diagnostics)?; + } + "closureIndex" => { + self.closure_index = self.map_to_usize(&value, name_text, usize::MAX, diagnostics); + } + "dependenciesIndex" => { + self.dependencies_index = + self.map_to_usize(&value, name_text, usize::MAX, diagnostics); + } + _ => {} + } + + Some(()) + } +} + +impl FromStr for Hooks { + type Err = (); + + fn from_str(_s: &str) -> Result { + Ok(Hooks::default()) + } +} impl VisitJsonNode for HooksOptions {} impl VisitNode for HooksOptions { @@ -196,6 +273,27 @@ impl VisitNode for HooksOptions { has_only_known_keys(node, &["hooks"], diagnostics) } + fn visit_array_member( + &mut self, + element: &SyntaxNode, + diagnostics: &mut Vec, + ) -> Option<()> { + let mut hook = Hooks::default(); + let element = AnyJsonValue::cast(element.clone())?; + self.map_to_object(&element, "hooks", &mut hook, diagnostics)?; + if hook.name.is_empty() { + diagnostics.push( + DeserializationDiagnostic::new(markup!( + "The field ""name"" is mandatory" + )) + .with_range(element.range()), + ) + } else { + self.hooks.push(hook); + } + Some(()) + } + fn visit_map( &mut self, key: &JsonSyntaxNode, @@ -253,49 +351,26 @@ impl VisitNode for HooksOptions { None }; - self.0.push((hook_name, closure_index, dependencies_index)); + self.hooks.push(Hooks { + name: hook_name, + closure_index, + dependencies_index, + }); } } Some(()) } } -impl JsonDeserialize for HooksOptions { - fn deserialize_from_ast( - root: JsonRoot, - visitor: &mut impl VisitJsonNode, - diagnostics: &mut Vec, - ) -> Option<()> { - let object = root.value().ok()?; - let object = object.as_json_object_value()?; - for element in object.json_member_list() { - let element = element.ok()?; - visitor.visit_map( - element.name().ok()?.syntax(), - element.value().ok()?.syntax(), - diagnostics, - )?; - } - - Some(()) - } -} - -impl DeserializableRuleOptions for HooksOptions { - fn from(value: String) -> Deserialized { - deserialize_from_json_str(&value) - } -} - impl ReactExtensiveDependenciesOptions { pub fn new(hooks: HooksOptions) -> Self { let mut default = ReactExtensiveDependenciesOptions::default(); - for (k, closure_index, dependencies_index) in hooks.0.into_iter() { + for hook in hooks.hooks.into_iter() { default.hooks_config.insert( - k, + hook.name, ReactHookConfiguration { - closure_index, - dependencies_index, + closure_index: hook.closure_index, + dependencies_index: hook.dependencies_index, }, ); } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_hook_at_top_level.rs b/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_hook_at_top_level.rs index d551b7206c9..7aa571bfcff 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_hook_at_top_level.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_hook_at_top_level.rs @@ -1,3 +1,5 @@ +use super::use_exhaustive_dependencies::ReactExtensiveDependenciesOptions; +use crate::semantic_analyzers::nursery::use_exhaustive_dependencies::HooksOptions; use crate::{react::hooks::react_hook_configuration, semantic_services::Semantic}; use rome_analyze::{context::RuleContext, declare_rule, Rule, RuleDiagnostic}; use rome_console::markup; @@ -5,8 +7,6 @@ use rome_js_semantic::CallsExtensions; use rome_js_syntax::{AnyJsFunction, JsCallExpression, JsFunctionBody, JsSyntaxKind, TextRange}; use rome_rowan::AstNode; -use super::use_exhaustive_dependencies::{HooksOptions, ReactExtensiveDependenciesOptions}; - declare_rule! { /// Enforce that all React hooks are being called from the Top Level /// component functions. @@ -33,6 +33,31 @@ declare_rule! { /// } /// ``` /// + /// ## Options + /// + /// Allows to specify custom hooks - from libraries or internal projects - that can be considered stable. + /// + /// ```json + /// { + /// "//": "...", + /// "options": { + /// "hooks": [ + /// { "name": "useLocation", "closureIndex": 0, "dependenciesIndex": 1}, + /// { "name": "useQuery", "closureIndex": 1, "dependenciesIndex": 0} + /// ] + /// } + /// } + /// ``` + /// + /// Given the previous example, your hooks be used like this: + /// + /// ```js + /// function Foo() { + /// const location = useLocation(() => {}, []); + /// const query = useQuery([], () => {}); + /// } + /// ``` + /// pub(crate) UseHookAtTopLevel { version: "12.0.0", name: "useHookAtTopLevel", diff --git a/crates/rome_js_analyze/src/semantic_analyzers/security.rs b/crates/rome_js_analyze/src/semantic_analyzers/security.rs index 489a5086ae7..35e088862d5 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/security.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/security.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_dangerously_set_inner_html; -mod no_dangerously_set_inner_html_with_children; +pub(crate) mod no_dangerously_set_inner_html; +pub(crate) mod no_dangerously_set_inner_html_with_children; declare_group! { pub (crate) Security { name : "security" , rules : [self :: no_dangerously_set_inner_html :: NoDangerouslySetInnerHtml , self :: no_dangerously_set_inner_html_with_children :: NoDangerouslySetInnerHtmlWithChildren ,] } } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/style.rs b/crates/rome_js_analyze/src/semantic_analyzers/style.rs index d6121e1ca50..c73896883d6 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/style.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/style.rs @@ -1,11 +1,11 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_arguments; -mod no_parameter_assign; -mod no_restricted_globals; -mod no_shouty_constants; -mod no_var; -mod use_const; -mod use_fragment_syntax; +pub(crate) mod no_arguments; +pub(crate) mod no_parameter_assign; +pub(crate) mod no_restricted_globals; +pub(crate) mod no_shouty_constants; +pub(crate) mod no_var; +pub(crate) mod use_const; +pub(crate) mod use_fragment_syntax; declare_group! { pub (crate) Style { name : "style" , rules : [self :: no_arguments :: NoArguments , self :: no_parameter_assign :: NoParameterAssign , self :: no_restricted_globals :: NoRestrictedGlobals , self :: no_shouty_constants :: NoShoutyConstants , self :: no_var :: NoVar , self :: use_const :: UseConst , self :: use_fragment_syntax :: UseFragmentSyntax ,] } } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/suspicious.rs b/crates/rome_js_analyze/src/semantic_analyzers/suspicious.rs index 545b072dabc..f109d983b77 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/suspicious.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/suspicious.rs @@ -1,12 +1,12 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_array_index_key; -mod no_catch_assign; -mod no_class_assign; -mod no_duplicate_parameters; -mod no_function_assign; -mod no_import_assign; -mod no_label_var; -mod no_redeclare; +pub(crate) mod no_array_index_key; +pub(crate) mod no_catch_assign; +pub(crate) mod no_class_assign; +pub(crate) mod no_duplicate_parameters; +pub(crate) mod no_function_assign; +pub(crate) mod no_import_assign; +pub(crate) mod no_label_var; +pub(crate) mod no_redeclare; declare_group! { pub (crate) Suspicious { name : "suspicious" , rules : [self :: no_array_index_key :: NoArrayIndexKey , self :: no_catch_assign :: NoCatchAssign , self :: no_class_assign :: NoClassAssign , self :: no_duplicate_parameters :: NoDuplicateParameters , self :: no_function_assign :: NoFunctionAssign , self :: no_import_assign :: NoImportAssign , self :: no_label_var :: NoLabelVar , self :: no_redeclare :: NoRedeclare ,] } } diff --git a/crates/rome_js_analyze/src/syntax.rs b/crates/rome_js_analyze/src/syntax.rs index 763fda77bae..d04df705751 100644 --- a/crates/rome_js_analyze/src/syntax.rs +++ b/crates/rome_js_analyze/src/syntax.rs @@ -1,4 +1,4 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -mod nursery; +pub(crate) mod nursery; ::rome_analyze::declare_category! { pub (crate) Syntax { kind : Syntax , groups : [self :: nursery :: Nursery ,] } } diff --git a/crates/rome_js_analyze/src/syntax/nursery.rs b/crates/rome_js_analyze/src/syntax/nursery.rs index e99eb64303d..dd190604368 100644 --- a/crates/rome_js_analyze/src/syntax/nursery.rs +++ b/crates/rome_js_analyze/src/syntax/nursery.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; -mod no_duplicate_private_class_members; -mod no_super_without_extends; +pub(crate) mod no_duplicate_private_class_members; +pub(crate) mod no_super_without_extends; declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_duplicate_private_class_members :: NoDuplicatePrivateClassMembers , self :: no_super_without_extends :: NoSuperWithoutExtends ,] } } diff --git a/crates/rome_js_analyze/tests/spec_tests.rs b/crates/rome_js_analyze/tests/spec_tests.rs index 11df489f559..1c48f01650a 100644 --- a/crates/rome_js_analyze/tests/spec_tests.rs +++ b/crates/rome_js_analyze/tests/spec_tests.rs @@ -1,11 +1,12 @@ use json_comments::StripComments; use rome_analyze::{ - AnalysisFilter, AnalyzerAction, AnalyzerOptions, ControlFlow, Never, RuleFilter, RuleKey, + AnalysisFilter, AnalyzerAction, AnalyzerOptions, ControlFlow, Never, RuleFilter, }; use rome_console::{ fmt::{Formatter, Termcolor}, markup, Markup, }; +use rome_deserialize::json::deserialize_from_json_str; use rome_diagnostics::advice::CodeSuggestionAdvice; use rome_diagnostics::termcolor::NoColor; use rome_diagnostics::{DiagnosticExt, Error, PrintDiagnostic, Severity}; @@ -14,6 +15,9 @@ use rome_js_parser::{ test_utils::{assert_errors_are_absent, has_bogus_nodes_or_empty_slots}, }; use rome_js_syntax::{JsFileSource, JsLanguage}; +use rome_service::configuration::to_analyzer_configuration; +use rome_service::settings::WorkspaceSettings; +use rome_service::Configuration; use similar::TextDiff; use std::{ ffi::OsStr, fmt::Write, fs::read_to_string, os::raw::c_int, path::Path, slice, sync::Once, @@ -127,23 +131,40 @@ pub(crate) fn write_analysis_to_snapshot( let mut diagnostics = Vec::new(); let mut code_fixes = Vec::new(); let mut options = AnalyzerOptions::default(); - // We allow a test file to configure its rule using a special // file with the same name as the test but with extension ".options.json" // that configures that specific rule. let options_file = input_file.with_extension("options.json"); - if let Ok(json) = std::fs::read_to_string(options_file) { - //RuleKey needs 'static string, so we must leak them here - let (group, rule) = parse_test_path(input_file); - let group = Box::leak(Box::new(group.to_string())); - let rule = Box::leak(Box::new(rule.to_string())); - let rule_key = RuleKey::new(group, rule); - - options - .configuration - .rules - .push_rule(rule_key, json.clone()); - Some(json) + if let Ok(json) = std::fs::read_to_string(options_file.clone()) { + let deserialized = deserialize_from_json_str::(json.as_str()); + if deserialized.has_errors() { + diagnostics.extend( + deserialized + .into_diagnostics() + .into_iter() + .map(|diagnostic| { + diagnostic_to_string( + options_file.file_stem().unwrap().to_str().unwrap(), + &json, + diagnostic, + ) + }) + .collect::>(), + ); + None + } else { + let configuration = deserialized.into_deserialized(); + let mut settings = WorkspaceSettings::default(); + settings.merge_with_configuration(configuration).unwrap(); + let configuration = + to_analyzer_configuration(&settings.linter, &settings.languages, |_| vec![]); + options = AnalyzerOptions { + configuration, + ..AnalyzerOptions::default() + }; + + Some(json) + } } else { None }; diff --git a/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/customHook.options.json b/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/customHook.options.json index 47f7a825df9..d5a39099e5a 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/customHook.options.json +++ b/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/customHook.options.json @@ -1,9 +1,21 @@ { - "hooks": [ - [ - "useMyEffect", - 0, - 1 - ] - ] -} \ No newline at end of file + "$schema": "../../../../../../npm/rome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "useExhaustiveDependencies": { + "level": "error", + "options": { + "hooks": [ + { + "name": "useMyEffect", + "closureIndex": 0, + "dependenciesIndex": 1 + } + ] + } + } + } + } + } +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/malformedOptions.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/malformedOptions.js.snap index 8e96baed153..7180430d3f4 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/malformedOptions.js.snap +++ b/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/malformedOptions.js.snap @@ -9,15 +9,16 @@ expression: malformedOptions.js # Diagnostics ``` -malformedOptions.js:2:5 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +malformedOptions.options:9:7 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Found an unknown key `hook`. - 1 │ { - > 2 │ "hook": [ - │ ^^^^^^ - 3 │ [ - 4 │ "useMyEffect", + 7 │ "level": "error", + 8 │ "options": { + > 9 │ "hook": [ + │ ^^^^^^ + 10 │ { + 11 │ "name": "useMyEffect", i Accepted keys diff --git a/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/malformedOptions.options.json b/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/malformedOptions.options.json index 3593651e458..e24e3548389 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/malformedOptions.options.json +++ b/crates/rome_js_analyze/tests/specs/nursery/useExhaustiveDependencies/malformedOptions.options.json @@ -1,9 +1,21 @@ { - "hook": [ - [ - "useMyEffect", - 0, - 1 - ] - ] -} \ No newline at end of file + "$schema": "../../../../../../npm/rome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "useExhaustiveDependencies": { + "level": "error", + "options": { + "hook": [ + { + "name": "useMyEffect", + "closureIndex": 0, + "dependenciesIndex": 1 + } + ] + } + } + } + } + } +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/customHook.options.json b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/customHook.options.json index 8726f6ba4e5..fd1546dac41 100644 --- a/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/customHook.options.json +++ b/crates/rome_js_analyze/tests/specs/nursery/useHookAtTopLevel/customHook.options.json @@ -1,5 +1,19 @@ { - "hooks": [ - ["useCustomHook"] - ] + "$schema": "../../../../../../npm/rome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "useHookAtTopLevel": { + "level": "error", + "options": { + "hooks": [ + { + "name": "useCustomHook" + } + ] + } + } + } + } + } } diff --git a/crates/rome_migrate/Cargo.toml b/crates/rome_migrate/Cargo.toml index 0ee3fdf5f8f..14c22952ea0 100644 --- a/crates/rome_migrate/Cargo.toml +++ b/crates/rome_migrate/Cargo.toml @@ -8,7 +8,10 @@ edition = "2021" [dependencies] rome_rowan = { workspace = true } rome_json_syntax = { workspace = true } -rome_json_parser = { workspace = true} rome_analyze = { workspace = true} rome_diagnostics = { workspace = true} lazy_static = { workspace = true } + +[dev-dependencies] +rome_json_parser = { workspace = true } + diff --git a/crates/rome_migrate/src/lib.rs b/crates/rome_migrate/src/lib.rs index 3fa2af4c023..65953d6f527 100644 --- a/crates/rome_migrate/src/lib.rs +++ b/crates/rome_migrate/src/lib.rs @@ -11,7 +11,7 @@ use rome_analyze::{ use rome_diagnostics::Error; use rome_json_syntax::JsonLanguage; use std::convert::Infallible; -use std::path::Path; +use std::path::{Path, PathBuf}; /// Return the static [MetadataRegistry] for the JS analyzer rules pub fn metadata() -> &'static MetadataRegistry { @@ -44,8 +44,11 @@ where B: 'a, { let filter = AnalysisFilter::default(); - let options = AnalyzerOptions::default(); - let mut registry = RuleRegistry::builder(&filter, &options, root); + let options = AnalyzerOptions { + file_path: PathBuf::from(configuration_file_path), + ..AnalyzerOptions::default() + }; + let mut registry = RuleRegistry::builder(&filter, root); visit_migration_registry(&mut registry); let (migration_registry, services, diagnostics, visitors) = registry.build(); @@ -67,19 +70,12 @@ where analyzer.add_visitor(phase, visitor); } - let globals: Vec<_> = options - .configuration - .globals - .iter() - .map(|global| global.as_str()) - .collect(); ( analyzer.run(AnalyzerContext { root: root.clone(), range: filter.range, services, - globals: globals.as_slice(), - file_path: configuration_file_path, + options: &options, }), diagnostics, ) diff --git a/crates/rome_service/Cargo.toml b/crates/rome_service/Cargo.toml index 52255fe74a8..e642760eca9 100644 --- a/crates/rome_service/Cargo.toml +++ b/crates/rome_service/Cargo.toml @@ -37,7 +37,7 @@ tracing = { workspace = true, features = ["attributes"] } bpaf = { workspace = true } [features] -schemars = ["dep:schemars", "rome_formatter/serde", "rome_js_factory", "rome_text_edit/schemars"] +schemars = ["dep:schemars", "rome_js_analyze/schemars", "rome_formatter/serde", "rome_js_factory", "rome_text_edit/schemars"] [dev-dependencies] insta = { workspace = true } diff --git a/crates/rome_service/src/configuration/generated.rs b/crates/rome_service/src/configuration/generated.rs index 828c4124868..d9c2351486c 100644 --- a/crates/rome_service/src/configuration/generated.rs +++ b/crates/rome_service/src/configuration/generated.rs @@ -13,9 +13,10 @@ pub(crate) fn push_to_analyzer_rules( if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule("a11y", rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } } @@ -26,9 +27,10 @@ pub(crate) fn push_to_analyzer_rules( if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule("complexity", rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } } @@ -39,9 +41,10 @@ pub(crate) fn push_to_analyzer_rules( if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule("correctness", rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } } @@ -52,9 +55,10 @@ pub(crate) fn push_to_analyzer_rules( if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule("nursery", rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } } @@ -65,9 +69,10 @@ pub(crate) fn push_to_analyzer_rules( if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule("performance", rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } } @@ -78,9 +83,10 @@ pub(crate) fn push_to_analyzer_rules( if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule("security", rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } } @@ -91,9 +97,10 @@ pub(crate) fn push_to_analyzer_rules( if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule("style", rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } } @@ -104,9 +111,10 @@ pub(crate) fn push_to_analyzer_rules( if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule("suspicious", rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } } diff --git a/crates/rome_service/src/configuration/linter/mod.rs b/crates/rome_service/src/configuration/linter/mod.rs index b7992284bc8..811679f270a 100644 --- a/crates/rome_service/src/configuration/linter/mod.rs +++ b/crates/rome_service/src/configuration/linter/mod.rs @@ -8,9 +8,10 @@ use crate::settings::LinterSettings; use crate::{ConfigurationDiagnostic, MatchOptions, Matcher, WorkspaceError}; use bpaf::Bpaf; use rome_diagnostics::Severity; +use rome_js_analyze::options::{possible_options, PossibleOptions}; pub use rules::*; #[cfg(feature = "schemars")] -use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -188,8 +189,8 @@ impl FromStr for RulePlainConfiguration { pub struct RuleWithOptions { pub level: RulePlainConfiguration, #[serde(skip_serializing_if = "Option::is_none")] - #[cfg_attr(feature = "schemars", schemars(schema_with = "schema_any"))] - pub options: Option, + #[bpaf(external(possible_options), hide, optional)] + pub options: Option, } impl FromStr for RuleWithOptions { @@ -201,16 +202,3 @@ impl FromStr for RuleWithOptions { }) } } - -#[cfg(feature = "schemars")] -fn schema_any(_gen: &mut SchemaGenerator) -> Schema { - Schema::Bool(true) -} - -impl FromStr for Rules { - type Err = String; - - fn from_str(_s: &str) -> Result { - Ok(Rules::default()) - } -} diff --git a/crates/rome_service/src/configuration/parse/json/configuration.rs b/crates/rome_service/src/configuration/parse/json/configuration.rs index 982bfb4940d..ba915c4c958 100644 --- a/crates/rome_service/src/configuration/parse/json/configuration.rs +++ b/crates/rome_service/src/configuration/parse/json/configuration.rs @@ -31,7 +31,7 @@ impl VisitNode for Configuration { let name_text = name.text(); match name_text { "$schema" => { - self.schema = Some(self.map_to_string(&value, name_text, diagnostics)?); + self.schema = self.map_to_string(&value, name_text, diagnostics); } "files" => { let mut files = FilesConfiguration::default(); diff --git a/crates/rome_service/src/configuration/parse/json/linter.rs b/crates/rome_service/src/configuration/parse/json/linter.rs index bde15df2ebe..7ef2ad22714 100644 --- a/crates/rome_service/src/configuration/parse/json/linter.rs +++ b/crates/rome_service/src/configuration/parse/json/linter.rs @@ -5,6 +5,7 @@ use crate::{RuleConfiguration, Rules}; use rome_console::markup; use rome_deserialize::json::{has_only_known_keys, with_only_known_variants, VisitJsonNode}; use rome_deserialize::{DeserializationDiagnostic, VisitNode}; +use rome_js_analyze::options::PossibleOptions; use rome_json_syntax::{AnyJsonValue, JsonLanguage, JsonObjectValue, JsonSyntaxNode}; use rome_rowan::{AstNode, AstSeparatedList, SyntaxNode}; @@ -106,11 +107,13 @@ impl VisitNode for RuleConfiguration { } } "options" => { + let mut possible_options = PossibleOptions::default(); + self.map_to_object(&value, name_text, &mut possible_options, diagnostics); if let RuleConfiguration::WithOptions(options) = self { - options.options = Some(format!("{value}")) + options.options = Some(possible_options) } else { *self = RuleConfiguration::WithOptions(RuleWithOptions { - options: Some(format!("{value}")), + options: Some(possible_options), ..RuleWithOptions::default() }) } @@ -175,7 +178,9 @@ impl VisitNode for RuleWithOptions { self.level = rule_options; } "options" => { - self.options = Some(format!("{}", value)); + let mut possible_options = PossibleOptions::default(); + self.map_to_object(&value, name_text, &mut possible_options, diagnostics); + self.options = Some(possible_options); } _ => {} } diff --git a/crates/rome_service/tests/invalid/hooks_missing_name.json b/crates/rome_service/tests/invalid/hooks_missing_name.json new file mode 100644 index 00000000000..f2d0d482428 --- /dev/null +++ b/crates/rome_service/tests/invalid/hooks_missing_name.json @@ -0,0 +1,20 @@ +{ + "$schema": "../../../../npm/rome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "useExhaustiveDependencies": { + "level": "error", + "options": { + "hooks": [ + { + "closureIndex": 0, + "dependenciesIndex": 1 + } + ] + } + } + } + } + } +} diff --git a/crates/rome_service/tests/invalid/hooks_missing_name.json.snap b/crates/rome_service/tests/invalid/hooks_missing_name.json.snap new file mode 100644 index 00000000000..486043b56cd --- /dev/null +++ b/crates/rome_service/tests/invalid/hooks_missing_name.json.snap @@ -0,0 +1,21 @@ +--- +source: crates/rome_service/tests/spec_tests.rs +expression: hooks_missing_name.json +--- +hooks_missing_name.json:10:8 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × The field name is mandatory + + 8 │ "options": { + 9 │ "hooks": [ + > 10 │ { + │ ^ + > 11 │ "closureIndex": 0, + > 12 │ "dependenciesIndex": 1 + > 13 │ } + │ ^ + 14 │ ] + 15 │ } + + + diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index f2f85cfb21a..a36ffd6508d 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -567,6 +567,39 @@ }, "additionalProperties": false }, + "Hooks": { + "type": "object", + "required": ["name"], + "properties": { + "closureIndex": { + "description": "The \"position\" of the closure function, starting from zero.\n\n### Example", + "type": ["integer", "null"], + "format": "uint", + "minimum": 0.0 + }, + "dependenciesIndex": { + "description": "The \"position\" of the array of dependencies, starting from zero.", + "type": ["integer", "null"], + "format": "uint", + "minimum": 0.0 + }, + "name": { "description": "The name of the hook", "type": "string" } + }, + "additionalProperties": false + }, + "HooksOptions": { + "description": "Options for the rule `useExhaustiveDependencies` and `useHookAtTopLevel`", + "type": "object", + "required": ["hooks"], + "properties": { + "hooks": { + "description": "List of safe hooks", + "type": "array", + "items": { "$ref": "#/definitions/Hooks" } + } + }, + "additionalProperties": false + }, "JavascriptConfiguration": { "type": "object", "properties": { @@ -854,6 +887,15 @@ { "description": "Space", "type": "string", "enum": ["space"] } ] }, + "PossibleOptions": { + "anyOf": [ + { + "description": "Options for `useExhaustiveDependencies` and `useHookAtTopLevel` rule", + "allOf": [{ "$ref": "#/definitions/HooksOptions" }] + }, + { "description": "No options available", "type": "null" } + ] + }, "QuoteProperties": { "type": "string", "enum": ["asNeeded", "preserve"] }, "QuoteStyle": { "type": "string", "enum": ["double", "single"] }, "RuleConfiguration": { @@ -868,10 +910,15 @@ }, "RuleWithOptions": { "type": "object", - "required": ["level", "options"], + "required": ["level"], "properties": { "level": { "$ref": "#/definitions/RulePlainConfiguration" }, - "options": true + "options": { + "anyOf": [ + { "$ref": "#/definitions/PossibleOptions" }, + { "type": "null" } + ] + } }, "additionalProperties": false }, diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index 1bab966b30e..34df8ca5805 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -873,7 +873,33 @@ export type RuleConfiguration = RulePlainConfiguration | RuleWithOptions; export type RulePlainConfiguration = "warn" | "error" | "off"; export interface RuleWithOptions { level: RulePlainConfiguration; - options: any; + options?: PossibleOptions; +} +export type PossibleOptions = HooksOptions | null; +/** + * Options for the rule `useExhaustiveDependencies` and `useHookAtTopLevel` + */ +export interface HooksOptions { + /** + * List of safe hooks + */ + hooks: Hooks[]; +} +export interface Hooks { + /** + * The "position" of the closure function, starting from zero. + +### Example + */ + closureIndex?: number; + /** + * The "position" of the array of dependencies, starting from zero. + */ + dependenciesIndex?: number; + /** + * The name of the hook + */ + name: string; } export interface OpenFileParams { content: string; diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index f2f85cfb21a..a36ffd6508d 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -567,6 +567,39 @@ }, "additionalProperties": false }, + "Hooks": { + "type": "object", + "required": ["name"], + "properties": { + "closureIndex": { + "description": "The \"position\" of the closure function, starting from zero.\n\n### Example", + "type": ["integer", "null"], + "format": "uint", + "minimum": 0.0 + }, + "dependenciesIndex": { + "description": "The \"position\" of the array of dependencies, starting from zero.", + "type": ["integer", "null"], + "format": "uint", + "minimum": 0.0 + }, + "name": { "description": "The name of the hook", "type": "string" } + }, + "additionalProperties": false + }, + "HooksOptions": { + "description": "Options for the rule `useExhaustiveDependencies` and `useHookAtTopLevel`", + "type": "object", + "required": ["hooks"], + "properties": { + "hooks": { + "description": "List of safe hooks", + "type": "array", + "items": { "$ref": "#/definitions/Hooks" } + } + }, + "additionalProperties": false + }, "JavascriptConfiguration": { "type": "object", "properties": { @@ -854,6 +887,15 @@ { "description": "Space", "type": "string", "enum": ["space"] } ] }, + "PossibleOptions": { + "anyOf": [ + { + "description": "Options for `useExhaustiveDependencies` and `useHookAtTopLevel` rule", + "allOf": [{ "$ref": "#/definitions/HooksOptions" }] + }, + { "description": "No options available", "type": "null" } + ] + }, "QuoteProperties": { "type": "string", "enum": ["asNeeded", "preserve"] }, "QuoteStyle": { "type": "string", "enum": ["double", "single"] }, "RuleConfiguration": { @@ -868,10 +910,15 @@ }, "RuleWithOptions": { "type": "object", - "required": ["level", "options"], + "required": ["level"], "properties": { "level": { "$ref": "#/definitions/RulePlainConfiguration" }, - "options": true + "options": { + "anyOf": [ + { "$ref": "#/definitions/PossibleOptions" }, + { "type": "null" } + ] + } }, "additionalProperties": false }, diff --git a/website/src/pages/lint/rules/useExhaustiveDependencies.md b/website/src/pages/lint/rules/useExhaustiveDependencies.md index 4df13eaa77a..1ac70a86188 100644 --- a/website/src/pages/lint/rules/useExhaustiveDependencies.md +++ b/website/src/pages/lint/rules/useExhaustiveDependencies.md @@ -175,19 +175,13 @@ Allows to specify custom hooks - from libraries or internal projects - that can "//": "...", "options": { "hooks": [ - ["useLocation", 0, 1], - ["useQuery", 1, 0] + { "name": "useLocation", "closureIndex": 0, "dependenciesIndex": 1}, + { "name": "useQuery", "closureIndex": 1, "dependenciesIndex": 0} ] } } ``` -The following items mean: - -1. the name of the hook -2. the index of the closure -3. the index of the array of dependencies - Given the previous example, your hooks be used like this: ```jsx diff --git a/website/src/pages/lint/rules/useHookAtTopLevel.md b/website/src/pages/lint/rules/useHookAtTopLevel.md index 8b48dde0532..018c3e3fd0b 100644 --- a/website/src/pages/lint/rules/useHookAtTopLevel.md +++ b/website/src/pages/lint/rules/useHookAtTopLevel.md @@ -47,6 +47,31 @@ function Component1() { } ``` +## Options + +Allows to specify custom hooks - from libraries or internal projects - that can be considered stable. + +``` +{ + "//": "...", + "options": { + "hooks": [ + { "name": "useLocation", "closureIndex": 0, "dependenciesIndex": 1}, + { "name": "useQuery", "closureIndex": 1, "dependenciesIndex": 0} + ] + } +} +``` + +Given the previous example, your hooks be used like this: + +```jsx +function Foo() { + const location = useLocation(() => {}, []); + const query = useQuery([], () => {}); +} +``` + ## Related links - [Disable a rule](/linter/#disable-a-lint-rule) diff --git a/xtask/codegen/src/generate_analyzer.rs b/xtask/codegen/src/generate_analyzer.rs index 1202229c946..c2b16a18a64 100644 --- a/xtask/codegen/src/generate_analyzer.rs +++ b/xtask/codegen/src/generate_analyzer.rs @@ -60,7 +60,7 @@ fn generate_category( file_name.to_string(), ( quote! { - mod #module_name; + pub(crate) mod #module_name; }, quote! { self::#module_name::#group_name @@ -137,7 +137,7 @@ fn generate_group(category: &'static str, group: &str) -> Result<()> { key, ( quote! { - mod #module_name; + pub(crate) mod #module_name; }, quote! { self::#module_name::#rule_type diff --git a/xtask/codegen/src/generate_configuration.rs b/xtask/codegen/src/generate_configuration.rs index 7a074751143..2efe27cc918 100644 --- a/xtask/codegen/src/generate_configuration.rs +++ b/xtask/codegen/src/generate_configuration.rs @@ -652,9 +652,10 @@ fn generate_push_to_analyzer_rules(group: &str) -> TokenStream { if let Some(RuleConfiguration::WithOptions(rule_options)) = rules.get_rule_configuration(rule_name) { - if let Some(options) = &rule_options.options { + if let Some(possible_options) = &rule_options.options { if let Some(rule_key) = metadata.find_rule(#group, rule_name) { - analyzer_rules.push_rule(rule_key, options.to_string()); + let rule_options = possible_options.extract_option(&rule_key); + analyzer_rules.push_rule(rule_key, rule_options); } } }