This repository has been archived by the owner on Aug 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 659
feat(rome_js_analyze): lint/correctness/noDupeKeys #3562
Merged
Merged
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
a933e70
WIP feat(rome_js_analyze): lint/correctness/noDupeKeys
jeysal 577aed0
multi failure test
jeysal d4f1dbd
ci fixes
jeysal 9faaee0
Merge remote-tracking branch 'upstream/main' into no-dupe-keys
jeysal b1bad71
fix diagnostic copy
jeysal 278e0af
fix obj member removal code action and thus fix snap test
jeysal cfb93ce
update rule release version
jeysal 880e50e
Merge remote-tracking branch 'upstream/main' into no-dupe-keys
jeysal 5b08f04
consistent diagnostics: always only highlight the last overwriting pr…
jeysal 4d6b1cc
codegen
jeysal 83702b5
thanks clippy!
jeysal 9fffd25
Merge remote-tracking branch 'upstream/main' into no-dupe-keys
jeysal 9df697f
Update crates/rome_js_analyze/src/analyzers/nursery/no_dupe_keys.rs
jeysal 2393996
Micha suggestion
jeysal 4e95ccf
Another Micha suggestion
jeysal 034e0fc
Merge remote-tracking branch 'origin/no-dupe-keys' into no-dupe-keys
jeysal 1fc7282
codegen
jeysal 62c5af4
Merge remote-tracking branch 'upstream/main' into no-dupe-keys
jeysal fed78da
micha feedback refactors
jeysal 725d684
Merge remote-tracking branch 'upstream/main' into no-dupe-keys
jeysal 09adebb
codegen
jeysal 1b0f6fa
Merge remote-tracking branch 'upstream/main' into no-dupe-keys
jeysal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
262 changes: 262 additions & 0 deletions
262
crates/rome_js_analyze/src/analyzers/nursery/no_dupe_keys.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
use crate::utils::batch::JsBatchMutation; | ||
use rome_analyze::context::RuleContext; | ||
use rome_analyze::{declare_rule, Ast, Rule, RuleDiagnostic}; | ||
use rome_console::markup; | ||
use rome_js_syntax::{JsAnyObjectMember, JsObjectExpression}; | ||
use rome_rowan::{AstNode, BatchMutationExt}; | ||
use std::cmp::Ordering; | ||
use std::collections::HashMap; | ||
use std::fmt::Display; | ||
|
||
use crate::JsRuleAction; | ||
|
||
declare_rule! { | ||
/// Prevents object literals having more than one property declaration for the same key. | ||
/// If an object property with the same key is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored, which is likely a mistake. | ||
/// | ||
/// ## Examples | ||
/// | ||
/// ### Invalid | ||
/// | ||
/// ```js,expect_diagnostic | ||
/// const obj = { | ||
/// a: 1, | ||
/// a: 2, | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```js,expect_diagnostic | ||
/// const obj = { | ||
/// set a(v) {}, | ||
/// a: 2, | ||
/// } | ||
/// ``` | ||
/// | ||
/// ### Valid | ||
/// | ||
/// ```js | ||
/// const obj = { | ||
/// a: 1, | ||
/// b: 2, | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```js | ||
/// const obj = { | ||
/// get a() { return 1; }, | ||
/// set a(v) {}, | ||
/// } | ||
/// ``` | ||
/// | ||
pub(crate) NoDupeKeys { | ||
version: "10.1.0", | ||
jeysal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
name: "noDupeKeys", | ||
recommended: false, // should be once out of nursery | ||
} | ||
} | ||
|
||
enum PropertyType { | ||
Getter, | ||
Setter, | ||
Value, | ||
jeysal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
impl Display for PropertyType { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
f.write_str(match self { | ||
Self::Getter => "getter", | ||
Self::Setter => "setter", | ||
Self::Value => "value", | ||
}) | ||
} | ||
} | ||
struct PropertyDefinition(PropertyType, JsAnyObjectMember); | ||
|
||
#[derive(Clone)] | ||
enum DefinedProperty { | ||
Getter(JsAnyObjectMember), | ||
Setter(JsAnyObjectMember), | ||
jeysal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
GetterSetter(JsAnyObjectMember, JsAnyObjectMember), | ||
Value(JsAnyObjectMember), | ||
jeysal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
impl From<PropertyDefinition> for DefinedProperty { | ||
fn from(PropertyDefinition(property_type, range): PropertyDefinition) -> Self { | ||
match property_type { | ||
PropertyType::Getter => DefinedProperty::Getter(range), | ||
PropertyType::Setter => DefinedProperty::Setter(range), | ||
PropertyType::Value => DefinedProperty::Value(range), | ||
} | ||
} | ||
} | ||
|
||
pub(crate) struct PropertyConflict(DefinedProperty, PropertyDefinition); | ||
|
||
impl Rule for NoDupeKeys { | ||
type Query = Ast<JsObjectExpression>; | ||
type State = PropertyConflict; | ||
type Signals = Vec<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
let node = ctx.query(); | ||
|
||
let mut defined_properties: HashMap<String, DefinedProperty> = HashMap::new(); | ||
let mut signals: Self::Signals = Vec::new(); | ||
|
||
for member in node | ||
.members() | ||
.into_iter() | ||
// Note that we iterate from last to first property, so that we highlight properties being overwritten as problems and not those that take effect. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be a bit of a hot take, and ESLint does it the other way. |
||
.rev() | ||
.filter_map(|result| result.ok()) | ||
jeysal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
let property_name = get_property_name(&member); | ||
if let Some(property_name) = property_name { | ||
let property_definition = PropertyDefinition( | ||
match member { | ||
JsAnyObjectMember::JsGetterObjectMember(_) => PropertyType::Getter, | ||
JsAnyObjectMember::JsSetterObjectMember(_) => PropertyType::Setter, | ||
_ => PropertyType::Value, | ||
}, | ||
member, | ||
); | ||
let defined_property = defined_properties.remove(&property_name); | ||
match (defined_property, property_definition) { | ||
// Property not seen before | ||
(None, property_definition) => { | ||
// Put a new definition. | ||
defined_properties | ||
.insert(property_name, DefinedProperty::from(property_definition)); | ||
} | ||
// Only get/set counterpart seen before | ||
( | ||
Some(DefinedProperty::Setter(set_range)), | ||
PropertyDefinition(PropertyType::Getter, get_range), | ||
) | ||
| ( | ||
Some(DefinedProperty::Getter(get_range)), | ||
PropertyDefinition(PropertyType::Setter, set_range), | ||
) => { | ||
// Put definition back with the missing get or set filled. | ||
defined_properties.insert( | ||
property_name, | ||
DefinedProperty::GetterSetter(get_range, set_range), | ||
); | ||
} | ||
// Trying to define something that was already defined | ||
( | ||
Some(defined_property @ DefinedProperty::Getter(_)), | ||
property_definition @ (PropertyDefinition(PropertyType::Getter, _) | ||
| PropertyDefinition(PropertyType::Value, _)), | ||
) | ||
| ( | ||
Some(defined_property @ DefinedProperty::Setter(_)), | ||
property_definition @ (PropertyDefinition(PropertyType::Setter, _) | ||
| PropertyDefinition(PropertyType::Value, _)), | ||
) | ||
| ( | ||
Some( | ||
defined_property @ (DefinedProperty::Value(_) | ||
| DefinedProperty::GetterSetter(..)), | ||
), | ||
property_definition, | ||
) => { | ||
// Register the conflict. | ||
signals.push(PropertyConflict( | ||
defined_property.clone(), | ||
property_definition, | ||
)); | ||
// Put definition back unchanged. | ||
defined_properties.insert(property_name, defined_property); | ||
} | ||
} | ||
} | ||
} | ||
|
||
signals | ||
} | ||
|
||
fn diagnostic( | ||
_ctx: &RuleContext<Self>, | ||
PropertyConflict(defined_property, PropertyDefinition(property_type, node)): &Self::State, | ||
) -> Option<RuleDiagnostic> { | ||
let mut diagnostic = RuleDiagnostic::new( | ||
rule_category!(), | ||
node.range(), | ||
format!( | ||
"This property {} is later overwritten by a property with the same name.", | ||
property_type | ||
), | ||
); | ||
diagnostic = match defined_property { | ||
DefinedProperty::Getter(node) => { | ||
diagnostic.detail(node.range(), "Overwritten by this getter.") | ||
} | ||
DefinedProperty::Setter(node) => { | ||
diagnostic.detail(node.range(), "Overwritten by this setter.") | ||
} | ||
DefinedProperty::Value(node) => { | ||
diagnostic.detail(node.range(), "Overwritten with this value.") | ||
} | ||
DefinedProperty::GetterSetter(getter_node, setter_node) => { | ||
match property_type { | ||
PropertyType::Getter => { | ||
diagnostic.detail(getter_node.range(), "Overwritten by this getter.") | ||
} | ||
PropertyType::Setter => { | ||
diagnostic.detail(setter_node.range(), "Overwritten by this setter.") | ||
} | ||
PropertyType::Value => { | ||
match getter_node.range().ordering(setter_node.range()) { | ||
Ordering::Less => diagnostic | ||
.detail(setter_node.range(), "Overwritten by this setter."), | ||
Ordering::Greater => diagnostic | ||
.detail(getter_node.range(), "Overwritten by this getter."), | ||
Ordering::Equal => { | ||
panic!("The ranges of the property getter and property setter cannot overlap.") | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
diagnostic = diagnostic.note("If an object property with the same key is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored."); | ||
|
||
Some(diagnostic) | ||
} | ||
|
||
fn action( | ||
ctx: &RuleContext<Self>, | ||
PropertyConflict(_, PropertyDefinition(property_type, node)): &Self::State, | ||
) -> Option<JsRuleAction> { | ||
let mut batch = ctx.root().begin(); | ||
batch.remove_js_object_member(node); | ||
Some(JsRuleAction { | ||
category: rome_analyze::ActionCategory::QuickFix, | ||
// The property initialization could contain side effects | ||
applicability: rome_diagnostics::Applicability::MaybeIncorrect, | ||
message: markup!("Remove this property " {property_type.to_string()}).to_owned(), | ||
mutation: batch, | ||
}) | ||
} | ||
} | ||
|
||
fn get_property_name(member: &JsAnyObjectMember) -> Option<String> { | ||
jeysal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
match member { | ||
JsAnyObjectMember::JsGetterObjectMember(member) => { | ||
member.name().ok()?.as_js_literal_member_name()?.name().ok() | ||
} | ||
JsAnyObjectMember::JsMethodObjectMember(member) => { | ||
member.name().ok()?.as_js_literal_member_name()?.name().ok() | ||
} | ||
JsAnyObjectMember::JsPropertyObjectMember(member) => { | ||
member.name().ok()?.as_js_literal_member_name()?.name().ok() | ||
} | ||
JsAnyObjectMember::JsSetterObjectMember(member) => { | ||
member.name().ok()?.as_js_literal_member_name()?.name().ok() | ||
} | ||
JsAnyObjectMember::JsShorthandPropertyObjectMember(member) => { | ||
Some(member.name().ok()?.text()) | ||
} | ||
JsAnyObjectMember::JsSpread(_) | JsAnyObjectMember::JsUnknownMember(_) => None, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
went ahead and pressed sort on these as there was no apparent order to them