Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

Commit

Permalink
feat(rome_json_analyze): noDuplicateKeys
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Jun 18, 2023
1 parent e7dfbce commit 722c212
Show file tree
Hide file tree
Showing 28 changed files with 1,025 additions and 93 deletions.
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ 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" }
rome_json_analyze = { path = "./crates/rome_json_analyze" }
schemars = { version = "0.8.10" }


Expand Down
2 changes: 1 addition & 1 deletion crates/rome_analyze/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub trait RegistryVisitor<L: Language> {

/// Stores metadata information for all the rules in the registry, sorted
/// alphabetically
#[derive(Default)]
#[derive(Debug, Default)]
pub struct MetadataRegistry {
inner: BTreeSet<MetadataKey>,
}
Expand Down
1 change: 1 addition & 0 deletions crates/rome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ define_categories! {
"lint/nursery/useLiteralKeys": "https://docs.rome.tools/lint/rules/useLiteralKeys",
"lint/nursery/useSimpleNumberKeys": "https://docs.rome.tools/lint/rules/useSimpleNumberKeys",
"lint/nursery/noStaticOnlyClass": "https://docs.rome.tools/lint/rules/noStaticOnlyClass",
"lint/nursery/noDuplicateKeys": "https://docs.rome.tools/lint/rules/noDuplicateKeys",
// Insert new nursery rule here


Expand Down
2 changes: 1 addition & 1 deletion crates/rome_js_analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ rome_aria = { workspace = true }
roaring = "0.10.1"
rustc-hash = { workspace = true }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = { version = "1.0.74", features = ["raw_value"] }
serde_json = { version = "1.0.74" }
lazy_static = { workspace = true }
natord = "1.0.9"
bpaf.workspace = true
Expand Down
29 changes: 29 additions & 0 deletions crates/rome_json_analyze/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "rome_json_analyze"
version = "0.0.0"
edition.workspace = true
authors.workspace = true
repository.workspace = true
license.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rome_analyze = { workspace = true }
rome_rowan = { workspace = true }
rome_json_syntax = { workspace = true }
rome_json_factory = { workspace = true }
rome_json_parser = { workspace = true }
rome_console = { workspace = true }
rome_diagnostics = { workspace = true }
rome_deserialize = { workspace = true }
lazy_static = { workspace = true }

[dev-dependencies]
tests_macros = { workspace = true }
rome_text_edit = { workspace = true }
insta = { workspace = true, features = ["glob"] }
countme = { workspace = true, features = ["enable"] }
similar = "2.1.0"
json_comments = "0.2.1"
rome_service = { path = "../rome_service"}
4 changes: 4 additions & 0 deletions crates/rome_json_analyze/src/analyzers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Generated file, do not edit by hand, see `xtask/codegen`

pub(crate) mod nursery;
::rome_analyze::declare_category! { pub (crate) Analyzers { kind : Lint , groups : [self :: nursery :: Nursery ,] } }
5 changes: 5 additions & 0 deletions crates/rome_json_analyze/src/analyzers/nursery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Generated file, do not edit by hand, see `xtask/codegen`

use rome_analyze::declare_group;
pub(crate) mod no_duplicate_keys;
declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_duplicate_keys :: NoDuplicateKeys ,] } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
use rome_console::markup;
use rome_json_syntax::{JsonObjectValue, TextRange};
use rome_rowan::{AstNode, AstSeparatedList};
use std::collections::HashMap;

declare_rule! {
/// Disallow two keys with the same name inside an object.
///
/// ## Examples
///
/// ### Invalid
///
/// ```json,expect_diagnostic
/// {
/// "title": "New title",
/// "title": "Second title"
/// }
/// ```
pub(crate) NoDuplicateKeys {
version: "next",
name: "noDuplicateKeys",
recommended: true,
}
}

impl Rule for NoDuplicateKeys {
type Query = Ast<JsonObjectValue>;
type State = (String, TextRange);
type Signals = Vec<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let query = ctx.query();
let mut names = HashMap::<String, Vec<TextRange>>::new();
for member in query.json_member_list().iter().flatten() {
if let Ok(name) = member.name() {
if let Ok(text) = name.inner_string_text() {
if let Some(ranges) = names.get_mut(text.text()) {
ranges.push(name.range());
} else {
names.insert(text.text().to_string(), vec![]);
}
}
}
}

let names: Vec<_> = names
.into_iter()
.filter(|(_, ranges)| !ranges.is_empty())
.flat_map(|(string, ranges)| {
ranges.into_iter().map(move |range| (string.clone(), range))
})
.collect();

names
}

fn diagnostic(_ctx: &RuleContext<Self>, (name, range): &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(
rule_category!(),
range,
markup! {
"The key "<Emphasis>{{name}}</Emphasis>" was already declared"
},
)
.detail(
range,
markup! {
"Remove or rename the key"
},
),
)
}
}
5 changes: 5 additions & 0 deletions crates/rome_json_analyze/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use rome_diagnostics::Diagnostic;

#[derive(Clone, Debug, PartialEq, Eq, Diagnostic)]
#[diagnostic(category = "suppressions/parse")]
pub struct SuppressionDiagnostic {}
169 changes: 169 additions & 0 deletions crates/rome_json_analyze/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
mod analyzers;
mod diagnostics;
mod registry;

use crate::diagnostics::SuppressionDiagnostic;
pub use crate::registry::visit_registry;
use rome_analyze::{
AnalysisFilter, AnalyzerOptions, AnalyzerSignal, ControlFlow, LanguageRoot, MatchQueryParams,
MetadataRegistry, RuleRegistry, SuppressionKind,
};
use rome_diagnostics::Error;
use rome_json_syntax::JsonLanguage;

/// Return the static [MetadataRegistry] for the JSON analyzer rules
pub fn metadata() -> &'static MetadataRegistry {
lazy_static::lazy_static! {
static ref METADATA: MetadataRegistry = {
let mut metadata = MetadataRegistry::default();
visit_registry(&mut metadata);
metadata
};
}

&METADATA
}

/// Run the analyzer on the provided `root`: this process will use the given `filter`
/// to selectively restrict analysis to specific rules / a specific source range,
/// then call `emit_signal` when an analysis rule emits a diagnostic or action
pub fn analyze<'a, F, B>(
root: &LanguageRoot<JsonLanguage>,
filter: AnalysisFilter,
options: &'a AnalyzerOptions,
emit_signal: F,
) -> (Option<B>, Vec<Error>)
where
F: FnMut(&dyn AnalyzerSignal<JsonLanguage>) -> ControlFlow<B> + 'a,
B: 'a,
{
analyze_with_inspect_matcher(root, filter, |_| {}, options, emit_signal)
}

/// Run the analyzer on the provided `root`: this process will use the given `filter`
/// to selectively restrict analysis to specific rules / a specific source range,
/// then call `emit_signal` when an analysis rule emits a diagnostic or action.
/// Additionally, this function takes a `inspect_matcher` function that can be
/// used to inspect the "query matches" emitted by the analyzer before they are
/// processed by the lint rules registry
pub fn analyze_with_inspect_matcher<'a, V, F, B>(
root: &LanguageRoot<JsonLanguage>,
filter: AnalysisFilter,
inspect_matcher: V,
options: &'a AnalyzerOptions,
mut emit_signal: F,
) -> (Option<B>, Vec<Error>)
where
V: FnMut(&MatchQueryParams<JsonLanguage>) + 'a,
F: FnMut(&dyn AnalyzerSignal<JsonLanguage>) -> ControlFlow<B> + 'a,
B: 'a,
{
fn parse_linter_suppression_comment(
_text: &str,
) -> Vec<Result<SuppressionKind, SuppressionDiagnostic>> {
vec![]
}
let mut registry = RuleRegistry::builder(&filter, root);
visit_registry(&mut registry);

let (registry, services, diagnostics, visitors) = registry.build();

// Bail if we can't parse a rule option
if !diagnostics.is_empty() {
return (None, diagnostics);
}

let mut analyzer = rome_analyze::Analyzer::new(
metadata(),
rome_analyze::InspectMatcher::new(registry, inspect_matcher),
parse_linter_suppression_comment,
|_| {},
&mut emit_signal,
);

for ((phase, _), visitor) in visitors {
analyzer.add_visitor(phase, visitor);
}

(
analyzer.run(rome_analyze::AnalyzerContext {
root: root.clone(),
range: filter.range,
services,
options,
}),
diagnostics,
)
}

#[cfg(test)]
mod tests {
use rome_analyze::{AnalyzerOptions, Never, RuleFilter};
use rome_console::fmt::{Formatter, Termcolor};
use rome_console::{markup, Markup};
use rome_diagnostics::termcolor::NoColor;
use rome_diagnostics::{Diagnostic, DiagnosticExt, PrintDiagnostic, Severity};
use rome_json_parser::parse_json;
use rome_json_syntax::TextRange;
use std::slice;

use crate::{analyze, AnalysisFilter, ControlFlow};

#[ignore]
#[test]
fn quick_test() {
fn markup_to_string(markup: Markup) -> String {
let mut buffer = Vec::new();
let mut write = Termcolor(NoColor::new(&mut buffer));
let mut fmt = Formatter::new(&mut write);
fmt.write_markup(markup).unwrap();

String::from_utf8(buffer).unwrap()
}

const SOURCE: &str = r#"{
"foo": "",
"foo": "",
"foo": "",
"bar": "",
"bar": ""
}
"#;

let parsed = parse_json(SOURCE);

let mut error_ranges: Vec<TextRange> = Vec::new();
let rule_filter = RuleFilter::Rule("nursery", "noDuplicateKeys");
let options = AnalyzerOptions::default();
analyze(
&parsed.tree().value().unwrap(),
AnalysisFilter {
enabled_rules: Some(slice::from_ref(&rule_filter)),
..AnalysisFilter::default()
},
&options,
|signal| {
if let Some(diag) = signal.diagnostic() {
error_ranges.push(diag.location().span.unwrap());
let error = diag
.with_severity(Severity::Warning)
.with_file_path("ahahah")
.with_file_source_code(SOURCE);
let text = markup_to_string(markup! {
{PrintDiagnostic::verbose(&error)}
});
eprintln!("{text}");
}

for action in signal.actions() {
let new_code = action.mutation.commit();
eprintln!("{new_code}");
}

ControlFlow::<Never>::Continue(())
},
);

assert_eq!(error_ranges.as_slice(), &[]);
}
}
7 changes: 7 additions & 0 deletions crates/rome_json_analyze/src/registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Generated file, do not edit by hand, see `xtask/codegen`

use rome_analyze::RegistryVisitor;
use rome_json_syntax::JsonLanguage;
pub fn visit_registry<V: RegistryVisitor<JsonLanguage>>(registry: &mut V) {
registry.record_category::<crate::analyzers::Analyzers>();
}
Loading

0 comments on commit 722c212

Please sign in to comment.