From 0144a8ea568916490249ed89fc79356948bc01e1 Mon Sep 17 00:00:00 2001 From: Victorien Elvinger Date: Sat, 15 Jul 2023 00:18:47 +0200 Subject: [PATCH] feat(rome_js_analyze): useIsArray --- CHANGELOG.md | 4 + .../src/categories.rs | 1 + crates/rome_formatter/src/comments/builder.rs | 29 +-- .../src/analyzers/a11y/no_auto_focus.rs | 22 +- .../analyzers/complexity/no_useless_label.rs | 17 +- .../analyzers/complexity/no_useless_rename.rs | 17 +- .../complexity/no_useless_switch_case.rs | 12 +- .../complexity/use_optional_chain.rs | 50 ++-- .../analyzers/correctness/no_new_symbol.rs | 18 +- .../correctness/no_switch_declarations.rs | 7 +- .../correctness/no_unnecessary_continue.rs | 18 +- .../nursery/use_grouped_type_import.rs | 5 +- .../analyzers/style/no_implicit_boolean.rs | 3 +- .../analyzers/style/no_inferrable_types.rs | 20 +- .../analyzers/style/use_block_statements.rs | 35 ++- .../style/use_default_parameter_last.rs | 40 +--- .../style/use_self_closing_elements.rs | 2 +- .../style/use_shorthand_array_type.rs | 10 +- .../style/use_single_case_statement.rs | 9 +- .../style/use_single_var_declarator.rs | 6 +- .../src/analyzers/style/use_template.rs | 68 +----- .../suspicious/no_assign_in_expressions.rs | 1 - .../analyzers/suspicious/no_comment_text.rs | 11 +- .../src/analyzers/suspicious/no_const_enum.rs | 18 +- .../analyzers/suspicious/no_sparse_array.rs | 2 +- .../assists/correctness/organize_imports.rs | 56 +---- .../src/semantic_analyzers/nursery.rs | 2 + .../nursery/use_is_array.rs | 112 +++++++++ .../tests/specs/nursery/useIsArray/invalid.js | 6 + .../specs/nursery/useIsArray/invalid.js.snap | 90 +++++++ .../nursery/useIsArray/valid-shadowing.js | 3 + .../useIsArray/valid-shadowing.js.snap | 13 + .../tests/specs/nursery/useIsArray/valid.js | 4 + .../specs/nursery/useIsArray/valid.js.snap | 13 + .../rome_js_formatter/src/syntax_rewriter.rs | 5 +- crates/rome_js_syntax/src/binding_ext.rs | 8 +- crates/rome_js_syntax/src/expr_ext.rs | 6 +- crates/rome_js_syntax/src/parameter_ext.rs | 18 +- .../src/transformers/ts_enum.rs | 17 +- crates/rome_rowan/src/ast/batch.rs | 6 +- crates/rome_rowan/src/ast/mod.rs | 59 ++++- crates/rome_rowan/src/green/trivia.rs | 2 +- crates/rome_rowan/src/lib.rs | 9 +- crates/rome_rowan/src/syntax.rs | 7 +- crates/rome_rowan/src/syntax/node.rs | 224 +++++++++++++++++- crates/rome_rowan/src/syntax/rewriter.rs | 9 +- crates/rome_rowan/src/syntax/token.rs | 150 ++++++++++-- crates/rome_rowan/src/syntax/trivia.rs | 75 ++++++ .../src/configuration/linter/rules.rs | 51 ++-- .../src/configuration/parse/json/rules.rs | 24 ++ editors/vscode/configuration_schema.json | 7 + npm/backend-jsonrpc/src/workspace.ts | 5 + npm/rome/configuration_schema.json | 7 + .../components/generated/NumberOfRules.astro | 2 +- website/src/pages/lint/rules/index.mdx | 6 + website/src/pages/lint/rules/useIsArray.md | 58 +++++ xtask/codegen/src/generate_bindings.rs | 12 +- xtask/codegen/src/parser_tests.rs | 4 +- 58 files changed, 1057 insertions(+), 438 deletions(-) create mode 100644 crates/rome_js_analyze/src/semantic_analyzers/nursery/use_is_array.rs create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useIsArray/invalid.js create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useIsArray/invalid.js.snap create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid-shadowing.js create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid-shadowing.js.snap create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid.js create mode 100644 crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid.js.snap create mode 100644 website/src/pages/lint/rules/useIsArray.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0131c0fa81b..9f37bdeda82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,6 +147,10 @@ if no error diagnostics are emitted. This rule disallows useless `export {}`. +- Add [`useIsArray`](https://docs.rome.tools/lint/rules/useIsArray/) + + This rule proposes using `Array.isArray()` instead of `instanceof Array`. + #### Other changes - Add new TypeScript globals (`AsyncDisposable`, `Awaited`, `DecoratorContext`, and others) [4643](https://github.com/rome/tools/issues/4643). diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index 9ae3141167a..7f65283bc09 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -103,6 +103,7 @@ define_categories! { "lint/nursery/useGroupedTypeImport": "https://docs.rome.tools/lint/rules/useGroupedTypeImport", "lint/nursery/useHeadingContent": "https://docs.rome.tools/lint/rules/useHeadingContent", "lint/nursery/useHookAtTopLevel": "https://docs.rome.tools/lint/rules/useHookAtTopLevel", + "lint/nursery/useIsArray": "https://docs.rome.tools/lint/rules/useIsArray", "lint/nursery/useIsNan": "https://docs.rome.tools/lint/rules/useIsNan", "lint/nursery/useLiteralEnumMembers": "https://docs.rome.tools/lint/rules/useLiteralEnumMembers", "lint/nursery/useLiteralKeys": "https://docs.rome.tools/lint/rules/useLiteralKeys", diff --git a/crates/rome_formatter/src/comments/builder.rs b/crates/rome_formatter/src/comments/builder.rs index 715405545af..ab5559d2a43 100644 --- a/crates/rome_formatter/src/comments/builder.rs +++ b/crates/rome_formatter/src/comments/builder.rs @@ -646,7 +646,10 @@ mod tests { JsSyntaxKind, JsSyntaxNode, JsUnaryExpression, }; use rome_rowan::syntax::SyntaxElementKey; - use rome_rowan::{AstNode, BatchMutation, SyntaxNode, SyntaxTriviaPieceComments, TextRange}; + use rome_rowan::{ + chain_trivia_pieces, AstNode, BatchMutation, SyntaxNode, SyntaxTriviaPieceComments, + TextRange, + }; use std::cell::RefCell; #[test] @@ -874,22 +877,14 @@ b;"#; let identifier_token = reference_identifier.value_token().unwrap(); let new_identifier_token = identifier_token - .with_leading_trivia_pieces( - l_paren - .leading_trivia() - .pieces() - .chain(l_paren.trailing_trivia().pieces()) - .chain(identifier_token.leading_trivia().pieces()) - .collect::>(), - ) - .with_trailing_trivia_pieces( - identifier_token - .trailing_trivia() - .pieces() - .chain(r_paren.leading_trivia().pieces()) - .chain(r_paren.trailing_trivia().pieces()) - .collect::>(), - ); + .prepend_trivia_pieces(chain_trivia_pieces( + l_paren.leading_trivia().pieces(), + l_paren.trailing_trivia().pieces(), + )) + .append_trivia_pieces(chain_trivia_pieces( + r_paren.leading_trivia().pieces(), + r_paren.trailing_trivia().pieces(), + )); let new_reference_identifier = reference_identifier.with_value_token(new_identifier_token); diff --git a/crates/rome_js_analyze/src/analyzers/a11y/no_auto_focus.rs b/crates/rome_js_analyze/src/analyzers/a11y/no_auto_focus.rs index 5da3350b9b6..dc054f575e3 100644 --- a/crates/rome_js_analyze/src/analyzers/a11y/no_auto_focus.rs +++ b/crates/rome_js_analyze/src/analyzers/a11y/no_auto_focus.rs @@ -72,7 +72,6 @@ impl Rule for NoAutoFocus { if node.is_custom_component() { return None; } - node.find_attribute_by_name("autoFocus") } @@ -88,24 +87,13 @@ impl Rule for NoAutoFocus { fn action(ctx: &RuleContext, attr: &Self::State) -> Option { let mut mutation = ctx.root().begin(); - let trailing_trivia = attr.syntax().last_trailing_trivia(); - if let Some(trailing_trivia) = trailing_trivia { - if trailing_trivia.pieces().any(|piece| piece.is_comments()) { - let element = attr.syntax().ancestors().find_map(AnyJsxElement::cast); - if let Some(name) = element.and_then(|e| e.name_value_token()) { - let trivia_pieces = name - .trailing_trivia() - .pieces() - .chain(trailing_trivia.pieces()) - .collect::>(); - let new_name = name.with_trailing_trivia_pieces(trivia_pieces); - mutation.replace_token_discard_trivia(name, new_name); - } - } + if attr.syntax().has_trailing_comments() { + let prev_token = attr.syntax().first_token()?.prev_token()?; + let new_token = + prev_token.append_trivia_pieces(attr.syntax().last_trailing_trivia()?.pieces()); + mutation.replace_token_discard_trivia(prev_token, new_token); } - mutation.remove_node(attr.clone()); - Some(JsRuleAction { category: ActionCategory::QuickFix, applicability: Applicability::MaybeIncorrect, diff --git a/crates/rome_js_analyze/src/analyzers/complexity/no_useless_label.rs b/crates/rome_js_analyze/src/analyzers/complexity/no_useless_label.rs index 8dd53a3b31e..1c2dca15792 100644 --- a/crates/rome_js_analyze/src/analyzers/complexity/no_useless_label.rs +++ b/crates/rome_js_analyze/src/analyzers/complexity/no_useless_label.rs @@ -8,7 +8,7 @@ use rome_js_syntax::{ }; use crate::JsRuleAction; -use rome_rowan::{chain_trivia_pieces, declare_node_union, AstNode, BatchMutationExt}; +use rome_rowan::{declare_node_union, AstNode, BatchMutationExt}; declare_rule! { /// Disallow unnecessary labels. @@ -111,21 +111,12 @@ impl Rule for NoUselessLabel { // We want to remove trailing spaces and keep all comments that follows `stmt_token` // e.g. `break /* a comment */ ` to `break /* a comment */`. // This requires to traverse the trailing trivia in reverse order. - let mut stmt_token_trailing_trivia = stmt_token - .trailing_trivia() - .pieces() - .rev() - .skip_while(|p| p.is_newline() || p.is_whitespace()) - .collect::>(); - // We restore initial trivia order - stmt_token_trailing_trivia.reverse(); // We keep trailing trivia of `label_stmt` // e.g. `break label // a comment` -> `break // a comment` // We do not keep leading trivia of `label_stmt` because we assume that they are associated to the label. - let new_stmt_token = stmt_token.with_trailing_trivia_pieces(chain_trivia_pieces( - stmt_token_trailing_trivia.into_iter(), - label_token.trailing_trivia().pieces(), - )); + let new_stmt_token = stmt_token + .trim_trailing_trivia() + .append_trivia_pieces(label_token.trailing_trivia().pieces()); let mut mutation = ctx.root().begin(); mutation.remove_token(label_token); mutation.replace_token_discard_trivia(stmt_token, new_stmt_token); diff --git a/crates/rome_js_analyze/src/analyzers/complexity/no_useless_rename.rs b/crates/rome_js_analyze/src/analyzers/complexity/no_useless_rename.rs index c161b7e6914..fe3603c56be 100644 --- a/crates/rome_js_analyze/src/analyzers/complexity/no_useless_rename.rs +++ b/crates/rome_js_analyze/src/analyzers/complexity/no_useless_rename.rs @@ -7,7 +7,7 @@ use rome_js_syntax::{ JsExportNamedFromSpecifier, JsExportNamedSpecifier, JsNamedImportSpecifier, JsObjectBindingPatternProperty, JsSyntaxElement, }; -use rome_rowan::{declare_node_union, AstNode, BatchMutationExt}; +use rome_rowan::{declare_node_union, trim_leading_trivia_pieces, AstNode, BatchMutationExt}; use crate::JsRuleAction; @@ -126,18 +126,9 @@ impl Rule for NoUselessRename { let last_token = x.source_name().ok()?.value().ok()?; let export_as = x.export_as()?; let export_as_last_token = export_as.exported_name().ok()?.value().ok()?; - let replacing_token = last_token.with_trailing_trivia_pieces( - last_token - .trailing_trivia() - .pieces() - .chain( - export_as_last_token - .trailing_trivia() - .pieces() - .skip_while(|p| p.is_newline() || p.is_whitespace()), - ) - .collect::>(), - ); + let replacing_token = last_token.append_trivia_pieces(trim_leading_trivia_pieces( + export_as_last_token.trailing_trivia().pieces(), + )); mutation.remove_node(export_as); mutation.replace_token_discard_trivia(last_token, replacing_token); } diff --git a/crates/rome_js_analyze/src/analyzers/complexity/no_useless_switch_case.rs b/crates/rome_js_analyze/src/analyzers/complexity/no_useless_switch_case.rs index 2252dcc7ae3..6c2affb0823 100644 --- a/crates/rome_js_analyze/src/analyzers/complexity/no_useless_switch_case.rs +++ b/crates/rome_js_analyze/src/analyzers/complexity/no_useless_switch_case.rs @@ -139,15 +139,9 @@ impl Rule for NoUselessSwitchCase { let new_default_clause = default_clause .to_owned() .with_consequent(consequent) - .with_colon_token( - default_clause_colon_token.with_trailing_trivia_pieces( - default_clause_colon_token - .trailing_trivia() - .pieces() - .chain(useless_case.colon_token().ok()?.trailing_trivia().pieces()) - .collect::>(), - ), - ); + .with_colon_token(default_clause_colon_token.append_trivia_pieces( + useless_case.colon_token().ok()?.trailing_trivia().pieces(), + )); mutation.remove_node(default_clause.to_owned()); mutation.replace_element( SyntaxElement::Node(useless_case.syntax().to_owned()), diff --git a/crates/rome_js_analyze/src/analyzers/complexity/use_optional_chain.rs b/crates/rome_js_analyze/src/analyzers/complexity/use_optional_chain.rs index 307a596005d..5e23dd53637 100644 --- a/crates/rome_js_analyze/src/analyzers/complexity/use_optional_chain.rs +++ b/crates/rome_js_analyze/src/analyzers/complexity/use_optional_chain.rs @@ -144,49 +144,32 @@ impl Rule for UseOptionalChain { .unwrap_or_else(|| expression.clone()); let next_expression = match next_expression { AnyJsExpression::JsCallExpression(call_expression) => { - let mut call_expression_builder = make::js_call_expression( - call_expression.callee().ok()?, - call_expression.arguments().ok()?, - ) - .with_optional_chain_token(make::token(T![?.])); - if let Some(type_arguments) = call_expression.type_arguments() { - call_expression_builder = - call_expression_builder.with_type_arguments(type_arguments); - } - let call_expression = call_expression_builder.build(); - AnyJsExpression::from(call_expression) + let optional_chain_token = call_expression + .optional_chain_token() + .unwrap_or_else(|| make::token(T![?.])); + call_expression + .with_optional_chain_token(Some(optional_chain_token)) + .into() } AnyJsExpression::JsStaticMemberExpression(member_expression) => { - let operator_token = member_expression.operator_token().ok()?; + let operator = member_expression.operator_token().ok()?; AnyJsExpression::from(make::js_static_member_expression( member_expression.object().ok()?, make::token(T![?.]) - .with_leading_trivia_pieces( - operator_token.leading_trivia().pieces(), - ) + .with_leading_trivia_pieces(operator.leading_trivia().pieces()) .with_trailing_trivia_pieces( - operator_token.trailing_trivia().pieces(), + operator.trailing_trivia().pieces(), ), member_expression.member().ok()?, )) } AnyJsExpression::JsComputedMemberExpression(member_expression) => { - let operator_token = match member_expression.optional_chain_token() { - Some(token) => make::token(T![?.]) - .with_leading_trivia_pieces(token.leading_trivia().pieces()) - .with_trailing_trivia_pieces(token.trailing_trivia().pieces()), - None => make::token(T![?.]), - }; - AnyJsExpression::from( - make::js_computed_member_expression( - member_expression.object().ok()?, - member_expression.l_brack_token().ok()?, - member_expression.member().ok()?, - member_expression.r_brack_token().ok()?, - ) - .with_optional_chain_token(operator_token) - .build(), - ) + let optional_chain_token = member_expression + .optional_chain_token() + .unwrap_or_else(|| make::token(T![?.])); + member_expression + .with_optional_chain_token(Some(optional_chain_token)) + .into() } _ => return None, }; @@ -746,7 +729,6 @@ impl LogicalOrLikeChain { fn trim_trailing_space(node: AnyJsExpression) -> Option { let Some(last_token_of_left_syntax) = node.syntax().last_token() else { return Some(node) }; - let next_token_of_left_syntax = - last_token_of_left_syntax.with_trailing_trivia(std::iter::empty()); + let next_token_of_left_syntax = last_token_of_left_syntax.with_trailing_trivia([]); node.replace_token_discard_trivia(last_token_of_left_syntax, next_token_of_left_syntax) } diff --git a/crates/rome_js_analyze/src/analyzers/correctness/no_new_symbol.rs b/crates/rome_js_analyze/src/analyzers/correctness/no_new_symbol.rs index a324680f52e..5eeba174282 100644 --- a/crates/rome_js_analyze/src/analyzers/correctness/no_new_symbol.rs +++ b/crates/rome_js_analyze/src/analyzers/correctness/no_new_symbol.rs @@ -4,7 +4,7 @@ use rome_console::markup; use rome_diagnostics::Applicability; use rome_js_factory::make; use rome_js_syntax::{AnyJsExpression, JsCallExpression, JsNewExpression, JsNewExpressionFields}; -use rome_rowan::{AstNode, BatchMutationExt}; +use rome_rowan::{chain_trivia_pieces, AstNode, BatchMutationExt}; declare_rule! { /// Disallow `new` operators with the `Symbol` object @@ -102,18 +102,10 @@ fn convert_new_expression_to_call_expression(expr: &JsNewExpression) -> Option>(); - - let symbol = symbol.with_leading_trivia_pieces(leading_trivia); - - callee = make::js_identifier_expression(make::js_reference_identifier(symbol)).into(); + callee = callee.prepend_trivia_pieces(chain_trivia_pieces( + new_token.leading_trivia().pieces(), + new_token.trailing_trivia().pieces(), + ))?; } Some(make::js_call_expression(callee, arguments).build()) diff --git a/crates/rome_js_analyze/src/analyzers/correctness/no_switch_declarations.rs b/crates/rome_js_analyze/src/analyzers/correctness/no_switch_declarations.rs index ce1f9bc8bd5..3ae419442a3 100644 --- a/crates/rome_js_analyze/src/analyzers/correctness/no_switch_declarations.rs +++ b/crates/rome_js_analyze/src/analyzers/correctness/no_switch_declarations.rs @@ -1,5 +1,3 @@ -use std::iter; - use rome_analyze::context::RuleContext; use rome_analyze::{declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic}; use rome_console::markup; @@ -125,14 +123,15 @@ impl Rule for NoSwitchDeclarations { let clause_token = switch_clause.clause_token().ok()?; let colon_token = switch_clause.colon_token().ok()?; let consequent = switch_clause.consequent(); - let new_colon_token = colon_token.with_trailing_trivia(iter::empty()); + let new_colon_token = colon_token.with_trailing_trivia([]); let new_consequent = make::js_statement_list(Some(AnyJsStatement::JsBlockStatement( make::js_block_statement( make::token(T!['{']) .with_leading_trivia(Some((TriviaPieceKind::Whitespace, " "))) .with_trailing_trivia_pieces(colon_token.trailing_trivia().pieces()), consequent.to_owned(), - make::token(T!['}']).with_leading_trivia_pieces(clause_token.indentation_trivia()), + make::token(T!['}']) + .with_leading_trivia_pieces(clause_token.indentation_trivia_pieces()), ), ))); let mut mutation = ctx.root().begin(); diff --git a/crates/rome_js_analyze/src/analyzers/correctness/no_unnecessary_continue.rs b/crates/rome_js_analyze/src/analyzers/correctness/no_unnecessary_continue.rs index 40ec558e088..7ee69d2e0bf 100644 --- a/crates/rome_js_analyze/src/analyzers/correctness/no_unnecessary_continue.rs +++ b/crates/rome_js_analyze/src/analyzers/correctness/no_unnecessary_continue.rs @@ -2,7 +2,7 @@ use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule use rome_console::markup; use rome_diagnostics::Applicability; use rome_js_syntax::{JsContinueStatement, JsLabeledStatement, JsSyntaxKind, JsSyntaxNode}; -use rome_rowan::{chain_trivia_pieces, AstNode, BatchMutationExt}; +use rome_rowan::{AstNode, BatchMutationExt}; use crate::{utils, JsRuleAction}; @@ -101,23 +101,7 @@ impl Rule for NoUnnecessaryContinue { fn action(ctx: &RuleContext, _: &Self::State) -> Option { let node = ctx.query(); - let continue_token = node.continue_token().ok()?; let mut mutation = ctx.root().begin(); - if continue_token.has_leading_comments() { - let prev_token = continue_token.prev_token()?; - let leading_trivia = continue_token.leading_trivia().pieces(); - let skip_count = leading_trivia.len() - - leading_trivia - .rev() - .position(|x| x.is_comments()) - .map(|pos| pos + 1) - .unwrap_or(0); - let new_token = prev_token.with_trailing_trivia_pieces(chain_trivia_pieces( - prev_token.trailing_trivia().pieces(), - continue_token.leading_trivia().pieces().skip(skip_count), - )); - mutation.replace_token(prev_token, new_token); - } utils::remove_statement(&mut mutation, node)?; Some(JsRuleAction { category: ActionCategory::QuickFix, diff --git a/crates/rome_js_analyze/src/analyzers/nursery/use_grouped_type_import.rs b/crates/rome_js_analyze/src/analyzers/nursery/use_grouped_type_import.rs index 6d36abe20d8..c5b7c0b405f 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery/use_grouped_type_import.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery/use_grouped_type_import.rs @@ -1,5 +1,3 @@ -use std::iter; - use crate::JsRuleAction; use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic}; use rome_console::markup; @@ -156,8 +154,7 @@ impl Rule for UseGroupedTypeImport { let new_node = node .clone() .with_type_token(Some( - make::token(T![type]) - .with_trailing_trivia(iter::once((TriviaPieceKind::Whitespace, " "))), + make::token(T![type]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), )) .with_named_import(AnyJsNamedImport::JsNamedImportSpecifiers( named_import_specifiers diff --git a/crates/rome_js_analyze/src/analyzers/style/no_implicit_boolean.rs b/crates/rome_js_analyze/src/analyzers/style/no_implicit_boolean.rs index fd6bb2dd4cb..a1c435ecf3f 100644 --- a/crates/rome_js_analyze/src/analyzers/style/no_implicit_boolean.rs +++ b/crates/rome_js_analyze/src/analyzers/style/no_implicit_boolean.rs @@ -95,8 +95,7 @@ impl Rule for NoImplicitBoolean { let last_token_of_name_syntax = name_syntax.last_token()?; // drop the trailing trivia of name_syntax, at CST level it means // clean the trailing trivia of last token of name_syntax - let next_last_token_of_name_syntax = - last_token_of_name_syntax.with_trailing_trivia(std::iter::empty()); + let next_last_token_of_name_syntax = last_token_of_name_syntax.with_trailing_trivia([]); let next_name = name.replace_token_discard_trivia( last_token_of_name_syntax, diff --git a/crates/rome_js_analyze/src/analyzers/style/no_inferrable_types.rs b/crates/rome_js_analyze/src/analyzers/style/no_inferrable_types.rs index 9b3458d8621..9a31b230c1a 100644 --- a/crates/rome_js_analyze/src/analyzers/style/no_inferrable_types.rs +++ b/crates/rome_js_analyze/src/analyzers/style/no_inferrable_types.rs @@ -8,7 +8,6 @@ use rome_js_syntax::{ JsVariableDeclarator, JsVariableDeclaratorList, TsPropertyParameter, TsReadonlyModifier, TsTypeAnnotation, }; -use rome_rowan::chain_trivia_pieces; use rome_rowan::AstNode; use rome_rowan::BatchMutationExt; @@ -169,18 +168,13 @@ impl Rule for NoInferrableTypes { fn action(ctx: &RuleContext, annotation: &Self::State) -> Option { let mut mutation = ctx.root().begin(); - let first_del_token = annotation.syntax().first_token()?; - let prev_token = first_del_token.prev_token()?; - let new_prev_token = prev_token.with_trailing_trivia_pieces(chain_trivia_pieces( - first_del_token.leading_trivia().pieces(), - prev_token.trailing_trivia().pieces(), - )); - let last_del_token = annotation.syntax().last_token()?; - let next_token = last_del_token.next_token()?; - let new_next_token = next_token.with_leading_trivia_pieces(chain_trivia_pieces( - last_del_token.trailing_trivia().pieces(), - next_token.leading_trivia().pieces(), - )); + let first_token = annotation.syntax().first_token()?; + let prev_token = first_token.prev_token()?; + let new_prev_token = prev_token.append_trivia_pieces(first_token.leading_trivia().pieces()); + let last_token = annotation.syntax().last_token()?; + let next_token = last_token.next_token()?; + let new_next_token = + next_token.prepend_trivia_pieces(last_token.trailing_trivia().pieces()); mutation.replace_token_discard_trivia(prev_token, new_prev_token); mutation.replace_token_discard_trivia(next_token, new_next_token); mutation.remove_node(annotation.clone()); diff --git a/crates/rome_js_analyze/src/analyzers/style/use_block_statements.rs b/crates/rome_js_analyze/src/analyzers/style/use_block_statements.rs index 34fac219b92..db14a0f66b4 100644 --- a/crates/rome_js_analyze/src/analyzers/style/use_block_statements.rs +++ b/crates/rome_js_analyze/src/analyzers/style/use_block_statements.rs @@ -1,5 +1,3 @@ -use std::iter; - use rome_analyze::context::RuleContext; use rome_analyze::{declare_rule, ActionCategory, Ast, Rule, RuleAction, RuleDiagnostic}; use rome_console::markup; @@ -161,8 +159,8 @@ impl Rule for UseBlockStatements { .unwrap_or(false); if !has_previous_space { - l_curly_token = l_curly_token - .with_leading_trivia(iter::once((TriviaPieceKind::Whitespace, " "))); + l_curly_token = + l_curly_token.with_leading_trivia([(TriviaPieceKind::Whitespace, " ")]); } // Clone the leading trivia of the single statement as the @@ -176,8 +174,8 @@ impl Rule for UseBlockStatements { // If the statement has no leading trivia, add a space after // the opening curly token if leading_trivia.is_empty() { - l_curly_token = l_curly_token - .with_trailing_trivia(iter::once((TriviaPieceKind::Whitespace, " "))); + l_curly_token = + l_curly_token.with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]); } // If the leading trivia for the statement contains any newline, @@ -226,11 +224,9 @@ impl Rule for UseBlockStatements { // if the node we have to enclose has some trailing comments, then we add a new line // to the leading trivia of the right curly brace if !has_trailing_single_line_comments { - r_curly_token - .with_leading_trivia(iter::once((TriviaPieceKind::Whitespace, " "))) + r_curly_token.with_leading_trivia([(TriviaPieceKind::Whitespace, " ")]) } else { - r_curly_token - .with_leading_trivia(iter::once((TriviaPieceKind::Newline, "\n"))) + r_curly_token.with_leading_trivia([(TriviaPieceKind::Newline, "\n")]) } }; @@ -238,7 +234,7 @@ impl Rule for UseBlockStatements { stmt.clone(), AnyJsStatement::JsBlockStatement(make::js_block_statement( l_curly_token, - make::js_statement_list(iter::once(stmt.clone())), + make::js_statement_list([stmt.clone()]), r_curly_token, )), ); @@ -333,16 +329,13 @@ macro_rules! use_block_statements_replace_body { ($stmt_type:ident, $builder_method:ident, $mutation:ident, $node:ident, $stmt:ident) => { $mutation.replace_node( $node.clone(), - AnyJsBlockStatement::$stmt_type( - $stmt - .clone() - .$builder_method(AnyJsStatement::JsBlockStatement(make::js_block_statement( - make::token(T!['{']) - .with_leading_trivia(iter::once((TriviaPieceKind::Whitespace, " "))), - make::js_statement_list([]), - make::token(T!['}']), - ))), - ), + AnyJsBlockStatement::$stmt_type($stmt.clone().$builder_method( + AnyJsStatement::JsBlockStatement(make::js_block_statement( + make::token(T!['{']).with_leading_trivia([(TriviaPieceKind::Whitespace, " ")]), + make::js_statement_list([]), + make::token(T!['}']), + )), + )), ) }; diff --git a/crates/rome_js_analyze/src/analyzers/style/use_default_parameter_last.rs b/crates/rome_js_analyze/src/analyzers/style/use_default_parameter_last.rs index 2e9452f7cca..d5ed3990e26 100644 --- a/crates/rome_js_analyze/src/analyzers/style/use_default_parameter_last.rs +++ b/crates/rome_js_analyze/src/analyzers/style/use_default_parameter_last.rs @@ -140,40 +140,18 @@ impl Rule for UseDefaultParameterLast { let mut mutation = ctx.root().begin(); if opt_param.question_mark_token().is_some() { let question_mark = opt_param.question_mark_token()?; - let next_token = question_mark.next_token()?; - let new_next_token = next_token.with_leading_trivia_pieces( - question_mark - .leading_trivia() - .pieces() - .chain(question_mark.trailing_trivia().pieces()) - .chain(next_token.leading_trivia().pieces()) - .collect::>(), - ); - mutation.replace_token_discard_trivia(next_token, new_next_token); + let prev_token = question_mark.prev_token()?; + let new_token = + prev_token.append_trivia_pieces(question_mark.trailing_trivia().pieces()); + mutation.replace_token_discard_trivia(prev_token, new_token); mutation.remove_token(question_mark); } else { let initializer = opt_param.initializer()?; - let first_initializer_token = initializer.syntax().first_token()?; - let last_initializer_token = initializer.syntax().last_token()?; - let prev_initializer_token = first_initializer_token.prev_token()?; - let trailing_trivia_count = prev_initializer_token.trailing_trivia().pieces().count(); - let last_trailing_non_space = prev_initializer_token - .trailing_trivia() - .pieces() - .rev() - .position(|p| !p.is_newline() && !p.is_whitespace()) - .unwrap_or(trailing_trivia_count); - let new_prev_initializer_token = prev_initializer_token.with_trailing_trivia_pieces( - prev_initializer_token - .trailing_trivia() - .pieces() - .take(trailing_trivia_count - last_trailing_non_space) - .chain(first_initializer_token.leading_trivia().pieces()) - .chain(last_initializer_token.trailing_trivia().pieces()) - .collect::>(), - ); - mutation - .replace_token_discard_trivia(prev_initializer_token, new_prev_initializer_token); + let prev_token = initializer.syntax().prev_sibling()?.last_token()?; + let new_token = prev_token + .trim_trailing_trivia() + .append_trivia_pieces(initializer.syntax().last_trailing_trivia()?.pieces()); + mutation.replace_token_discard_trivia(prev_token, new_token); mutation.remove_node(initializer); } Some(JsRuleAction { diff --git a/crates/rome_js_analyze/src/analyzers/style/use_self_closing_elements.rs b/crates/rome_js_analyze/src/analyzers/style/use_self_closing_elements.rs index 4dca9b013d7..f88f710fb59 100644 --- a/crates/rome_js_analyze/src/analyzers/style/use_self_closing_elements.rs +++ b/crates/rome_js_analyze/src/analyzers/style/use_self_closing_elements.rs @@ -110,7 +110,7 @@ impl Rule for UseSelfClosingElements { .map_or(true, |token| !token.trailing_trivia().text().ends_with(' ')); // drop the leading trivia of `r_angle_token` - r_angle_token = r_angle_token.with_leading_trivia(std::iter::empty()); + r_angle_token = r_angle_token.with_leading_trivia([]); if leading_trivia.is_empty() && need_extra_whitespace { slash_token.push(' '); diff --git a/crates/rome_js_analyze/src/analyzers/style/use_shorthand_array_type.rs b/crates/rome_js_analyze/src/analyzers/style/use_shorthand_array_type.rs index bd9ffedeefd..260f0568c8e 100644 --- a/crates/rome_js_analyze/src/analyzers/style/use_shorthand_array_type.rs +++ b/crates/rome_js_analyze/src/analyzers/style/use_shorthand_array_type.rs @@ -236,14 +236,8 @@ fn convert_to_array_type( types_array.into_iter(), (0..length - 1).map(|_| { make::token(T![|]) - .with_leading_trivia(std::iter::once(( - TriviaPieceKind::Whitespace, - " ", - ))) - .with_trailing_trivia(std::iter::once(( - TriviaPieceKind::Whitespace, - " ", - ))) + .with_leading_trivia([(TriviaPieceKind::Whitespace, " ")]) + .with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]) }), )); return Some(AnyTsType::TsUnionType(ts_union_type_builder.build())); diff --git a/crates/rome_js_analyze/src/analyzers/style/use_single_case_statement.rs b/crates/rome_js_analyze/src/analyzers/style/use_single_case_statement.rs index 59be8050d55..3efeaa42c9e 100644 --- a/crates/rome_js_analyze/src/analyzers/style/use_single_case_statement.rs +++ b/crates/rome_js_analyze/src/analyzers/style/use_single_case_statement.rs @@ -1,5 +1,3 @@ -use std::iter; - use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic}; use rome_console::markup; use rome_diagnostics::Applicability; @@ -75,14 +73,15 @@ impl Rule for UseSingleCaseStatement { let clause_token = switch_clause.clause_token().ok()?; let colon_token = switch_clause.colon_token().ok()?; let consequent = switch_clause.consequent(); - let new_colon_token = colon_token.with_trailing_trivia(iter::empty()); + let new_colon_token = colon_token.with_trailing_trivia([]); let new_consequent = make::js_statement_list(Some(AnyJsStatement::JsBlockStatement( make::js_block_statement( make::token(T!['{']) - .with_leading_trivia(Some((TriviaPieceKind::Whitespace, " "))) + .with_leading_trivia([(TriviaPieceKind::Whitespace, " ")]) .with_trailing_trivia_pieces(colon_token.trailing_trivia().pieces()), consequent.clone(), - make::token(T!['}']).with_leading_trivia_pieces(clause_token.indentation_trivia()), + make::token(T!['}']) + .with_leading_trivia_pieces(clause_token.indentation_trivia_pieces()), ), ))); let mut mutation = ctx.root().begin(); diff --git a/crates/rome_js_analyze/src/analyzers/style/use_single_var_declarator.rs b/crates/rome_js_analyze/src/analyzers/style/use_single_var_declarator.rs index 5896b7a2730..325db4d57f9 100644 --- a/crates/rome_js_analyze/src/analyzers/style/use_single_var_declarator.rs +++ b/crates/rome_js_analyze/src/analyzers/style/use_single_var_declarator.rs @@ -1,5 +1,3 @@ -use std::iter; - use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic}; use rome_console::markup; use rome_diagnostics::Applicability; @@ -142,7 +140,7 @@ impl Rule for UseSingleVarDeclarator { declarator = declarator .replace_token_discard_trivia( first_token.clone(), - first_token.with_leading_trivia(iter::empty()), + first_token.with_leading_trivia([]), ) // SAFETY: first_token is a known child of declarator .unwrap(); @@ -198,7 +196,7 @@ impl Rule for UseSingleVarDeclarator { let mut builder = make::js_variable_statement(make::js_variable_declaration( kind, - make::js_variable_declarator_list(iter::once(declarator), iter::empty()), + make::js_variable_declarator_list([declarator], []), )); let semicolon_token = if index + 1 == declarators_len { diff --git a/crates/rome_js_analyze/src/analyzers/style/use_template.rs b/crates/rome_js_analyze/src/analyzers/style/use_template.rs index f4217fa89a5..67e8134be00 100644 --- a/crates/rome_js_analyze/src/analyzers/style/use_template.rs +++ b/crates/rome_js_analyze/src/analyzers/style/use_template.rs @@ -8,7 +8,7 @@ use rome_js_syntax::{ AnyJsExpression, AnyJsLiteralExpression, JsBinaryExpression, JsBinaryOperator, JsLanguage, JsSyntaxKind, JsSyntaxToken, JsTemplateElementList, JsTemplateExpression, WalkEvent, T, }; -use rome_rowan::{AstNode, AstNodeExt, AstNodeList, BatchMutationExt, SyntaxToken, TriviaPiece}; +use rome_rowan::{AstNode, AstNodeList, BatchMutationExt, SyntaxToken}; use crate::{utils::escape::escape, utils::escape_string, JsRuleAction}; @@ -154,69 +154,15 @@ fn convert_expressions_to_js_template( reduced_exprs.extend(flatten_template_element_list(template.elements())?); } _ => { - // drop the leading and trailing whitespace of original expression make the generated `JsTemplate` a little nicer, if we don't do this - // the `1 * (2 + "foo") + "bar"` will become - // ```js - // `${1 * (2 + "foo") }bar` - // ``` - let expr_next = expr.clone(); - let first_token = expr_next.syntax().first_token()?; - // drop the leading whitespace of leading trivia of first token - // ## Example - // `1 * (2 + "foo") /**trailing */ + "bar"` - // ^^^^ drop ^^^^^^^^^^^^^ drop - let next_first_token = { - let token_kind = first_token.kind(); - let token_text = first_token.text().trim_start(); - let leading_trivia = first_token - .leading_trivia() - .pieces() - .skip_while(|item| item.is_newline() || item.is_whitespace()) - .map(|item| TriviaPiece::new(item.kind(), item.text_len())) - .collect::>(); - let trailing_trivia = first_token - .trailing_trivia() - .pieces() - .map(|item| TriviaPiece::new(item.kind(), item.text_len())); - JsSyntaxToken::new_detached( - token_kind, - token_text, - leading_trivia, - trailing_trivia, - ) - }; - let expr_next = expr_next - .replace_token_discard_trivia(first_token.clone(), next_first_token)?; - // Drop the trailing whitespace of trailing trivia of last token - let last_token = expr_next.syntax().last_token()?; - let next_last_token = { - let token_kind = last_token.kind(); - let mut trailing_trivia = last_token - .trailing_trivia() - .pieces() - .rev() - .skip_while(|item| item.is_newline() || item.is_whitespace()) - .map(|item| TriviaPiece::new(item.kind(), item.text_len())) - .collect::>(); - trailing_trivia.reverse(); - let leading_trivia = last_token - .leading_trivia() - .pieces() - .map(|item| TriviaPiece::new(item.kind(), item.text_len())); - let token_text = last_token.text().trim_end(); - JsSyntaxToken::new_detached( - token_kind, - token_text, - leading_trivia, - trailing_trivia, - ) - }; - let expr_next = - expr_next.replace_token_discard_trivia(last_token.clone(), next_last_token)?; let template_element = AnyJsTemplateElement::JsTemplateElement(make::js_template_element( SyntaxToken::new_detached(JsSyntaxKind::DOLLAR_CURLY, "${", [], []), - expr_next, + // Trim spaces to make the generated `JsTemplate` a little nicer, + // if we don't do this the `1 * (2 + "foo") + "bar"` will become: + // ```js + // `${1 * (2 + "foo") }bar` + // ``` + expr.clone().trim()?, SyntaxToken::new_detached(JsSyntaxKind::DOLLAR_CURLY, "}", [], []), )); reduced_exprs.push(template_element); diff --git a/crates/rome_js_analyze/src/analyzers/suspicious/no_assign_in_expressions.rs b/crates/rome_js_analyze/src/analyzers/suspicious/no_assign_in_expressions.rs index 7acedc8c73c..befcd3d43e4 100644 --- a/crates/rome_js_analyze/src/analyzers/suspicious/no_assign_in_expressions.rs +++ b/crates/rome_js_analyze/src/analyzers/suspicious/no_assign_in_expressions.rs @@ -123,7 +123,6 @@ impl Rule for NoAssignInExpressions { let op = assign.operator().ok()?; if let JsAssignmentOperator::Assign = op { let mut mutation = ctx.root().begin(); - let token = assign.operator_token().ok()?; let binary_expression = make::js_binary_expression( assign.left().ok()?.try_into_expression().ok()?, diff --git a/crates/rome_js_analyze/src/analyzers/suspicious/no_comment_text.rs b/crates/rome_js_analyze/src/analyzers/suspicious/no_comment_text.rs index d8391dbd12e..f748778abfc 100644 --- a/crates/rome_js_analyze/src/analyzers/suspicious/no_comment_text.rs +++ b/crates/rome_js_analyze/src/analyzers/suspicious/no_comment_text.rs @@ -86,13 +86,10 @@ impl Rule for NoCommentText { AnyJsxChild::JsxText(node.clone()), AnyJsxChild::JsxExpressionChild( make::jsx_expression_child( - make::token(T!['{']).with_trailing_trivia( - [( - TriviaPieceKind::MultiLineComment, - normalized_comment.as_str(), - )] - .into_iter(), - ), + make::token(T!['{']).with_trailing_trivia([( + TriviaPieceKind::MultiLineComment, + normalized_comment.as_str(), + )]), make::token(T!['}']), ) .build(), diff --git a/crates/rome_js_analyze/src/analyzers/suspicious/no_const_enum.rs b/crates/rome_js_analyze/src/analyzers/suspicious/no_const_enum.rs index 09c423fe963..07f19a9690b 100644 --- a/crates/rome_js_analyze/src/analyzers/suspicious/no_const_enum.rs +++ b/crates/rome_js_analyze/src/analyzers/suspicious/no_const_enum.rs @@ -3,7 +3,7 @@ use rome_analyze::{declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic}; use rome_console::markup; use rome_diagnostics::Applicability; use rome_js_syntax::TsEnumDeclaration; -use rome_rowan::{AstNode, BatchMutationExt}; +use rome_rowan::{chain_trivia_pieces, trim_leading_trivia_pieces, AstNode, BatchMutationExt}; use crate::JsRuleAction; @@ -73,18 +73,10 @@ impl Rule for NoConstEnum { let mut mutation = ctx.root().begin(); let const_token = enum_decl.const_token()?; let enum_token = enum_decl.enum_token().ok()?; - let transferred_trivia = const_token - .leading_trivia() - .pieces() - .chain( - const_token - .trailing_trivia() - .pieces() - .skip_while(|x| x.is_whitespace() || x.is_whitespace()), - ) - .chain(enum_token.leading_trivia().pieces()) - .collect::>(); - let new_enum_token = enum_token.with_leading_trivia_pieces(transferred_trivia); + let new_enum_token = enum_token.prepend_trivia_pieces(chain_trivia_pieces( + const_token.leading_trivia().pieces(), + trim_leading_trivia_pieces(const_token.trailing_trivia().pieces()), + )); mutation.remove_token(const_token); mutation.replace_token_discard_trivia(enum_token, new_enum_token); Some(JsRuleAction { diff --git a/crates/rome_js_analyze/src/analyzers/suspicious/no_sparse_array.rs b/crates/rome_js_analyze/src/analyzers/suspicious/no_sparse_array.rs index c744d5afff9..5b819ad1b6c 100644 --- a/crates/rome_js_analyze/src/analyzers/suspicious/no_sparse_array.rs +++ b/crates/rome_js_analyze/src/analyzers/suspicious/no_sparse_array.rs @@ -67,7 +67,7 @@ markup! { make::ident("undefined") } else { make::ident("undefined") - .with_leading_trivia(std::iter::once((TriviaPieceKind::Whitespace, " "))) + .with_leading_trivia([(TriviaPieceKind::Whitespace, " ")]) }; let ident_expr = make::js_identifier_expression(make::js_reference_identifier(undefine_indent)); diff --git a/crates/rome_js_analyze/src/assists/correctness/organize_imports.rs b/crates/rome_js_analyze/src/assists/correctness/organize_imports.rs index cbbf73a0ee8..4d0e06afd1b 100644 --- a/crates/rome_js_analyze/src/assists/correctness/organize_imports.rs +++ b/crates/rome_js_analyze/src/assists/correctness/organize_imports.rs @@ -2,7 +2,7 @@ use std::{ cell::Cell, cmp::Ordering, collections::{btree_map::Entry, BTreeMap}, - iter::once, + iter, mem::take, }; @@ -17,8 +17,8 @@ use rome_js_syntax::{ JsLanguage, JsModule, JsSyntaxToken, TextRange, TriviaPieceKind, T, }; use rome_rowan::{ - syntax::SyntaxTrivia, AstNode, AstNodeExt, AstNodeList, AstSeparatedList, BatchMutationExt, - SyntaxTokenText, SyntaxTriviaPiece, TriviaPiece, + chain_trivia_pieces, syntax::SyntaxTrivia, AstNode, AstNodeExt, AstNodeList, AstSeparatedList, + BatchMutationExt, SyntaxTokenText, SyntaxTriviaPiece, TriviaPiece, }; use crate::JsRuleAction; @@ -222,7 +222,7 @@ impl Rule for OrganizeImports { } node = node.with_import_token(first_token.with_leading_trivia_pieces( - exact_chain(group_leading_trivia, token_leading_trivia), + chain_trivia_pieces(group_leading_trivia, token_leading_trivia), )); } else if node_index > 0 && group_first_token == first_token { // If this node used to be in the leading position but @@ -235,7 +235,7 @@ impl Rule for OrganizeImports { .skip(group_leading_pieces); node = node.with_import_token(first_token.with_leading_trivia_pieces( - exact_chain(saved_leading_trivia, token_leading_trivia), + chain_trivia_pieces(saved_leading_trivia, token_leading_trivia), )); } @@ -440,10 +440,8 @@ impl ImportNode { if is_last && separator_count == last_element { // If this is the last item and we are removing its trailing separator, // move the trailing trivia from the separator to the node - let next_token = prev_token.with_trailing_trivia(exact_chain( - trivia_iter(&prev_token, TriviaPosition::Trailing), - trivia_iter(sep, TriviaPosition::Trailing), - )); + let next_token = + prev_token.append_trivia_pieces(sep.trailing_trivia().pieces()); node = node .replace_token_discard_trivia(prev_token, next_token) @@ -595,13 +593,10 @@ fn prepend_leading_newline( // Extract the leading newline from the `newline_source` token let leading_newline = newline_source.and_then(|newline_source| { - let leading_trivia = newline_source.leading_trivia(); - let leading_piece = leading_trivia.first()?; - + let leading_piece = newline_source.leading_trivia().first()?; if !leading_piece.is_newline() { return None; } - Some(leading_piece) }); @@ -614,32 +609,21 @@ fn prepend_leading_newline( }; let piece_count = 1 + leading_trivia.pieces().len(); - let mut iter = once(leading_newline).chain(trivia_iter(prev_token, TriviaPosition::Leading)); + let mut iter = iter::once(leading_newline).chain(leading_trivia_iter(prev_token)); Some(prev_token.with_leading_trivia((0..piece_count).map(|_| iter.next().unwrap()))) } -enum TriviaPosition { - Leading, - Trailing, -} - -/// Builds an iterator over the leading or trailing trivia pieces of a token +/// Builds an iterator over the leading trivia pieces of a token /// /// The items of the iterator inherit their lifetime from the token, /// rather than the trivia pieces themselves -fn trivia_iter( +fn leading_trivia_iter( token: &JsSyntaxToken, - position: TriviaPosition, -) -> impl Iterator + ExactSizeIterator { +) -> impl ExactSizeIterator { let token_text = token.text(); let token_range = token.text_range(); - - let trivia = match position { - TriviaPosition::Leading => token.leading_trivia(), - TriviaPosition::Trailing => token.trailing_trivia(), - }; - + let trivia = token.leading_trivia(); trivia.pieces().map(move |piece| { let piece_range = piece.text_range(); let range = TextRange::at(piece_range.start() - token_range.start(), piece_range.len()); @@ -692,17 +676,3 @@ fn has_empty_line(trivia: SyntaxTrivia) -> bool { prev_newline && was_newline }) } - -/// Returns an iterator yielding the full content of `lhs`, then the full -/// content of `rhs`. This is similar to the `.chain()` method on the -/// [Iterator] trait except the returned iterator implements [ExactSizeIterator] -fn exact_chain<'a, T>( - mut lhs: impl Iterator + ExactSizeIterator + 'a, - mut rhs: impl Iterator + ExactSizeIterator + 'a, -) -> impl Iterator + ExactSizeIterator + 'a { - let total_len = lhs.len() + rhs.len(); - (0..total_len).map(move |_| { - // SAFETY: The above range iterator should have the exact length of lhs + rhs - lhs.next().or_else(|| rhs.next()).unwrap() - }) -} diff --git a/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs b/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs index 59ba8cd39b5..ebb4d4c589b 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs @@ -11,6 +11,7 @@ pub(crate) mod no_global_is_nan; pub(crate) mod use_camel_case; pub(crate) mod use_exhaustive_dependencies; pub(crate) mod use_hook_at_top_level; +pub(crate) mod use_is_array; pub(crate) mod use_is_nan; pub(crate) mod use_naming_convention; @@ -27,6 +28,7 @@ declare_group! { self :: use_camel_case :: UseCamelCase , self :: use_exhaustive_dependencies :: UseExhaustiveDependencies , self :: use_hook_at_top_level :: UseHookAtTopLevel , + self :: use_is_array :: UseIsArray , self :: use_is_nan :: UseIsNan , self :: use_naming_convention :: UseNamingConvention , ] diff --git a/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_is_array.rs b/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_is_array.rs new file mode 100644 index 00000000000..b6e03f58502 --- /dev/null +++ b/crates/rome_js_analyze/src/semantic_analyzers/nursery/use_is_array.rs @@ -0,0 +1,112 @@ +use crate::{semantic_services::Semantic, JsRuleAction}; +use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Rule, RuleDiagnostic}; +use rome_console::markup; +use rome_diagnostics::Applicability; +use rome_js_factory::make; +use rome_js_syntax::{ + global_identifier, AnyJsCallArgument, AnyJsExpression, JsInstanceofExpression, T, +}; +use rome_rowan::{trim_leading_trivia_pieces, AstNode, BatchMutationExt}; + +declare_rule! { + /// Use `Array.isArray()` instead of `instanceof Array`. + /// + /// In _JavaScript_ some array-like objects such as _arguments_ are not instances of the `Array` class. /// + /// Moreover, the global `Array` class can be different between two execution contexts. + /// For instance, two frames in a web browser have a distinct `Array` class. + /// Passing arrays across these contexts, results in arrays that are not instances of the contextual global `Array` class. + /// To avoid these issues, use `Array.isArray()` instead of `instanceof Array`. + /// See the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) for more details. + /// + /// Source: https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-instanceof-array.md + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// const xs = []; + /// if (xs instanceof Array) {} + /// ``` + /// + /// ## Valid + /// + /// ```js + /// const xs = []; + /// if (Array.isArray(xs)) {} + /// ``` + /// + pub(crate) UseIsArray { + version: "next", + name: "useIsArray", + recommended: true, + } +} + +impl Rule for UseIsArray { + type Query = Semantic; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let model = ctx.model(); + let right = node.right().ok()?.omit_parentheses(); + let (reference, name) = global_identifier(&right)?; + if name.text() != "Array" { + return None; + } + model.binding(&reference).is_none().then_some(()) + } + + fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Use ""Array.isArray()"" instead of ""instanceof Array""." + }, + ) + .note(markup! { + "instanceof Array"" returns false for array-like objects and arrays from other execution contexts." + }), + ) + } + + fn action(ctx: &RuleContext, _: &Self::State) -> Option { + let node = ctx.query(); + let array = node.right().ok()?; + let array_trailing_trivia = array.syntax().last_trailing_trivia()?.pieces(); + let mut mutation = ctx.root().begin(); + let is_array = make::js_static_member_expression( + array.with_trailing_trivia_pieces([])?, + make::token(T![.]), + make::js_name(make::ident("isArray")).into(), + ); + let arg = AnyJsCallArgument::AnyJsExpression(node.left().ok()?.trim()?); + let instanceof_trailing_trivia = node.instanceof_token().ok()?.trailing_trivia().pieces(); + let args = make::js_call_arguments( + make::token(T!['(']).with_trailing_trivia_pieces(trim_leading_trivia_pieces( + instanceof_trailing_trivia, + )), + make::js_call_argument_list([arg], []), + make::token(T![')']).with_trailing_trivia_pieces(array_trailing_trivia), + ); + let call = make::js_call_expression(is_array.into(), args).build(); + mutation.replace_node_discard_trivia( + AnyJsExpression::JsInstanceofExpression(node.clone()), + call.into(), + ); + Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::MaybeIncorrect, + message: markup! { + "Use ""Array.isArray()"" instead." + } + .to_owned(), + mutation, + }) + } +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useIsArray/invalid.js b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/invalid.js new file mode 100644 index 00000000000..eb574d299fd --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/invalid.js @@ -0,0 +1,6 @@ + +if (/*a*/ arr /*b*/ instanceof /*c*/ Array /*d*/) {} + +const c = [1,2,3] instanceof globalThis.Array === b; + +const d = foo.bar[2] instanceof window.Array diff --git a/crates/rome_js_analyze/tests/specs/nursery/useIsArray/invalid.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/invalid.js.snap new file mode 100644 index 00000000000..29746deb9c1 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/invalid.js.snap @@ -0,0 +1,90 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js + +if (/*a*/ arr /*b*/ instanceof /*c*/ Array /*d*/) {} + +const c = [1,2,3] instanceof globalThis.Array === b; + +const d = foo.bar[2] instanceof window.Array + +``` + +# Diagnostics +``` +invalid.js:2:11 lint/nursery/useIsArray FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Array.isArray() instead of instanceof Array. + + > 2 │ if (/*a*/ arr /*b*/ instanceof /*c*/ Array /*d*/) {} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 │ + 4 │ const c = [1,2,3] instanceof globalThis.Array === b; + + i instanceof Array returns false for array-like objects and arrays from other execution contexts. + + i Suggested fix: Use Array.isArray() instead. + + 1 1 │ + 2 │ - if·(/*a*/·arr·/*b*/·instanceof·/*c*/·Array·/*d*/)·{} + 2 │ + if·(/*a*/·Array.isArray(/*c*/·arr·/*b*/)·/*d*/)·{} + 3 3 │ + 4 4 │ const c = [1,2,3] instanceof globalThis.Array === b; + + +``` + +``` +invalid.js:4:11 lint/nursery/useIsArray FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Array.isArray() instead of instanceof Array. + + 2 │ if (/*a*/ arr /*b*/ instanceof /*c*/ Array /*d*/) {} + 3 │ + > 4 │ const c = [1,2,3] instanceof globalThis.Array === b; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ + 6 │ const d = foo.bar[2] instanceof window.Array + + i instanceof Array returns false for array-like objects and arrays from other execution contexts. + + i Suggested fix: Use Array.isArray() instead. + + 2 2 │ if (/*a*/ arr /*b*/ instanceof /*c*/ Array /*d*/) {} + 3 3 │ + 4 │ - const·c·=·[1,2,3]·instanceof·globalThis.Array·===·b; + 4 │ + const·c·=·globalThis.Array.isArray([1,2,3])·===·b; + 5 5 │ + 6 6 │ const d = foo.bar[2] instanceof window.Array + + +``` + +``` +invalid.js:6:11 lint/nursery/useIsArray FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Array.isArray() instead of instanceof Array. + + 4 │ const c = [1,2,3] instanceof globalThis.Array === b; + 5 │ + > 6 │ const d = foo.bar[2] instanceof window.Array + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 7 │ + + i instanceof Array returns false for array-like objects and arrays from other execution contexts. + + i Suggested fix: Use Array.isArray() instead. + + 4 4 │ const c = [1,2,3] instanceof globalThis.Array === b; + 5 5 │ + 6 │ - const·d·=·foo.bar[2]·instanceof·window.Array + 6 │ + const·d·=·window.Array.isArray(foo.bar[2]) + 7 7 │ + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid-shadowing.js b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid-shadowing.js new file mode 100644 index 00000000000..1b5ba3d890c --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid-shadowing.js @@ -0,0 +1,3 @@ +import { b as Array } from "mod" +const a = new Array(); +const arr = a instanceof Arrray; diff --git a/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid-shadowing.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid-shadowing.js.snap new file mode 100644 index 00000000000..4336c32ef1d --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid-shadowing.js.snap @@ -0,0 +1,13 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: valid-shadowing.js +--- +# Input +```js +import { b as Array } from "mod" +const a = new Array(); +const arr = a instanceof Arrray; + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid.js b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid.js new file mode 100644 index 00000000000..8e1b57d5956 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid.js @@ -0,0 +1,4 @@ +Array.isArray(arr) +arr instanceof Object +a instanceof ns.Array +a.x[2] instanceof foo() \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid.js.snap new file mode 100644 index 00000000000..a6b6be24d1b --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useIsArray/valid.js.snap @@ -0,0 +1,13 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +Array.isArray(arr) +arr instanceof Object +a instanceof ns.Array +a.x[2] instanceof foo() +``` + + diff --git a/crates/rome_js_formatter/src/syntax_rewriter.rs b/crates/rome_js_formatter/src/syntax_rewriter.rs index 16910dfe68f..c6cae8d938b 100644 --- a/crates/rome_js_formatter/src/syntax_rewriter.rs +++ b/crates/rome_js_formatter/src/syntax_rewriter.rs @@ -289,10 +289,7 @@ impl JsFormatSyntaxRewriter { // doesn't contain ANY token, but we know that the subtree contains at least the first token. let last_token = updated.last_token().unwrap(); - let new_last = last_token.with_trailing_trivia_pieces(chain_trivia_pieces( - last_token.trailing_trivia().pieces(), - r_paren_trivia, - )); + let new_last = last_token.append_trivia_pieces(r_paren_trivia); self.source_map .add_deleted_range(r_paren.text_trimmed_range()); diff --git a/crates/rome_js_syntax/src/binding_ext.rs b/crates/rome_js_syntax/src/binding_ext.rs index 339eb250394..2baa9102c16 100644 --- a/crates/rome_js_syntax/src/binding_ext.rs +++ b/crates/rome_js_syntax/src/binding_ext.rs @@ -63,7 +63,7 @@ impl AnyJsBindingDeclaration { /// enum_id.into(), /// make::token(T!['{']), /// make::ts_enum_member_list( - /// std::iter::empty(), + /// [], /// Some(make::token(T![;])), /// ), /// make::token(T!['}']), @@ -75,18 +75,18 @@ impl AnyJsBindingDeclaration { /// namespace_id.into(), /// make::ts_module_block( /// make::token(T!['{']), - /// make::js_module_item_list(std::iter::empty()), + /// make::js_module_item_list([]), /// make::token(T!['}']), /// ), /// ).into(); /// /// let class_id = make::js_identifier_binding(make::ident("Order")); /// let class_decl: AnyJsBindingDeclaration = make::js_class_declaration( - /// make::js_decorator_list(std::iter::empty()), + /// make::js_decorator_list([]), /// make::token(T![class]), /// class_id.into(), /// make::token(T!['{']), - /// make::js_class_member_list(std::iter::empty()), + /// make::js_class_member_list([]), /// make::token(T!['}']), /// ).build().into(); /// diff --git a/crates/rome_js_syntax/src/expr_ext.rs b/crates/rome_js_syntax/src/expr_ext.rs index 5755c005f26..73919dcd830 100644 --- a/crates/rome_js_syntax/src/expr_ext.rs +++ b/crates/rome_js_syntax/src/expr_ext.rs @@ -537,7 +537,7 @@ impl JsTemplateExpression { /// let tick = make::token(JsSyntaxKind::BACKTICK); /// let empty_str = make::js_template_expression( /// tick.clone(), - /// make::js_template_element_list(iter::empty()), + /// make::js_template_element_list([]), /// tick.clone(), /// ).build(); /// @@ -548,7 +548,7 @@ impl JsTemplateExpression { /// ); /// let constant_str = make::js_template_expression( /// tick.clone(), - /// make::js_template_element_list(iter::once(chunk.clone())), + /// make::js_template_element_list([chunk.clone()]), /// tick.clone(), /// ).build(); /// @@ -571,7 +571,7 @@ impl JsTemplateExpression { /// ); /// let template_str = make::js_template_expression( /// tick.clone(), - /// make::js_template_element_list(iter::once(template_elt)), + /// make::js_template_element_list([template_elt]), /// tick, /// ).build(); /// diff --git a/crates/rome_js_syntax/src/parameter_ext.rs b/crates/rome_js_syntax/src/parameter_ext.rs index 93a659f5203..935aac5a0e3 100644 --- a/crates/rome_js_syntax/src/parameter_ext.rs +++ b/crates/rome_js_syntax/src/parameter_ext.rs @@ -24,7 +24,7 @@ use rome_rowan::{ /// Some(AnyJsParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( -/// make::js_decorator_list(std::iter::empty()), +/// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("params")), /// )), @@ -41,7 +41,7 @@ use rome_rowan::{ /// Some(AnyJsConstructorParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( -/// make::js_decorator_list(std::iter::empty()), +/// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("params")), /// )), @@ -96,7 +96,7 @@ impl AnyJsParameterList { /// Some(AnyJsParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( - /// make::js_decorator_list(std::iter::empty()), + /// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("params")), /// )), @@ -114,7 +114,7 @@ impl AnyJsParameterList { /// Some(AnyJsConstructorParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( - /// make::js_decorator_list(std::iter::empty()), + /// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("params")), /// )), @@ -156,7 +156,7 @@ impl AnyJsParameterList { /// Some(AnyJsParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( - /// make::js_decorator_list(std::iter::empty()), + /// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("params")), /// )), @@ -207,7 +207,7 @@ impl AnyJsParameterList { /// Some(AnyJsParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( - /// make::js_decorator_list(std::iter::empty()), + /// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("param1")), /// )), @@ -259,7 +259,7 @@ impl AnyJsParameterList { /// Some(AnyJsParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( - /// make::js_decorator_list(std::iter::empty()), + /// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("param1")), /// )), @@ -309,7 +309,7 @@ impl AnyJsParameterList { /// Some(AnyJsParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( - /// make::js_decorator_list(std::iter::empty()), + /// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("param1")), /// )), @@ -362,7 +362,7 @@ impl AnyJsParameterList { /// Some(AnyJsParameter::AnyJsFormalParameter( /// AnyJsFormalParameter::JsFormalParameter( /// make::js_formal_parameter( - /// make::js_decorator_list(std::iter::empty()), + /// make::js_decorator_list([]), /// AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding( /// make::js_identifier_binding(make::ident("param1")), /// )), diff --git a/crates/rome_js_transform/src/transformers/ts_enum.rs b/crates/rome_js_transform/src/transformers/ts_enum.rs index bbbcdedc1cb..e29ed808b86 100644 --- a/crates/rome_js_transform/src/transformers/ts_enum.rs +++ b/crates/rome_js_transform/src/transformers/ts_enum.rs @@ -19,7 +19,6 @@ use rome_js_syntax::{ TsEnumDeclaration, T, }; use rome_rowan::{AstNode, BatchMutationExt, TriviaPieceKind}; -use std::iter; declare_transformation! { /// Transform a TypeScript [TsEnumDeclaration] @@ -91,9 +90,9 @@ fn make_variable(node: &TsEnumMembers) -> JsVariableStatement { )) .build(); - let list = js_variable_declarator_list(iter::once(binding), iter::empty()); + let list = js_variable_declarator_list([binding], []); js_variable_statement(js_variable_declaration( - token(T![var]).with_trailing_trivia(iter::once((TriviaPieceKind::Whitespace, " "))), + token(T![var]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), list, )) .with_semicolon_token(token(T![;])) @@ -111,7 +110,7 @@ fn make_function_caller(node: &TsEnumMembers) -> JsExpressionStatement { )); let arguments = js_call_arguments( token(T!['(']), - js_call_argument_list(iter::once(argument), iter::empty()), + js_call_argument_list([argument], []), token(T![')']), ); let expression = js_call_expression( @@ -126,7 +125,7 @@ fn make_function_caller(node: &TsEnumMembers) -> JsExpressionStatement { fn make_function(node: &TsEnumMembers) -> JsFunctionExpression { let parameters_list = js_parameter_list( - iter::once(AnyJsParameter::AnyJsFormalParameter( + [AnyJsParameter::AnyJsFormalParameter( AnyJsFormalParameter::JsFormalParameter( js_formal_parameter( js_decorator_list(vec![]), @@ -136,14 +135,14 @@ fn make_function(node: &TsEnumMembers) -> JsFunctionExpression { ) .build(), ), - )), - iter::empty(), + )], + [], ); let parameters = js_parameters(token(T!['(']), parameters_list, token(T![')'])); let body = js_function_body( token(T!['{']), - js_directive_list(iter::empty()), + js_directive_list([]), make_members(node), token(T!['}']), ); @@ -171,7 +170,7 @@ fn make_logical_expression(node: &TsEnumMembers) -> JsLogicalExpression { token(T![=]), AnyJsExpression::JsObjectExpression(js_object_expression( token(T!['{']), - js_object_member_list(iter::empty(), iter::empty()), + js_object_member_list([], []), token(T!['}']), )), ); diff --git a/crates/rome_rowan/src/ast/batch.rs b/crates/rome_rowan/src/ast/batch.rs index e8280c74f75..09da8b6fbea 100644 --- a/crates/rome_rowan/src/ast/batch.rs +++ b/crates/rome_rowan/src/ast/batch.rs @@ -162,7 +162,7 @@ where Some(prev_trailing_trivia) => { token.with_trailing_trivia_pieces(prev_trailing_trivia.pieces()) } - None => token.with_trailing_trivia_pieces(empty()), + None => token.with_trailing_trivia_pieces([]), }; node = node.replace_child(token.into(), new_token.into()).unwrap(); @@ -175,14 +175,14 @@ where Some(prev_leading_trivia) => { token.with_leading_trivia_pieces(prev_leading_trivia.pieces()) } - None => token.with_leading_trivia_pieces(empty()), + None => token.with_leading_trivia_pieces([]), }; let new_token = match prev_trailing_trivia { Some(prev_trailing_trivia) => { new_token.with_trailing_trivia_pieces(prev_trailing_trivia.pieces()) } - None => new_token.with_trailing_trivia_pieces(empty()), + None => new_token.with_trailing_trivia_pieces([]), }; SyntaxElement::Token(new_token) } diff --git a/crates/rome_rowan/src/ast/mod.rs b/crates/rome_rowan/src/ast/mod.rs index 53a33e4c3c1..a320e4aedf5 100644 --- a/crates/rome_rowan/src/ast/mod.rs +++ b/crates/rome_rowan/src/ast/mod.rs @@ -16,7 +16,9 @@ mod batch; mod mutation; use crate::syntax::{SyntaxSlot, SyntaxSlots}; -use crate::{Language, RawSyntaxKind, SyntaxKind, SyntaxList, SyntaxNode, SyntaxToken}; +use crate::{ + Language, RawSyntaxKind, SyntaxKind, SyntaxList, SyntaxNode, SyntaxToken, SyntaxTriviaPiece, +}; pub use batch::*; pub use mutation::{AstNodeExt, AstNodeListExt, AstSeparatedListExt}; @@ -252,6 +254,61 @@ pub trait AstNode: Clone { fn parent>(&self) -> Option { self.syntax().parent().and_then(T::cast) } + + /// Return a new version of this node with the leading trivia of its first token replaced with `trivia`. + fn with_leading_trivia_pieces(self, trivia: I) -> Option + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + Self::cast(self.into_syntax().with_leading_trivia_pieces(trivia)?) + } + + /// Return a new version of this node with the trailing trivia of its last token replaced with `trivia`. + fn with_trailing_trivia_pieces(self, trivia: I) -> Option + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + Self::cast(self.into_syntax().with_trailing_trivia_pieces(trivia)?) + } + + // Return a new version of this node with `trivia` prepended to the leading trivia of the first token. + fn prepend_trivia_pieces(self, trivia: I) -> Option + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + Self::cast(self.into_syntax().prepend_trivia_pieces(trivia)?) + } + + // Return a new version of this node with `trivia` appended to the trailing trivia of the last token. + fn append_trivia_pieces(self, trivia: I) -> Option + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + Self::cast(self.into_syntax().append_trivia_pieces(trivia)?) + } + + /// Return a new version of this node without leading and trailing newlines and whitespaces. + fn trim(self) -> Option { + Self::cast( + self.into_syntax() + .trim_leading_trivia()? + .trim_trailing_trivia()?, + ) + } + + /// Return a new version of this node without leading newlines and whitespaces. + fn trim_start(self) -> Option { + Self::cast(self.into_syntax().trim_leading_trivia()?) + } + + /// Return a new version of this node without trailing newlines and whitespaces. + fn trim_end(self) -> Option { + Self::cast(self.into_syntax().trim_trailing_trivia()?) + } } pub trait SyntaxNodeCast { diff --git a/crates/rome_rowan/src/green/trivia.rs b/crates/rome_rowan/src/green/trivia.rs index 08102b4c801..d6c82fb4f37 100644 --- a/crates/rome_rowan/src/green/trivia.rs +++ b/crates/rome_rowan/src/green/trivia.rs @@ -162,7 +162,7 @@ mod tests { /// Creates a trivia containing a single piece pub fn single>(kind: TriviaPieceKind, len: L) -> Self { - Self::new(std::iter::once(TriviaPiece::new(kind, len))) + Self::new([TriviaPiece::new(kind, len)]) } } diff --git a/crates/rome_rowan/src/lib.rs b/crates/rome_rowan/src/lib.rs index 0f68906d5dc..b4f5e659fb1 100644 --- a/crates/rome_rowan/src/lib.rs +++ b/crates/rome_rowan/src/lib.rs @@ -40,10 +40,11 @@ pub use crate::{ file_source::{AnyFileSource, FileSource, FileSourceError}, green::{NodeCache, RawSyntaxKind}, syntax::{ - chain_trivia_pieces, ChainTriviaPiecesIterator, Language, SendNode, SyntaxElement, - SyntaxElementChildren, SyntaxKind, SyntaxList, SyntaxNode, SyntaxNodeChildren, - SyntaxNodeOptionExt, SyntaxRewriter, SyntaxSlot, SyntaxToken, SyntaxTriviaPiece, - SyntaxTriviaPieceComments, TriviaPiece, TriviaPieceKind, VisitNodeSignal, + chain_trivia_pieces, trim_leading_trivia_pieces, trim_trailing_trivia_pieces, + ChainTriviaPiecesIterator, Language, SendNode, SyntaxElement, SyntaxElementChildren, + SyntaxKind, SyntaxList, SyntaxNode, SyntaxNodeChildren, SyntaxNodeOptionExt, + SyntaxRewriter, SyntaxSlot, SyntaxToken, SyntaxTriviaPiece, SyntaxTriviaPieceComments, + TriviaPiece, TriviaPieceKind, VisitNodeSignal, }, syntax_factory::*, syntax_node_text::SyntaxNodeText, diff --git a/crates/rome_rowan/src/syntax.rs b/crates/rome_rowan/src/syntax.rs index c7511ccef0b..3af0327e7c6 100644 --- a/crates/rome_rowan/src/syntax.rs +++ b/crates/rome_rowan/src/syntax.rs @@ -16,9 +16,10 @@ use std::fmt; use std::fmt::Debug; pub use token::SyntaxToken; pub use trivia::{ - chain_trivia_pieces, ChainTriviaPiecesIterator, SyntaxTrivia, SyntaxTriviaPiece, - SyntaxTriviaPieceComments, SyntaxTriviaPieceNewline, SyntaxTriviaPieceSkipped, - SyntaxTriviaPieceWhitespace, SyntaxTriviaPiecesIterator, TriviaPiece, TriviaPieceKind, + chain_trivia_pieces, trim_leading_trivia_pieces, trim_trailing_trivia_pieces, + ChainTriviaPiecesIterator, SyntaxTrivia, SyntaxTriviaPiece, SyntaxTriviaPieceComments, + SyntaxTriviaPieceNewline, SyntaxTriviaPieceSkipped, SyntaxTriviaPieceWhitespace, + SyntaxTriviaPiecesIterator, TriviaPiece, TriviaPieceKind, }; /// Type tag for each node or token of a language diff --git a/crates/rome_rowan/src/syntax/node.rs b/crates/rome_rowan/src/syntax/node.rs index 18811f0a204..a25f4094966 100644 --- a/crates/rome_rowan/src/syntax/node.rs +++ b/crates/rome_rowan/src/syntax/node.rs @@ -3,7 +3,7 @@ use crate::syntax::element::{SyntaxElement, SyntaxElementKey}; use crate::syntax::SyntaxTrivia; use crate::{ cursor, Direction, GreenNode, Language, NodeOrToken, SyntaxKind, SyntaxList, SyntaxNodeText, - SyntaxToken, TokenAtOffset, WalkEvent, + SyntaxToken, SyntaxTriviaPiece, TokenAtOffset, WalkEvent, }; use rome_text_size::{TextRange, TextSize}; #[cfg(feature = "serde")] @@ -214,6 +214,8 @@ impl SyntaxNode { /// Returns the trailing trivia of the [last_token](SyntaxNode::last_token), or [None] if the node does not have any descendant tokens. /// + /// ## Examples + /// /// ``` /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind, RawSyntaxTreeBuilder}; /// use rome_rowan::*; @@ -453,6 +455,226 @@ impl SyntaxNode { }) } + /// Return a new version of this node with the leading trivia of its first token replaced with `trivia`. + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn with_leading_trivia_pieces(self, trivia: I) -> Option + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + let first_token = self.first_token()?; + let new_first_token = first_token.with_leading_trivia_pieces(trivia); + self.replace_child(first_token.into(), new_first_token.into()) + } + + /// Return a new version of this node with the trailing trivia of its last token replaced with `trivia`. + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn with_trailing_trivia_pieces(self, trivia: I) -> Option + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + let last_token = self.last_token()?; + let new_last_token = last_token.with_trailing_trivia_pieces(trivia); + self.replace_child(last_token.into(), new_last_token.into()) + } + + // Return a new version of this node with `trivia` prepended to the leading trivia of the first token. + /// + /// ## Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind, RawSyntaxTreeBuilder}; + /// use rome_rowan::*; + /// + /// let mut node = RawSyntaxTreeBuilder::wrap_with_node(RawLanguageKind::ROOT, |builder| { + /// builder.token_with_trivia( + /// RawLanguageKind::LET_TOKEN, + /// "\t let ", + /// &[TriviaPiece::whitespace(2)], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// builder.token(RawLanguageKind::STRING_TOKEN, "a"); + /// builder.token_with_trivia( + /// RawLanguageKind::SEMICOLON_TOKEN, + /// "; ", + /// &[], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// }); + /// + /// let new_node = node.clone().prepend_trivia_pieces(node.last_trailing_trivia().unwrap().pieces()).unwrap(); + /// let leading_trivia = new_node.first_leading_trivia().unwrap(); + /// let trailing_trivia = new_node.last_trailing_trivia().unwrap(); + /// + /// assert_eq!(" \t ", leading_trivia.text()); + /// assert_eq!(" ", trailing_trivia.text()); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn prepend_trivia_pieces(self, trivia: I) -> Option + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + let first_token = self.first_token()?; + let new_first_token = first_token.prepend_trivia_pieces(trivia); + self.replace_child(first_token.into(), new_first_token.into()) + } + + // Return a new version of this node with `trivia` appended to the trailing trivia of the last token. + /// + /// ## Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind, RawSyntaxTreeBuilder}; + /// use rome_rowan::*; + /// + /// let mut node = RawSyntaxTreeBuilder::wrap_with_node(RawLanguageKind::ROOT, |builder| { + /// builder.token_with_trivia( + /// RawLanguageKind::LET_TOKEN, + /// "\t let ", + /// &[TriviaPiece::whitespace(2)], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// builder.token(RawLanguageKind::STRING_TOKEN, "a"); + /// builder.token_with_trivia( + /// RawLanguageKind::SEMICOLON_TOKEN, + /// "; ", + /// &[], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// }); + /// + /// let new_node = node.clone().append_trivia_pieces(node.first_leading_trivia().unwrap().pieces()).unwrap(); + /// let leading_trivia = new_node.first_leading_trivia().unwrap(); + /// let trailing_trivia = new_node.last_trailing_trivia().unwrap(); + /// + /// assert_eq!("\t ", leading_trivia.text()); + /// assert_eq!(" \t ", trailing_trivia.text()); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn append_trivia_pieces(self, trivia: I) -> Option + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + let last_token = self.last_token()?; + let new_last_token = last_token.append_trivia_pieces(trivia); + self.replace_child(last_token.into(), new_last_token.into()) + } + + /// Return a new version of this node without leading and trailing newlines and whitespaces. + /// + /// ## Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind, RawSyntaxTreeBuilder}; + /// use rome_rowan::*; + /// + /// let mut node = RawSyntaxTreeBuilder::wrap_with_node(RawLanguageKind::ROOT, |builder| { + /// builder.token_with_trivia( + /// RawLanguageKind::LET_TOKEN, + /// "\n let ", + /// &[TriviaPiece::newline(1), TriviaPiece::whitespace(1)], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// builder.token(RawLanguageKind::STRING_TOKEN, "a"); + /// builder.token_with_trivia( + /// RawLanguageKind::SEMICOLON_TOKEN, + /// "; ", + /// &[], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// }); + /// + /// let new_node = node.trim_trivia().unwrap(); + /// let leading_trivia = new_node.first_leading_trivia().unwrap(); + /// let trailing_trivia = new_node.last_trailing_trivia().unwrap(); + /// + /// assert_eq!("", leading_trivia.text()); + /// assert_eq!("", trailing_trivia.text()); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn trim_trivia(self) -> Option { + self.trim_leading_trivia()?.trim_trailing_trivia() + } + + /// Return a new version of this node without leading newlines and whitespaces. + /// + /// ## Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind, RawSyntaxTreeBuilder}; + /// use rome_rowan::*; + /// + /// let mut node = RawSyntaxTreeBuilder::wrap_with_node(RawLanguageKind::ROOT, |builder| { + /// builder.token_with_trivia( + /// RawLanguageKind::LET_TOKEN, + /// "\n let ", + /// &[TriviaPiece::newline(1), TriviaPiece::whitespace(1)], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// builder.token(RawLanguageKind::STRING_TOKEN, "a"); + /// builder.token_with_trivia( + /// RawLanguageKind::SEMICOLON_TOKEN, + /// "; ", + /// &[], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// }); + /// + /// let new_node = node.trim_leading_trivia().unwrap(); + /// let leading_trivia = new_node.first_leading_trivia().unwrap(); + /// let trailing_trivia = new_node.last_trailing_trivia().unwrap(); + /// + /// assert_eq!("", leading_trivia.text()); + /// assert_eq!(" ", trailing_trivia.text()); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn trim_leading_trivia(self) -> Option { + let first_token = self.first_token()?; + let new_first_token = first_token.trim_leading_trivia(); + self.replace_child(first_token.into(), new_first_token.into()) + } + + /// Return a new version of this token without trailing whitespaces. + /// + /// ## Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind, RawSyntaxTreeBuilder}; + /// use rome_rowan::*; + /// + /// let mut node = RawSyntaxTreeBuilder::wrap_with_node(RawLanguageKind::ROOT, |builder| { + /// builder.token_with_trivia( + /// RawLanguageKind::LET_TOKEN, + /// "\n let ", + /// &[TriviaPiece::newline(1), TriviaPiece::whitespace(1)], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// builder.token(RawLanguageKind::STRING_TOKEN, "a"); + /// builder.token_with_trivia( + /// RawLanguageKind::SEMICOLON_TOKEN, + /// "; ", + /// &[], + /// &[TriviaPiece::whitespace(1)], + /// ); + /// }); + /// + /// let new_node = node.trim_trailing_trivia().unwrap(); + /// let leading_trivia = new_node.first_leading_trivia().unwrap(); + /// let trailing_trivia = new_node.last_trailing_trivia().unwrap(); + /// + /// assert_eq!("\n ", leading_trivia.text()); + /// assert_eq!("", trailing_trivia.text()); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn trim_trailing_trivia(self) -> Option { + let last_token = self.last_token()?; + let new_last_token = last_token.trim_trailing_trivia(); + self.replace_child(last_token.into(), new_last_token.into()) + } + pub fn into_list(self) -> SyntaxList { SyntaxList::new(self) } diff --git a/crates/rome_rowan/src/syntax/rewriter.rs b/crates/rome_rowan/src/syntax/rewriter.rs index 33731e3889a..eac06e39eca 100644 --- a/crates/rome_rowan/src/syntax/rewriter.rs +++ b/crates/rome_rowan/src/syntax/rewriter.rs @@ -1,7 +1,6 @@ //! A module that exports utilities to rewrite a syntax trees use crate::{Language, SyntaxNode, SyntaxSlot, SyntaxToken}; -use std::iter::once; /// A visitor that re-writes a syntax tree while visiting the nodes. /// @@ -21,7 +20,7 @@ use std::iter::once; /// with a bogus node. /// /// ``` -/// # use std::iter::once; +/// # use std::iter; /// # use rome_rowan::{AstNode, SyntaxNode, SyntaxRewriter, VisitNodeSignal}; /// # use rome_rowan::raw_language::{LiteralExpression, RawLanguage, RawLanguageKind, RawSyntaxTreeBuilder}; /// @@ -51,7 +50,7 @@ use std::iter::once; /// // Use your language's syntax factory instead /// let bogus_node = SyntaxNode::new_detached( /// RawLanguageKind::BOGUS, -/// once(Some(token.into())), +/// iter::once(Some(token.into())), /// ); /// /// VisitNodeSignal::Replace(bogus_node) @@ -172,7 +171,7 @@ where let updated = rewriter.transform(node); if updated.key() != original_key { - parent = parent.splice_slots(index..=index, once(Some(updated.into()))); + parent = parent.splice_slots(index..=index, [Some(updated.into())]); } } SyntaxSlot::Token(token) => { @@ -182,7 +181,7 @@ where let updated = rewriter.visit_token(token); if updated.key() != original_key { - parent = parent.splice_slots(index..=index, once(Some(updated.into()))); + parent = parent.splice_slots(index..=index, [Some(updated.into())]); } } SyntaxSlot::Empty => { diff --git a/crates/rome_rowan/src/syntax/token.rs b/crates/rome_rowan/src/syntax/token.rs index 920cd1ced63..a0bbb083e53 100644 --- a/crates/rome_rowan/src/syntax/token.rs +++ b/crates/rome_rowan/src/syntax/token.rs @@ -3,13 +3,15 @@ use crate::syntax::element::SyntaxElementKey; use crate::syntax::SyntaxTrivia; use crate::syntax_token_text::SyntaxTokenText; use crate::{ - cursor, Direction, Language, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, - SyntaxTriviaPiece, TriviaPiece, TriviaPieceKind, + chain_trivia_pieces, cursor, Direction, Language, NodeOrToken, SyntaxElement, SyntaxKind, + SyntaxNode, SyntaxTriviaPiece, TriviaPiece, TriviaPieceKind, }; use rome_text_size::{TextLen, TextRange, TextSize}; use std::fmt; use std::marker::PhantomData; +use super::trivia::{trim_leading_trivia_pieces, trim_trailing_trivia_pieces}; + #[derive(Clone, PartialEq, Eq, Hash)] pub struct SyntaxToken { raw: cursor::SyntaxToken, @@ -188,18 +190,6 @@ impl SyntaxToken { } } - /// Return whitespace and newlines that juxtapose the token until the first non-whitespace item. - pub fn indentation_trivia(&self) -> impl ExactSizeIterator> { - let leading_trivia = self.leading_trivia().pieces(); - let skip_count = leading_trivia.len() - - leading_trivia - .rev() - .position(|x| !x.is_whitespace()) - .map(|pos| pos + 1) - .unwrap_or(0); - self.leading_trivia().pieces().skip(skip_count) - } - /// Return a new version of this token with its leading trivia replaced with `trivia` #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] pub fn with_leading_trivia<'a, I>(&self, trivia: I) -> Self @@ -322,6 +312,138 @@ impl SyntaxToken { } } + // Return a new version of this token with `trivia` prepended to its leading trivia. + /// + /// # Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind}; + /// use rome_rowan::{RawSyntaxToken, SyntaxToken, TriviaPiece}; + /// + /// let token = SyntaxToken::::new_detached( + /// RawLanguageKind::LET_TOKEN, + /// "/*c*/ let \t", + /// [TriviaPiece::multi_line_comment(5), TriviaPiece::whitespace(1)], + /// [TriviaPiece::whitespace(2)] + /// ); + /// let new_token = token.prepend_trivia_pieces(token.trailing_trivia().pieces()); + /// + /// assert_eq!( + /// format!("{:?}", new_token), + /// "LET_TOKEN@0..13 \"let\" [Whitespace(\" \\t\"), Comments(\"/*c*/\"), Whitespace(\" \")] [Whitespace(\" \\t\")]" + /// ); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn prepend_trivia_pieces(&self, trivia: I) -> Self + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + self.with_leading_trivia_pieces(chain_trivia_pieces( + trivia.into_iter(), + self.leading_trivia().pieces(), + )) + } + + // Return a new version of this token with `trivia` appended to its trailing trivia. + /// + /// # Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind}; + /// use rome_rowan::{RawSyntaxToken, SyntaxToken, TriviaPiece}; + /// + /// let token = SyntaxToken::::new_detached( + /// RawLanguageKind::LET_TOKEN, + /// "\t let /*c*/", + /// [TriviaPiece::whitespace(2)], + /// [TriviaPiece::whitespace(1), TriviaPiece::multi_line_comment(5)], + /// ); + /// let new_token = token.append_trivia_pieces(token.leading_trivia().pieces()); + /// + /// assert_eq!( + /// format!("{:?}", new_token), + /// "LET_TOKEN@0..13 \"let\" [Whitespace(\"\\t \")] [Whitespace(\" \"), Comments(\"/*c*/\"), Whitespace(\"\\t \")]" + /// ); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn append_trivia_pieces(&self, trivia: I) -> Self + where + I: IntoIterator>, + I::IntoIter: ExactSizeIterator, + { + self.with_trailing_trivia_pieces(chain_trivia_pieces( + self.trailing_trivia().pieces(), + trivia.into_iter(), + )) + } + + /// Return a new version of this token without leading newlines and whitespaces. + /// + /// # Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind}; + /// use rome_rowan::{RawSyntaxToken, SyntaxToken, TriviaPiece}; + /// + /// let token = SyntaxToken::::new_detached( + /// RawLanguageKind::LET_TOKEN, + /// "\n\t /*c*/ let \t", + /// [TriviaPiece::newline(1), TriviaPiece::whitespace(2), TriviaPiece::multi_line_comment(5), TriviaPiece::whitespace(1)], + /// [TriviaPiece::whitespace(2)] + /// ); + /// let new_token = token.trim_leading_trivia(); + /// + /// assert_eq!( + /// format!("{:?}", new_token), + /// "LET_TOKEN@0..11 \"let\" [Comments(\"/*c*/\"), Whitespace(\" \")] [Whitespace(\" \\t\")]" + /// ); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn trim_leading_trivia(&self) -> Self { + self.with_leading_trivia_pieces(trim_leading_trivia_pieces(self.leading_trivia().pieces())) + } + + /// Return a new version of this token without trailing whitespaces. + /// + /// # Examples + /// + /// ``` + /// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind}; + /// use rome_rowan::{RawSyntaxToken, SyntaxToken, TriviaPiece}; + /// + /// let token = SyntaxToken::::new_detached( + /// RawLanguageKind::LET_TOKEN, + /// "\n let /*c*/\t ", + /// [TriviaPiece::newline(1), TriviaPiece::whitespace(1)], + /// [TriviaPiece::whitespace(1), TriviaPiece::multi_line_comment(5), TriviaPiece::whitespace(2)] + /// ); + /// let new_token = token.trim_trailing_trivia(); + /// + /// assert_eq!( + /// format!("{:?}", new_token), + /// "LET_TOKEN@0..11 \"let\" [Newline(\"\\n\"), Whitespace(\" \")] [Whitespace(\" \"), Comments(\"/*c*/\")]" + /// ); + /// ``` + #[must_use = "syntax elements are immutable, the result of update methods must be propagated to have any effect"] + pub fn trim_trailing_trivia(&self) -> Self { + self.with_trailing_trivia_pieces(trim_trailing_trivia_pieces( + self.trailing_trivia().pieces(), + )) + } + + /// Return whitespace that juxtapose the token until the first non-whitespace item. + pub fn indentation_trivia_pieces(&self) -> impl ExactSizeIterator> { + let leading_trivia = self.leading_trivia().pieces(); + let skip_count = leading_trivia.len() + - leading_trivia + .rev() + .position(|x| !x.is_whitespace()) + .map(|pos| pos + 1) + .unwrap_or(0); + self.leading_trivia().pieces().skip(skip_count) + } + /// Returns the token's leading trivia. /// /// Looking backward in the text, a token owns all of its preceding trivia up to and including the first newline character. diff --git a/crates/rome_rowan/src/syntax/trivia.rs b/crates/rome_rowan/src/syntax/trivia.rs index 25c5579fcd3..b585c053cf3 100644 --- a/crates/rome_rowan/src/syntax/trivia.rs +++ b/crates/rome_rowan/src/syntax/trivia.rs @@ -674,6 +674,81 @@ impl std::fmt::Debug for SyntaxTrivia { } } +/// Remove leading newlines and whitespaces from `trivia`. +/// +/// ## Examples +/// +/// ``` +/// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind}; +/// use rome_rowan::{trim_leading_trivia_pieces, RawSyntaxToken, SyntaxToken, TriviaPiece}; +/// +/// let token = SyntaxToken::::new_detached( +/// RawLanguageKind::LET_TOKEN, +/// "\n\t /*c*/ let \t", +/// [TriviaPiece::newline(1), TriviaPiece::whitespace(2), TriviaPiece::multi_line_comment(5), TriviaPiece::whitespace(1)], +/// [TriviaPiece::whitespace(2)] +/// ); +/// let new_token = token.with_leading_trivia_pieces( +/// trim_leading_trivia_pieces(token.leading_trivia().pieces()) +/// ); +/// +/// assert_eq!( +/// format!("{:?}", new_token), +/// "LET_TOKEN@0..11 \"let\" [Comments(\"/*c*/\"), Whitespace(\" \")] [Whitespace(\" \\t\")]" +/// ); +/// ``` +pub fn trim_leading_trivia_pieces( + trivia: impl ExactSizeIterator>, +) -> impl ExactSizeIterator> { + let mut trivia = trivia.peekable(); + // We cannot use `skip_while` because `SkipWhile` doesn't implement `ExactSizeIterator`. + // Eager version of `skip_while` which eagerly consume trivia. + while trivia + .next_if(|x| x.is_whitespace() || x.is_newline()) + .is_some() + {} + trivia +} + +/// Remove trailing newlines and whitespaces from `trivia`. +/// +/// ## Examples +/// +/// ``` +/// use rome_rowan::raw_language::{RawLanguage, RawLanguageKind}; +/// use rome_rowan::{trim_trailing_trivia_pieces, RawSyntaxToken, SyntaxToken, TriviaPiece}; +/// +/// let token = SyntaxToken::::new_detached( +/// RawLanguageKind::LET_TOKEN, +/// "\t/*c*/\n\t let ", +/// [TriviaPiece::whitespace(1), TriviaPiece::multi_line_comment(5), TriviaPiece::newline(1), TriviaPiece::whitespace(2)], +/// [TriviaPiece::whitespace(1)], +/// ); +/// let new_token = token.with_leading_trivia_pieces( +/// trim_trailing_trivia_pieces(token.leading_trivia().pieces()) +/// ); +/// +/// assert_eq!( +/// format!("{:?}", new_token), +/// "LET_TOKEN@0..10 \"let\" [Whitespace(\"\\t\"), Comments(\"/*c*/\")] [Whitespace(\" \")]" +/// ); +/// ``` +pub fn trim_trailing_trivia_pieces( + trivia: impl ExactSizeIterator> + DoubleEndedIterator, +) -> impl ExactSizeIterator> { + let mut trivia = trivia.rev().peekable(); + let mut take_count = trivia.len(); + // We cannot use `take_while` because `TakeWhile` doesn't implement `ExactSizeIterator`. + while trivia + .next_if(|x| x.is_whitespace() || x.is_newline()) + .is_some() + { + take_count -= 1; + } + // We have to use `take` to avoid panicking on `ExactSizeIterator under-reported length`. + trivia.rev().take(take_count) +} + /// It creates an iterator by chaining two trivia pieces. This iterator /// of trivia can be attached to a token using `*_pieces` APIs. /// diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index b2f784cc696..90731ccc398 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -1959,6 +1959,10 @@ pub struct Nursery { #[bpaf(long("use-hook-at-top-level"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] pub use_hook_at_top_level: Option, + #[doc = "Use Array.isArray() instead of instanceof Array."] + #[bpaf(long("use-is-array"), argument("on|off|warn"), optional, hide)] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_is_array: Option, #[doc = "Require calls to isNaN() when checking for NaN."] #[bpaf(long("use-is-nan"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] @@ -1992,7 +1996,7 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 33] = [ + pub(crate) const GROUP_RULES: [&'static str; 34] = [ "noAccumulatingSpread", "noAriaUnsupportedElements", "noBannedTypes", @@ -2021,13 +2025,14 @@ impl Nursery { "useGroupedTypeImport", "useHeadingContent", "useHookAtTopLevel", + "useIsArray", "useIsNan", "useLiteralEnumMembers", "useLiteralKeys", "useNamingConvention", "useSimpleNumberKeys", ]; - const RECOMMENDED_RULES: [&'static str; 19] = [ + const RECOMMENDED_RULES: [&'static str; 20] = [ "noAriaUnsupportedElements", "noBannedTypes", "noConstantCondition", @@ -2044,11 +2049,12 @@ impl Nursery { "useArrowFunction", "useExhaustiveDependencies", "useGroupedTypeImport", + "useIsArray", "useIsNan", "useLiteralEnumMembers", "useLiteralKeys", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 19] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 20] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), @@ -2068,8 +2074,9 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 33] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 34] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2103,6 +2110,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { matches!(self.recommended, Some(true)) } @@ -2253,31 +2261,36 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_is_array.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_simple_number_keys.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } + if let Some(rule) = self.use_simple_number_keys.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2422,31 +2435,36 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_is_array.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_simple_number_keys.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } + if let Some(rule) = self.use_simple_number_keys.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -2455,10 +2473,10 @@ impl Nursery { pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 19] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 20] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 33] { Self::ALL_RULES_AS_FILTERS } + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 34] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] pub(crate) fn collect_preset_rules( &self, @@ -2507,6 +2525,7 @@ impl Nursery { "useGroupedTypeImport" => self.use_grouped_type_import.as_ref(), "useHeadingContent" => self.use_heading_content.as_ref(), "useHookAtTopLevel" => self.use_hook_at_top_level.as_ref(), + "useIsArray" => self.use_is_array.as_ref(), "useIsNan" => self.use_is_nan.as_ref(), "useLiteralEnumMembers" => self.use_literal_enum_members.as_ref(), "useLiteralKeys" => self.use_literal_keys.as_ref(), diff --git a/crates/rome_service/src/configuration/parse/json/rules.rs b/crates/rome_service/src/configuration/parse/json/rules.rs index 0f8ec418a59..15f78ddafdb 100644 --- a/crates/rome_service/src/configuration/parse/json/rules.rs +++ b/crates/rome_service/src/configuration/parse/json/rules.rs @@ -1672,6 +1672,7 @@ impl VisitNode for Nursery { "useGroupedTypeImport", "useHeadingContent", "useHookAtTopLevel", + "useIsArray", "useIsNan", "useLiteralEnumMembers", "useLiteralKeys", @@ -2340,6 +2341,29 @@ impl VisitNode for Nursery { )); } }, + "useIsArray" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.use_is_array = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut rule_configuration = RuleConfiguration::default(); + rule_configuration.map_rule_configuration( + &value, + name_text, + "useIsArray", + diagnostics, + )?; + self.use_is_array = Some(rule_configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "useIsNan" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 128464c0519..c9e764d0910 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -962,6 +962,13 @@ { "type": "null" } ] }, + "useIsArray": { + "description": "Use Array.isArray() instead of instanceof Array.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useIsNan": { "description": "Require calls to isNaN() when checking for NaN.", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index 6d9cc28fa52..c988b36d882 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -626,6 +626,10 @@ export interface Nursery { * Enforce that all React hooks are being called from the Top Level component functions. */ useHookAtTopLevel?: RuleConfiguration; + /** + * Use Array.isArray() instead of instanceof Array. + */ + useIsArray?: RuleConfiguration; /** * Require calls to isNaN() when checking for NaN. */ @@ -1169,6 +1173,7 @@ export type Category = | "lint/nursery/useGroupedTypeImport" | "lint/nursery/useHeadingContent" | "lint/nursery/useHookAtTopLevel" + | "lint/nursery/useIsArray" | "lint/nursery/useIsNan" | "lint/nursery/useLiteralEnumMembers" | "lint/nursery/useLiteralKeys" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index 128464c0519..c9e764d0910 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -962,6 +962,13 @@ { "type": "null" } ] }, + "useIsArray": { + "description": "Use Array.isArray() instead of instanceof Array.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useIsNan": { "description": "Require calls to isNaN() when checking for NaN.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index 0e1f859bf5d..8f19d322ea5 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Rome's linter has a total of 152 rules

\ No newline at end of file +

Rome's linter has a total of 153 rules

\ No newline at end of file diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index 30b6d406694..096d9f4b422 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -1067,6 +1067,12 @@ Enforce that all React hooks are being called from the Top Level component functions.

+

+ useIsArray +

+Use Array.isArray() instead of instanceof Array. +
+

useIsNan

diff --git a/website/src/pages/lint/rules/useIsArray.md b/website/src/pages/lint/rules/useIsArray.md new file mode 100644 index 00000000000..6db0466eeeb --- /dev/null +++ b/website/src/pages/lint/rules/useIsArray.md @@ -0,0 +1,58 @@ +--- +title: Lint Rule useIsArray +parent: lint/rules/index +--- + +# useIsArray (since vnext) + +Use `Array.isArray()` instead of `instanceof Array`. + +In _JavaScript_ some array-like objects such as _arguments_ are not instances of the `Array` class. /// +Moreover, the global `Array` class can be different between two execution contexts. +For instance, two frames in a web browser have a distinct `Array` class. +Passing arrays across these contexts, results in arrays that are not instances of the contextual global `Array` class. +To avoid these issues, use `Array.isArray()` instead of `instanceof Array`. +See the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) for more details. + +Source: https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-instanceof-array.md + +## Examples + +### Invalid + +```jsx +const xs = []; +if (xs instanceof Array) {} +``` + +
nursery/useIsArray.js:2:5 lint/nursery/useIsArray  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Use Array.isArray() instead of instanceof Array.
+  
+    1 │ const xs = [];
+  > 2 │ if (xs instanceof Array) {}
+       ^^^^^^^^^^^^^^^^^^^
+    3 │ 
+  
+   instanceof Array returns false for array-like objects and arrays from other execution contexts.
+  
+   Suggested fix: Use Array.isArray() instead.
+  
+    1 1  const xs = [];
+    2  - if·(xs·instanceof·Array)·{}
+      2+ if·(Array.isArray(xs))·{}
+    3 3  
+  
+
+ +## Valid + +```jsx +const xs = []; +if (Array.isArray(xs)) {} +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options) diff --git a/xtask/codegen/src/generate_bindings.rs b/xtask/codegen/src/generate_bindings.rs index 3082d5b09bc..04753679f06 100644 --- a/xtask/codegen/src/generate_bindings.rs +++ b/xtask/codegen/src/generate_bindings.rs @@ -39,7 +39,7 @@ pub(crate) fn generate_workspace_bindings(mode: Mode) -> Result<()> { Some(AnyJsParameter::AnyJsFormalParameter( AnyJsFormalParameter::JsFormalParameter( make::js_formal_parameter( - make::js_decorator_list(std::iter::empty()), + make::js_decorator_list([]), AnyJsBindingPattern::AnyJsBinding( AnyJsBinding::JsIdentifierBinding( make::js_identifier_binding(make::ident("params")), @@ -85,7 +85,7 @@ pub(crate) fn generate_workspace_bindings(mode: Mode) -> Result<()> { make::js_parameter_list( Some(AnyJsParameter::AnyJsFormalParameter( AnyJsFormalParameter::JsFormalParameter( - make::js_formal_parameter(make::js_decorator_list(std::iter::empty()),AnyJsBindingPattern::AnyJsBinding( + make::js_formal_parameter(make::js_decorator_list([]),AnyJsBindingPattern::AnyJsBinding( AnyJsBinding::JsIdentifierBinding(make::js_identifier_binding( make::ident("params"), )), @@ -204,7 +204,7 @@ pub(crate) fn generate_workspace_bindings(mode: Mode) -> Result<()> { export = export.with_leading_trivia(trivia); } AnyJsModuleItem::JsExport(make::js_export( - make::js_decorator_list(std::iter::empty()), + make::js_decorator_list([]), export, AnyJsExportClause::AnyJsDeclarationClause(match decl { AnyJsDeclaration::JsClassDeclaration(decl) => { @@ -310,7 +310,7 @@ pub(crate) fn generate_workspace_bindings(mode: Mode) -> Result<()> { )); items.push(AnyJsModuleItem::JsExport(make::js_export( - make::js_decorator_list(std::iter::empty()), + make::js_decorator_list([]), make::token(T![export]), AnyJsExportClause::AnyJsDeclarationClause(AnyJsDeclarationClause::TsInterfaceDeclaration( make::ts_interface_declaration( @@ -327,7 +327,7 @@ pub(crate) fn generate_workspace_bindings(mode: Mode) -> Result<()> { let member_separators = (0..member_declarations.len()).map(|_| make::token(T![,])); items.push(AnyJsModuleItem::JsExport(make::js_export( - make::js_decorator_list(std::iter::empty()), + make::js_decorator_list([]), make::token(T![export]), AnyJsExportClause::AnyJsDeclarationClause(AnyJsDeclarationClause::JsFunctionDeclaration( make::js_function_declaration( @@ -341,7 +341,7 @@ pub(crate) fn generate_workspace_bindings(mode: Mode) -> Result<()> { Some(AnyJsParameter::AnyJsFormalParameter( AnyJsFormalParameter::JsFormalParameter( make::js_formal_parameter( - make::js_decorator_list(std::iter::empty()), + make::js_decorator_list([]), AnyJsBindingPattern::AnyJsBinding( AnyJsBinding::JsIdentifierBinding( make::js_identifier_binding(make::ident("transport")), diff --git a/xtask/codegen/src/parser_tests.rs b/xtask/codegen/src/parser_tests.rs index 1f81afa0de4..b1ac6910654 100644 --- a/xtask/codegen/src/parser_tests.rs +++ b/xtask/codegen/src/parser_tests.rs @@ -3,7 +3,7 @@ use std::{ collections::HashMap, - fs, iter, mem, + fs, mem, path::{Path, PathBuf}, }; @@ -181,7 +181,7 @@ fn collect_tests(s: &str) -> Vec { let text: String = comment_block[1..] .iter() .cloned() - .chain(iter::once(String::new())) + .chain([String::new()]) .collect::>() .join("\n");