feat(rome_service): auto generate configuration for rules
MODIFY
CONTRIBUTING.md
CONTRIBUTING.md
@@ -97,6 +97,7 @@ It's strongly advised to **run this command before committing new changes**.
#### `cargo codegen analyzer`
This command will detect linter rules declared in the `analyzers` and `assists` directories in `rome_analyze`, regenerate the index modules `analyzers.rs` and `assists.rs` to import these files, and update the registry builder function in `registry.rs` to include all these rules.
+It will also regenerate the configuration of the rules.
#### `cargo coverage`

MODIFY
Cargo.lock
Cargo.lock
@@ -1148,11 +1148,11 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.36"
+version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
dependencies = [
- "unicode-xid",
+ "unicode-ident",
]
[[package]]
@@ -1857,13 +1857,13 @@ dependencies = [
[[package]]
name = "syn"
-version = "1.0.94"
+version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"
+checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
dependencies = [
"proc-macro2",
"quote",
- "unicode-xid",
+ "unicode-ident",
]
[[package]]
@@ -2208,6 +2208,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+[[package]]
+name = "unicode-ident"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
+
[[package]]
name = "unicode-normalization"
version = "0.1.19"
@@ -2229,12 +2235,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
-[[package]]
-name = "unicode-xid"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
-
[[package]]
name = "untrusted"
version = "0.7.1"
@@ -2505,11 +2505,14 @@ name = "xtask_codegen"
version = "0.0.0"
dependencies = [
"anyhow",
+ "case",
"filetime",
"git2",
"pico-args",
"proc-macro2",
"quote",
+ "rome_analyze",
+ "rome_js_analyze",
"ungrammar",
"ureq",
"walkdir",

MODIFY
crates/rome_cli/src/traversal.rs
crates/rome_cli/src/traversal.rs
@@ -226,7 +226,7 @@ fn print_messages_to_console(
}
}
- if !mode.is_ci() {
+ if !mode.is_ci() && not_printed_diagnostics > 0 {
console.log(markup! {
<Warn>"The number of diagnostics exceeds the number allowed by Rome."</Warn>
<Info>"Diagnostics not shown: "</Info><Emphasis>{not_printed_diagnostics}</Emphasis><Info>"."</Info>

MODIFY
crates/rome_cli/tests/configs.rs
crates/rome_cli/tests/configs.rs
@@ -36,7 +36,14 @@ pub const CONFIG_ALL_FIELDS: &str = r#"{
"lineWidth": 80
},
"linter": {
- "enabled": true
+ "enabled": true,
+ "globals": ["$"],
+ "rules": {
+ "js": {
+ "noDeadCode": "off",
+ "useSimplifiedLogicExpression": "warn"
+ }
+ }
},
"javascript": {
"formatter": {
@@ -58,3 +65,18 @@ pub const CONFIG_LINTER_DISABLED: &str = r#"{
"enabled": false
}
}"#;
+
+pub const CONFIG_LINTER_WRONG_RULE: &str = r#"{
+ "root": true,
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "js": {
+ "foo_rule": "off"
+ },
+ "jsx": {
+ "what_the_hell": "off"
+ }
+ }
+ }
+}"#;

MODIFY
crates/rome_cli/tests/main.rs
crates/rome_cli/tests/main.rs
@@ -851,7 +851,9 @@ mod init {
}
mod configuration {
- use crate::configs::{CONFIG_ALL_FIELDS, CONFIG_BAD_LINE_WIDTH, CONFIG_ROOT_FALSE};
+ use crate::configs::{
+ CONFIG_ALL_FIELDS, CONFIG_BAD_LINE_WIDTH, CONFIG_LINTER_WRONG_RULE, CONFIG_ROOT_FALSE,
+ };
use pico_args::Arguments;
use rome_cli::{run_cli, CliSession};
use rome_console::BufferConsole;
@@ -929,4 +931,27 @@ mod configuration {
_ => panic!("expected an error, but found none"),
}
}
+
+ #[test]
+ fn incorrect_rule_name() {
+ let mut fs = MemoryFileSystem::default();
+
+ let file_path = Path::new("rome.json");
+ fs.insert(file_path.into(), CONFIG_LINTER_WRONG_RULE.as_bytes());
+
+ let result = run_cli(CliSession {
+ app: App::with_filesystem_and_console(
+ DynRef::Borrowed(&mut fs),
+ DynRef::Owned(Box::new(BufferConsole::default())),
+ ),
+ args: Arguments::from_vec(vec![OsString::from("check"), OsString::from("file.js")]),
+ });
+
+ match result {
+ Err(error) => {
+ assert!(error.to_string().contains("unknown field `foo_rule`"),)
+ }
+ _ => panic!("expected an error, but found none"),
+ }
+ }
}

DELETE
crates/rome_service/src/configuration/linter.rs
-use crate::settings::LinterSettings;
-use serde::{Deserialize, Serialize};
-
-#[derive(Deserialize, Serialize, Debug, Eq, PartialEq)]
-#[serde(rename_all = "camelCase", default)]
-pub struct LinterConfiguration {
- // if `false`, it disables the feature. `true` by default
- pub enabled: bool,
-}
-
-impl Default for LinterConfiguration {
- fn default() -> Self {
- Self { enabled: true }
- }
-}
-
-impl From<&LinterConfiguration> for LinterSettings {
- fn from(conf: &LinterConfiguration) -> Self {
- Self {
- enabled: conf.enabled,
- }
- }
-}

ADD
crates/rome_service/src/configuration/linter/mod.rs
+#[rustfmt::skip]
+mod rules;
+
+pub use crate::configuration::linter::rules::Rules;
+use crate::settings::LinterSettings;
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Serialize, Debug, Eq, PartialEq)]
+#[serde(rename_all = "camelCase", default)]
+pub struct LinterConfiguration {
+ /// if `false`, it disables the feature. `true` by default
+ pub enabled: bool,
+
+ /// A list of global bindings that should be ignored by the analyzers
+ ///
+ /// If defined here, they should not emit diagnostics.
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub globals: Vec<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub rules: Option<Rules>,
+}
+
+impl Default for LinterConfiguration {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ globals: vec![],
+ rules: None,
+ }
+ }
+}
+
+impl From<&LinterConfiguration> for LinterSettings {
+ fn from(conf: &LinterConfiguration) -> Self {
+ Self {
+ enabled: conf.enabled,
+ globals: conf.globals.clone(),
+ rules: conf.rules.clone(),
+ }
+ }
+}
+
+#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub enum RuleConfiguration {
+ Warn,
+ Error,
+ Off,
+}
+impl RuleConfiguration {
+ pub fn is_err(&self) -> bool {
+ matches!(self, Self::Error)
+ }
+}
+impl Default for RuleConfiguration {
+ fn default() -> Self {
+ Self::Error
+ }
+}

ADD
crates/rome_service/src/configuration/linter/rules.rs
+//! Generated file, do not edit by hand, see `xtask/codegen`
+
+use crate::RuleConfiguration;
+use serde::{Deserialize, Serialize};
+#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct Rules {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub js: Option<JsRules>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub jsx: Option<JsxRules>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub regex: Option<RegexRules>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ts: Option<TsRules>,
+}
+impl Default for Rules {
+ fn default() -> Self {
+ Self {
+ js: Some(JsRules::default()),
+ jsx: Some(JsxRules::default()),
+ regex: Some(RegexRules::default()),
+ ts: Some(TsRules::default()),
+ }
+ }
+}
+#[derive(Deserialize, Default, Serialize, Debug, Eq, PartialEq, Clone)]
+#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
+pub struct JsRules {
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_arguments: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_async_promise_executor: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_catch_assign: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_compare_neg_zero: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_dead_code: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_debugger: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_delete: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_double_equals: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_empty_pattern: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_label_var: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_negation_else: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_sparse_array: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_unnecessary_continue: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_unsafe_negation: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_unused_template_literal: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub use_block_statements: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub use_simplified_logic_expression: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub use_single_case_statement: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub use_single_var_declarator: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub use_valid_typeof: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub use_while: RuleConfiguration,
+}
+#[derive(Deserialize, Default, Serialize, Debug, Eq, PartialEq, Clone)]
+#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
+pub struct JsxRules {
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_comment_text: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_implicit_boolean: RuleConfiguration,
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub use_self_closing_elements: RuleConfiguration,
+}
+#[derive(Deserialize, Default, Serialize, Debug, Eq, PartialEq, Clone)]
+#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
+pub struct RegexRules {
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub no_multiple_spaces_in_regular_expression_literals: RuleConfiguration,
+}
+#[derive(Deserialize, Default, Serialize, Debug, Eq, PartialEq, Clone)]
+#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
+pub struct TsRules {
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub use_shorthand_array_type: RuleConfiguration,
+}

MODIFY
crates/rome_service/src/configuration/mod.rs
crates/rome_service/src/configuration/mod.rs
@@ -15,10 +15,13 @@ use std::path::PathBuf;
mod formatter;
mod javascript;
-mod linter;
+pub mod linter;
+
+pub use linter::{RuleConfiguration, Rules};
/// The configuration that is contained inside the file `rome.json`
#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
pub struct Configuration {
/// One root file should exist. Useful when `extends` comes into play.
///

MODIFY
crates/rome_service/src/lib.rs
crates/rome_service/src/lib.rs
@@ -13,6 +13,7 @@ pub mod workspace;
pub use crate::configuration::{
create_config, load_config, Configuration, ConfigurationError, ConfigurationType,
+ RuleConfiguration, Rules,
};
pub use crate::file_handlers::JsFormatSettings;
pub use crate::workspace::Workspace;

MODIFY
crates/rome_service/src/settings.rs
crates/rome_service/src/settings.rs
use std::sync::{RwLock, RwLockReadGuard};
-use crate::Configuration;
+use crate::{Configuration, Rules};
use rome_formatter::{IndentStyle, LineWidth};
use rome_fs::RomePath;
use rome_js_syntax::JsLanguage;
@@ -19,6 +19,7 @@ pub struct WorkspaceSettings {
impl WorkspaceSettings {
/// The (configuration)[Configuration] is merged into the workspace
pub fn merge_with_configuration(&mut self, configuration: &Configuration) {
+ // formatter part
if let Some(formatter) = &configuration.formatter {
self.format = FormatSettings::from(formatter);
}
@@ -29,6 +30,11 @@ impl WorkspaceSettings {
if let Some(formatter) = formatter {
self.languages.javascript.format.quote_style = Some(formatter.quote_style);
}
+
+ // linter part
+ if let Some(linter) = &configuration.linter {
+ self.linter = LinterSettings::from(linter)
+ }
}
}
@@ -55,16 +61,27 @@ impl Default for FormatSettings {
}
}
-/// Formatter settings for the entire workspace
+/// Linter settings for the entire workspace
#[derive(Debug)]
pub struct LinterSettings {
/// Enabled by default
pub enabled: bool,
+
+ /// A list of global bindings that should be ignored by the analyzers
+ ///
+ /// If defined here, they should not emit diagnostics.
+ pub globals: Vec<String>,
+
+ pub rules: Option<Rules>,
}
impl Default for LinterSettings {
fn default() -> Self {
- Self { enabled: true }
+ Self {
+ enabled: true,
+ globals: vec![],
+ rules: Some(Rules::default()),
+ }
}
}

MODIFY
xtask/codegen/Cargo.toml
xtask/codegen/Cargo.toml
@@ -15,3 +15,6 @@ walkdir = "2.3.2"
ureq = "2.4.0"
git2 = { version = "0.13.25", default-features = false }
filetime = "0.2.15"
+rome_analyze = { path = "../../crates/rome_analyze" }
+rome_js_analyze = { path = "../../crates/rome_js_analyze" }
+case = "1.0.0"

ADD
xtask/codegen/src/generate_configuration.rs
+use case::CaseExt;
+use proc_macro2::{Ident, Span};
+use quote::quote;
+use rome_analyze::{AnalysisFilter, RuleCategories};
+use rome_js_analyze::metadata;
+use std::collections::BTreeMap;
+use xtask::*;
+use xtask_codegen::{to_lower_snake_case, update};
+
+pub(crate) fn generate_rules_configuration(mode: Mode) -> Result<()> {
+ let config_root = project_root().join("crates/rome_service/src/configuration/linter");
+
+ let filter = AnalysisFilter {
+ categories: RuleCategories::LINT,
+ ..AnalysisFilter::default()
+ };
+
+ // Ensure the list of rules is stored alphabetically
+ let mut groups = BTreeMap::new();
+ for meta in metadata(filter) {
+ groups
+ .entry(meta.group)
+ .or_insert_with(BTreeMap::new)
+ .insert(meta.name, meta.docs);
+ }
+
+ let mut struct_groups = Vec::new();
+ let mut line_groups = Vec::new();
+ let mut default_for_groups = Vec::new();
+ for (group, rules) in groups {
+ let mut declarations = Vec::new();
+
+ for (rule, _) in rules {
+ let rule = Ident::new(&to_lower_snake_case(rule), Span::call_site());
+ let declaration = quote! {
+ #[serde(skip_serializing_if = "RuleConfiguration::is_err")]
+ pub #rule: RuleConfiguration
+ };
+ declarations.push(declaration);
+ }
+
+ let property_group_name = Ident::new(&to_lower_snake_case(group), Span::call_site());
+
+ let struct_group_name = Ident::new(
+ &format!("{}Rules", group.to_capitalized()),
+ Span::call_site(),
+ );
+
+ let the_struct = quote! {
+
+ #[derive(Deserialize, Default, Serialize, Debug, Eq, PartialEq, Clone)]
+ #[serde(rename_all = "camelCase", default, deny_unknown_fields)]
+ pub struct #struct_group_name {
+ #( #declarations ),*
+ }
+
+ };
+
+ let line = quote! {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub #property_group_name: Option<#struct_group_name>
+ };
+
+ let default = quote! {
+ #property_group_name: Some(#struct_group_name::default())
+ };
+
+ struct_groups.push(the_struct);
+ line_groups.push(line);
+ default_for_groups.push(default);
+ }
+
+ let groups = quote! {
+ use serde::{Deserialize, Serialize};
+ use crate::RuleConfiguration;
+
+
+ #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)]
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
+ pub struct Rules {
+ #( #line_groups ),*
+ }
+
+ impl Default for Rules {
+ fn default() -> Self {
+ Self {
+ #( #default_for_groups ),*
+ }
+ }
+ }
+
+ #( #struct_groups )*
+ };
+
+ let ast = groups.to_string();
+
+ let pretty = xtask::reformat(ast)?;
+
+ update(&config_root.join("rules.rs"), &pretty, &mode)?;
+
+ Ok(())
+}

MODIFY
xtask/codegen/src/lib.rs
xtask/codegen/src/lib.rs
@@ -49,7 +49,7 @@ const JSON_SYNTAX_FACTORY: &str = "crates/rome_json_factory/src/generated/syntax
const JSON_NODE_FACTORY: &str = "crates/rome_json_factory/src/generated/node_factory.rs";
const JSON_AST_MACROS: &str = "crates/rome_json_syntax/src/generated/macros.rs";
-enum UpdateResult {
+pub enum UpdateResult {
NotUpdated,
Updated,
}
@@ -158,7 +158,7 @@ impl LanguageKind {
/// A helper to update file on disk if it has changed.
/// With verify = false,
-fn update(path: &Path, contents: &str, mode: &Mode) -> Result<UpdateResult> {
+pub fn update(path: &Path, contents: &str, mode: &Mode) -> Result<UpdateResult> {
match fs2::read_to_string(path) {
Ok(old_contents) if old_contents == contents => {
return Ok(UpdateResult::NotUpdated);

MODIFY
xtask/codegen/src/main.rs
xtask/codegen/src/main.rs
+mod generate_configuration;
+
use pico_args::Arguments;
use xtask::{project_root, pushd, Mode, Result};
+use crate::generate_configuration::generate_rules_configuration;
use xtask_codegen::{
generate_analyzer, generate_ast, generate_formatter, generate_parser_tests, generate_tables,
};
@@ -34,6 +37,7 @@ fn main() -> Result<()> {
}
"analyzer" => {
generate_analyzer()?;
+ generate_rules_configuration(Mode::Overwrite)?;
Ok(())
}
_ => {
@@ -44,8 +48,9 @@ Run codegen command.
USAGE:
cargo codegen <SUBCOMMAND> [option]
SUBCOMMANDS:
- analyzer Generate factory functions for the analyzer
- grammar Transforms js.ungram into AST
+ analyzer Generate factory functions for the analyzer and the configuration of the analyzers
+ grammar Transforms ungram files into AST
+ formatter Generates formatters for each language
test Extracts parser inline comments into test files
unicode Generates unicode table inside lexer
"

feat(rome_service): auto generate configuration for rules (#2890)