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

feat(rome_js_analyze): lint/correctness/noDupeKeys #3562

Merged
merged 22 commits into from
Nov 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions crates/rome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,16 @@ define_dategories! {


// nursery
"lint/nursery/useFlatMap": "https://docs.rome.tools/lint/rules/useFlatMap",
"lint/nursery/noBannedTypes":"https://docs.rome.tools/lint/rules/noBannedTypes",
Copy link
Contributor Author

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

"lint/nursery/noConstAssign": "https://docs.rome.tools/lint/rules/noConstAssign",
"lint/nursery/noDupeKeys":"https://docs.rome.tools/lint/rules/noDupeKeys",
"lint/nursery/noExplicitAny": "https://docs.rome.tools/lint/rules/noExplicitAny",
"lint/nursery/useValidForDirection": "https://docs.rome.tools/lint/rules/useValidForDirection",
"lint/nursery/noInvalidConstructorSuper": "https://docs.rome.tools/lint/rules/noInvalidConstructorSuper",
"lint/nursery/useExhaustiveDependencies": "https://docs.rome.tools/lint/rules/useExhaustiveDependencies",
"lint/nursery/useCamelCase": "https://docs.rome.tools/lint/rules/useCamelCase",
"lint/nursery/noBannedTypes":"https://docs.rome.tools/lint/rules/noBannedTypes",
"lint/nursery/useExhaustiveDependencies": "https://docs.rome.tools/lint/rules/useExhaustiveDependencies",
"lint/nursery/useFlatMap": "https://docs.rome.tools/lint/rules/useFlatMap",
"lint/nursery/useNumericLiterals": "https://docs.rome.tools/lint/rules/useNumericLiterals",
"lint/nursery/useValidForDirection": "https://docs.rome.tools/lint/rules/useValidForDirection",

;

Expand Down
3 changes: 2 additions & 1 deletion crates/rome_js_analyze/src/analyzers/nursery.rs

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

311 changes: 311 additions & 0 deletions crates/rome_js_analyze/src/analyzers/nursery/no_dupe_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
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, JsGetterObjectMember, JsObjectExpression, JsSetterObjectMember,
};
use rome_js_syntax::{
JsMethodObjectMember, JsPropertyObjectMember, JsShorthandPropertyObjectMember, TextRange,
};
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 name.
/// If an object property with the same name 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: "11.0.0",
name: "noDupeKeys",
recommended: false, // should be once out of nursery
}
}

/// An object member defining a single object property.
enum MemberDefinition {
Getter(JsGetterObjectMember),
Setter(JsSetterObjectMember),
Method(JsMethodObjectMember),
Property(JsPropertyObjectMember),
ShorthandProperty(JsShorthandPropertyObjectMember),
}
impl MemberDefinition {
fn name(&self) -> Option<String> {
match self {
MemberDefinition::Getter(getter) => {
getter.name().ok()?.as_js_literal_member_name()?.name().ok()
}
MemberDefinition::Setter(setter) => {
setter.name().ok()?.as_js_literal_member_name()?.name().ok()
}
MemberDefinition::Method(method) => {
method.name().ok()?.as_js_literal_member_name()?.name().ok()
}
MemberDefinition::Property(property) => property
.name()
.ok()?
.as_js_literal_member_name()?
.name()
.ok(),
MemberDefinition::ShorthandProperty(shorthand_property) => {
Some(shorthand_property.name().ok()?.text())
}
}
}

fn range(&self) -> TextRange {
match self {
MemberDefinition::Getter(getter) => getter.range(),
MemberDefinition::Setter(setter) => setter.range(),
MemberDefinition::Method(method) => method.range(),
MemberDefinition::Property(property) => property.range(),
MemberDefinition::ShorthandProperty(shorthand_property) => shorthand_property.range(),
}
}

fn node(&self) -> JsAnyObjectMember {
match self {
MemberDefinition::Getter(getter) => JsAnyObjectMember::from(getter.clone()),
MemberDefinition::Setter(setter) => JsAnyObjectMember::from(setter.clone()),
MemberDefinition::Method(method) => JsAnyObjectMember::from(method.clone()),
MemberDefinition::Property(property) => JsAnyObjectMember::from(property.clone()),
MemberDefinition::ShorthandProperty(shorthand_property) => {
JsAnyObjectMember::from(shorthand_property.clone())
}
}
}
}
impl Display for MemberDefinition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Getter(_) => "getter",
Self::Setter(_) => "setter",
Self::Method(_) => "method",
Self::Property(_) => "property value",
Self::ShorthandProperty(_) => "shorthand property",
})?;
if let Some(name) = self.name() {
f.write_str(" named ")?;
f.write_str(&name)?;
}
Ok(())
}
}
enum MemberDefinitionError {
NotASinglePropertyMember,
UnknownMemberType,
}
impl TryFrom<JsAnyObjectMember> for MemberDefinition {
type Error = MemberDefinitionError;

fn try_from(member: JsAnyObjectMember) -> Result<Self, Self::Error> {
match member {
JsAnyObjectMember::JsGetterObjectMember(member) => Ok(MemberDefinition::Getter(member)),
JsAnyObjectMember::JsSetterObjectMember(member) => Ok(MemberDefinition::Setter(member)),
JsAnyObjectMember::JsMethodObjectMember(member) => Ok(MemberDefinition::Method(member)),
JsAnyObjectMember::JsPropertyObjectMember(member) => {
Ok(MemberDefinition::Property(member))
}
JsAnyObjectMember::JsShorthandPropertyObjectMember(member) => {
Ok(MemberDefinition::ShorthandProperty(member))
}
JsAnyObjectMember::JsSpread(_) => Err(MemberDefinitionError::NotASinglePropertyMember),
JsAnyObjectMember::JsUnknownMember(_) => Err(MemberDefinitionError::UnknownMemberType),
}
}
}

/// A descriptor for a property that is, as far as we can tell from statically analyzing the object expression,
/// not overwritten by another object member and will make it into the object.
#[derive(Clone)]
enum DefinedProperty {
Get(TextRange),
Set(TextRange),
GetSet(TextRange, TextRange),
Value(TextRange),
}
impl From<MemberDefinition> for DefinedProperty {
fn from(definition: MemberDefinition) -> Self {
match definition {
MemberDefinition::Getter(getter) => DefinedProperty::Get(getter.range()),
MemberDefinition::Setter(setter) => DefinedProperty::Set(setter.range()),
MemberDefinition::Method(method) => DefinedProperty::Value(method.range()),
MemberDefinition::Property(property) => DefinedProperty::Value(property.range()),
MemberDefinition::ShorthandProperty(shorthand_property) => {
DefinedProperty::Value(shorthand_property.range())
}
}
}
}

pub(crate) struct PropertyConflict(DefinedProperty, MemberDefinition);
impl DefinedProperty {
fn extend_with(
&self,
member_definition: MemberDefinition,
) -> Result<DefinedProperty, PropertyConflict> {
match (self, member_definition) {
// Add missing get/set counterpart
(DefinedProperty::Set(set_range), MemberDefinition::Getter(getter)) => {
Ok(DefinedProperty::GetSet(getter.range(), *set_range))
}

(DefinedProperty::Get(get_range), MemberDefinition::Setter(setter)) => {
Ok(DefinedProperty::GetSet(*get_range, setter.range()))
}
// Else conflict
(defined_property, member_definition) => Err(PropertyConflict(
defined_property.clone(),
member_definition,
)),
}
}
}

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_definition in node
.members()
.into_iter()
.flatten()
.filter_map(|member| MemberDefinition::try_from(member).ok())
// Note that we iterate from last to first property, so that we highlight properties being overwritten as problems and not those that take effect.
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
But I think it's the right way, esp. considering the fix suggestion.

.rev()
{
if let Some(member_name) = member_definition.name() {
match defined_properties.remove(&member_name) {
None => {
defined_properties
.insert(member_name, DefinedProperty::from(member_definition));
}
Some(defined_property) => {
match defined_property.extend_with(member_definition) {
Ok(new_defined_property) => {
defined_properties.insert(member_name, new_defined_property);
}
Err(conflict) => {
signals.push(conflict);
defined_properties.insert(member_name, defined_property);
}
}
}
}
}
}

signals
}

fn diagnostic(
_ctx: &RuleContext<Self>,
PropertyConflict(defined_property, member_definition): &Self::State,
) -> Option<RuleDiagnostic> {
let mut diagnostic = RuleDiagnostic::new(
rule_category!(),
member_definition.range(),
format!(
"This {} is later overwritten by an object member with the same name.",
member_definition
),
);
diagnostic = match defined_property {
DefinedProperty::Get(range) => {
diagnostic.detail(range, "Overwritten with this getter.")
}
DefinedProperty::Set(range) => {
diagnostic.detail(range, "Overwritten with this setter.")
}
DefinedProperty::Value(range) => {
diagnostic.detail(range, "Overwritten with this value.")
}
DefinedProperty::GetSet(get_range, set_range) => match member_definition {
MemberDefinition::Getter(_) => {
diagnostic.detail(get_range, "Overwritten with this getter.")
}
MemberDefinition::Setter(_) => {
diagnostic.detail(set_range, "Overwritten with this setter.")
}
MemberDefinition::Method(_)
| MemberDefinition::Property(_)
| MemberDefinition::ShorthandProperty(_) => match get_range.ordering(*set_range) {
Ordering::Less => diagnostic.detail(set_range, "Overwritten with this setter."),
Ordering::Greater => {
diagnostic.detail(get_range, "Overwritten with 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 name 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(_, member_definition): &Self::State,
) -> Option<JsRuleAction> {
let mut batch = ctx.root().begin();
batch.remove_js_object_member(&member_definition.node());
Some(JsRuleAction {
category: rome_analyze::ActionCategory::QuickFix,
// The property initialization could contain side effects
applicability: rome_diagnostics::Applicability::MaybeIncorrect,
message: markup!("Remove this " {member_definition.to_string()}).to_owned(),
mutation: batch,
})
}
}
Loading