feat(rome_js_analyze): `noPrototypeBuiltins`
Closes
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -95,6 +95,7 @@ define_dategories! {
"lint/nursery/noDuplicateJsxProps": "https://docs.rome.tools/lint/rules/noDuplicateJsxProps",
"lint/nursery/useYield": "https://docs.rome.tools/lint/rules/useYield",
"lint/nursery/noGlobalObjectCalls": "https://docs.rome.tools/lint/rules/noGlobalObjectCalls",
+ "lint/nursery/noPrototypeBuiltins": "https://docs.rome.tools/lint/rules/noPrototypeBuiltins",
// Insert new nursery rule here
// performance

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -20,6 +20,7 @@ mod no_inner_declarations;
mod no_invalid_constructor_super;
mod no_non_null_assertion;
mod no_precision_loss;
+mod no_prototype_builtins;
mod no_redundant_alt;
mod no_redundant_use_strict;
mod no_self_compare;
@@ -39,4 +40,4 @@ mod use_is_nan;
mod use_media_caption;
mod use_numeric_literals;
mod use_yield;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_header_scope :: NoHeaderScope , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_redundant_alt :: NoRedundantAlt , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_void_type_return :: NoVoidTypeReturn , self :: no_with :: NoWith , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_is_nan :: UseIsNan , self :: use_media_caption :: UseMediaCaption , self :: use_numeric_literals :: UseNumericLiterals , self :: use_yield :: UseYield ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_access_key :: NoAccessKey , self :: no_assign_in_expressions :: NoAssignInExpressions , self :: no_banned_types :: NoBannedTypes , self :: no_comma_operator :: NoCommaOperator , self :: no_const_enum :: NoConstEnum , self :: no_constructor_return :: NoConstructorReturn , self :: no_distracting_elements :: NoDistractingElements , self :: no_duplicate_case :: NoDuplicateCase , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_duplicate_object_keys :: NoDuplicateObjectKeys , self :: no_empty_interface :: NoEmptyInterface , self :: no_extra_non_null_assertion :: NoExtraNonNullAssertion , self :: no_extra_semicolons :: NoExtraSemicolons , self :: no_global_object_calls :: NoGlobalObjectCalls , self :: no_header_scope :: NoHeaderScope , self :: no_inner_declarations :: NoInnerDeclarations , self :: no_invalid_constructor_super :: NoInvalidConstructorSuper , self :: no_non_null_assertion :: NoNonNullAssertion , self :: no_precision_loss :: NoPrecisionLoss , self :: no_prototype_builtins :: NoPrototypeBuiltins , self :: no_redundant_alt :: NoRedundantAlt , self :: no_redundant_use_strict :: NoRedundantUseStrict , self :: no_self_compare :: NoSelfCompare , self :: no_setter_return :: NoSetterReturn , self :: no_string_case_mismatch :: NoStringCaseMismatch , self :: no_unreachable_super :: NoUnreachableSuper , self :: no_unsafe_finally :: NoUnsafeFinally , self :: no_unused_labels :: NoUnusedLabels , self :: no_useless_switch_case :: NoUselessSwitchCase , self :: no_void_type_return :: NoVoidTypeReturn , self :: no_with :: NoWith , self :: use_default_parameter_last :: UseDefaultParameterLast , self :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast , self :: use_enum_initializers :: UseEnumInitializers , self :: use_exponentiation_operator :: UseExponentiationOperator , self :: use_is_nan :: UseIsNan , self :: use_media_caption :: UseMediaCaption , self :: use_numeric_literals :: UseNumericLiterals , self :: use_yield :: UseYield ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/no_prototype_builtins.rs
+use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_js_syntax::{AnyJsExpression, JsCallExpression, TextRange};
+use rome_rowan::AstNodeList;
+
+declare_rule! {
+ /// Disallow direct use of `Object.prototype` builtins.
+ ///
+ /// ECMAScript 5.1 added `Object.create` which allows the creation of an object with a custom prototype.
+ /// This pattern is often used for objects used as Maps. However, this pattern can lead to errors
+ /// if something else relies on prototype properties/methods.
+ /// Moreover, the methods could be shadowed, this can lead to random bugs and denial of service
+ /// vulnerabilities. For example, calling `hasOwnProperty` directly on parsed JSON like `{"hasOwnProperty": 1}` could lead to vulnerabilities.
+ /// To avoid subtle bugs like this, you should call these methods from `Object.prototype`.
+ /// For example, `foo.isPrototypeof(bar)` should be replaced with `Object.prototype.isPrototypeof.call(foo, "bar")`
+ /// As for the `hasOwn` method, `foo.hasOwn("bar")` should be replaced with `Object.hasOwn(foo, "bar")`.
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```js,expect_diagnostic
+ /// var invalid = foo.hasOwnProperty("bar");
+ /// ```
+ ///
+ /// ```js,expect_diagnostic
+ /// var invalid = foo.isPrototypeOf(bar);
+ /// ```
+ ///
+ /// ```js,expect_diagnostic
+ /// var invalid = foo.propertyIsEnumerable("bar");
+ /// ```
+ ///
+ /// ## Valid
+ ///
+ /// ```js
+ /// var valid = Object.hasOwn(foo, "bar");
+ /// var valid = Object.prototype.isPrototypeOf.call(foo, bar);
+ /// var valid = {}.propertyIsEnumerable.call(foo, "bar");
+ /// ```
+ ///
+ pub(crate) NoPrototypeBuiltins {
+ version: "next",
+ name: "noPrototypeBuiltins",
+ recommended: false,
+ }
+}
+
+pub(crate) struct RuleState {
+ prototype_builtins_method_name: String,
+ text_range: TextRange,
+}
+
+impl Rule for NoPrototypeBuiltins {
+ type Query = Ast<JsCallExpression>;
+ type State = RuleState;
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let call_expr = ctx.query();
+ let callee = call_expr.callee().ok()?.omit_parentheses();
+
+ match callee {
+ AnyJsExpression::JsComputedMemberExpression(expr) => {
+ let expr = expr.member().ok()?;
+ match expr {
+ AnyJsExpression::AnyJsLiteralExpression(expr) => {
+ let literal_expr = expr.as_js_string_literal_expression()?;
+ let token_text = literal_expr.inner_string_text().ok()?;
+ let token = literal_expr.value_token().ok()?;
+
+ is_prototype_builtins(token_text.text()).then_some(RuleState {
+ prototype_builtins_method_name: token_text.to_string(),
+ text_range: token.text_range(),
+ })
+ }
+ AnyJsExpression::JsTemplateExpression(expr) => {
+ let template_element = expr.as_fields().elements.first()?;
+ let template_chunk_token = template_element
+ .as_js_template_chunk_element()?
+ .template_chunk_token()
+ .ok()?;
+ let token_text = template_chunk_token.text();
+ is_prototype_builtins(token_text).then_some(RuleState {
+ prototype_builtins_method_name: token_text.to_string(),
+ text_range: template_chunk_token.text_trimmed_range(),
+ })
+ }
+ _ => None,
+ }
+ }
+ AnyJsExpression::JsStaticMemberExpression(expr) => {
+ let member = expr.as_fields().member.ok()?;
+ let value_token = member.as_js_name()?.value_token().ok()?;
+ let token_text = value_token.text();
+ is_prototype_builtins(token_text).then_some(RuleState {
+ prototype_builtins_method_name: token_text.to_string(),
+ text_range: value_token.text_range(),
+ })
+ }
+ _ => None,
+ }
+ }
+
+ fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+ let diag = RuleDiagnostic::new(
+ rule_category!(),
+ state.text_range,
+ markup! {
+ "Do not access Object.prototype method '"{&state.prototype_builtins_method_name}"' from target object."
+ },
+ );
+
+ if state.prototype_builtins_method_name == "hasOwnProperty" {
+ Some(
+ diag.note(markup! {
+ "It's recommended using "<Emphasis>"Object.hasOwn()"</Emphasis>" instead of using "<Emphasis>"Object.hasOwnProperty()"</Emphasis>"."
+ })
+ .note(markup! {
+ "See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn">"MDN web docs"</Hyperlink>" for more details."
+ }),
+ )
+ } else {
+ Some(diag)
+ }
+ }
+}
+
+/// Chekcks if the `Object.prototype` builtins called directly.
+fn is_prototype_builtins(token_text: &str) -> bool {
+ matches!(
+ token_text,
+ "hasOwnProperty" | "isPrototypeOf" | "propertyIsEnumerable"
+ )
+}

ADD
crates/rome_js_analyze/tests/specs/nursery/noPrototypeBuiltins/invalid.js
+foo.hasOwnProperty("bar");
+foo.isPrototypeOf(bar);
+foo.propertyIsEnumerable("bar");
+foo.bar.hasOwnProperty("bar");
+foo.bar.baz.isPrototypeOf("bar");
+foo["hasOwnProperty"]("bar");
+foo[`isPrototypeOf`]("bar").baz;
+foo?.hasOwnProperty(bar);
+(foo?.hasOwnProperty)("bar");
+foo?.["hasOwnProperty"]("bar");
+(foo?.[`hasOwnProperty`])("bar");
\ No newline at end of file

ADD
crates/rome_js_analyze/tests/specs/nursery/noPrototypeBuiltins/valid.js
+Object.hasOwn(foo, "bar");
+Object.prototype.hasOwnProperty.call(foo, "bar");
+Object.prototype.isPrototypeOf.call(foo, bar);
+Object.prototype.propertyIsEnumerable.call(foo, 'bar');
+Object.prototype.hasOwnProperty.apply(foo, ['bar']);
+Object.prototype.isPrototypeOf.apply(foo, ['bar']);
+Object.prototype.propertyIsEnumerable.apply(foo, ['bar']);
+foo.hasOwnProperty;
+foo.hasOwnProperty.bar();
+foo(hasOwnProperty);
+hasOwnProperty(foo, 'bar');
+isPrototypeOf(foo, 'bar');
+propertyIsEnumerable(foo, 'bar');
+({}.hasOwnProperty.call(foo, 'bar'));
+({}.isPrototypeOf.call(foo, 'bar'));
+({}.propertyIsEnumerable.call(foo, 'bar'));
+({}.hasOwnProperty.apply(foo, ['bar']));
+({}.isPrototypeOf.apply(foo, ['bar']));
+({}.propertyIsEnumerable.apply(foo, ['bar']));
+foo[hasOwnProperty]('bar');
+foo['HasOwnProperty']('bar');
\ No newline at end of file

ADD
crates/rome_js_analyze/tests/specs/nursery/noPrototypeBuiltins/valid.js.snap
+---
+source: crates/rome_js_analyze/tests/spec_tests.rs
+expression: valid.js
+---
+# Input
+```js
+Object.hasOwn(foo, "bar");
+Object.prototype.hasOwnProperty.call(foo, "bar");
+Object.prototype.isPrototypeOf.call(foo, bar);
+Object.prototype.propertyIsEnumerable.call(foo, 'bar');
+Object.prototype.hasOwnProperty.apply(foo, ['bar']);
+Object.prototype.isPrototypeOf.apply(foo, ['bar']);
+Object.prototype.propertyIsEnumerable.apply(foo, ['bar']);
+foo.hasOwnProperty;
+foo.hasOwnProperty.bar();
+foo(hasOwnProperty);
+hasOwnProperty(foo, 'bar');
+isPrototypeOf(foo, 'bar');
+propertyIsEnumerable(foo, 'bar');
+({}.hasOwnProperty.call(foo, 'bar'));
+({}.isPrototypeOf.call(foo, 'bar'));
+({}.propertyIsEnumerable.call(foo, 'bar'));
+({}.hasOwnProperty.apply(foo, ['bar']));
+({}.isPrototypeOf.apply(foo, ['bar']));
+({}.propertyIsEnumerable.apply(foo, ['bar']));
+foo[hasOwnProperty]('bar');
+foo['HasOwnProperty']('bar');
+```
+
+

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -773,6 +773,8 @@ struct NurserySchema {
no_noninteractive_element_to_interactive_role: Option<RuleConfiguration>,
#[doc = "Disallow literal numbers that lose precision"]
no_precision_loss: Option<RuleConfiguration>,
+ #[doc = "Disallow direct use of Object.prototype builtins."]
+ no_prototype_builtins: Option<RuleConfiguration>,
#[doc = "Enforce img alt prop does not contain the word \"image\", \"picture\", or \"photo\"."]
no_redundant_alt: Option<RuleConfiguration>,
#[doc = "Prevents from having redundant \"use strict\"."]
@@ -836,7 +838,7 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 51] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 52] = [
"noAccessKey",
"noAssignInExpressions",
"noBannedTypes",
@@ -858,6 +860,7 @@ impl Nursery {
"noNonNullAssertion",
"noNoninteractiveElementToInteractiveRole",
"noPrecisionLoss",
+ "noPrototypeBuiltins",
"noRedundantAlt",
"noRedundantUseStrict",
"noRestrictedGlobals",
@@ -952,8 +955,7 @@ impl Nursery {
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[16]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[17]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[19]),
- RuleFilter::Rule("nursery", Self::CATEGORY_RULES[21]),
- RuleFilter::Rule("nursery", Self::CATEGORY_RULES[24]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[22]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[25]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[26]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[27]),
@@ -963,19 +965,20 @@ impl Nursery {
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[31]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[32]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[33]),
- RuleFilter::Rule("nursery", Self::CATEGORY_RULES[35]),
- RuleFilter::Rule("nursery", Self::CATEGORY_RULES[37]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[34]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[36]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[38]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[39]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[40]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[41]),
- RuleFilter::Rule("nursery", Self::CATEGORY_RULES[44]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[42]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[45]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[46]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[47]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[48]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[49]),
RuleFilter::Rule("nursery", Self::CATEGORY_RULES[50]),
+ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[51]),
];
pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) }
pub(crate) fn get_enabled_rules(&self) -> IndexSet<RuleFilter> {

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "noPrototypeBuiltins": {
+ "description": "Disallow direct use of Object.prototype builtins.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noRedundantAlt": {
"description": "Enforce img alt prop does not contain the word \"image\", \"picture\", or \"photo\".",
"anyOf": [

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -368,6 +368,10 @@ export interface Nursery {
* Disallow literal numbers that lose precision
*/
noPrecisionLoss?: RuleConfiguration;
+ /**
+ * Disallow direct use of Object.prototype builtins.
+ */
+ noPrototypeBuiltins?: RuleConfiguration;
/**
* Enforce img alt prop does not contain the word "image", "picture", or "photo".
*/
@@ -814,6 +818,7 @@ export type Category =
| "lint/nursery/noDuplicateJsxProps"
| "lint/nursery/useYield"
| "lint/nursery/noGlobalObjectCalls"
+ | "lint/nursery/noPrototypeBuiltins"
| "lint/performance/noDelete"
| "lint/security/noDangerouslySetInnerHtml"
| "lint/security/noDangerouslySetInnerHtmlWithChildren"

MODIFY
npm/rome/configuration_schema.json
npm/rome/configuration_schema.json
}
]
},
+ "noPrototypeBuiltins": {
+ "description": "Disallow direct use of Object.prototype builtins.",
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"noRedundantAlt": {
"description": "Enforce img alt prop does not contain the word \"image\", \"picture\", or \"photo\".",
"anyOf": [

MODIFY
website/src/pages/lint/rules/index.mdx
website/src/pages/lint/rules/index.mdx
@@ -612,6 +612,12 @@ Enforce that interactive ARIA roles are not assigned to non-interactive HTML ele
Disallow literal numbers that lose precision
</section>
<section class="rule">
+<h3 data-toc-exclude id="noPrototypeBuiltins">
+ <a href="/lint/rules/noPrototypeBuiltins">noPrototypeBuiltins</a>
+</h3>
+Disallow direct use of <code>Object.prototype</code> builtins.
+</section>
+<section class="rule">
<h3 data-toc-exclude id="noRedundantAlt">
<a href="/lint/rules/noRedundantAlt">noRedundantAlt</a>
</h3>

feat(rome_js_analyze): `noPrototypeBuiltins` (#4101)

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
Closes https://github.com/rome/tools/issues/3979