diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 7705ba5bf87d2..8dce4e8f94884 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -3068,6 +3068,22 @@ pub enum ImportDeclarationSpecifier<'a> { ImportNamespaceSpecifier(Box<'a, ImportNamespaceSpecifier<'a>>), } +impl<'a> ImportDeclarationSpecifier<'a> { + pub fn name(&self) -> CompactStr { + match self { + ImportDeclarationSpecifier::ImportSpecifier(specifier) => { + specifier.local.name.to_compact_str() + } + ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => { + specifier.local.name.to_compact_str() + } + ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => { + specifier.local.name.to_compact_str() + } + } + } +} + // import {imported} from "source" // import {imported as local} from "source" #[visited_node] diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index db30b32abc919..4cc62b09de88a 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -111,6 +111,7 @@ mod eslint { pub mod radix; pub mod require_await; pub mod require_yield; + pub mod sort_imports; pub mod symbol_description; pub mod unicode_bom; pub mod use_isnan; @@ -490,6 +491,7 @@ oxc_macros::declare_all_lint_rules! { eslint::radix, eslint::require_yield, eslint::symbol_description, + eslint::sort_imports, eslint::unicode_bom, eslint::use_isnan, eslint::valid_typeof, diff --git a/crates/oxc_linter/src/rules/eslint/sort_imports.rs b/crates/oxc_linter/src/rules/eslint/sort_imports.rs new file mode 100644 index 0000000000000..fea6c86bc3fe1 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/sort_imports.rs @@ -0,0 +1,830 @@ +use std::{ + fmt::{Display, Write}, + str::FromStr, +}; + +use itertools::Itertools; +use oxc_ast::{ + ast::{ImportDeclaration, ImportDeclarationSpecifier, Statement}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{CompactStr, Span}; + +use crate::{context::LintContext, fixer::Fix, rule::Rule}; + +fn unexpected_syntax_order_diagnostic( + x0: &ImportKind, + x1: &ImportKind, + span2: Span, +) -> OxcDiagnostic { + OxcDiagnostic::warn(format!( + "eslint(sort-imports): Expected '{x0}' syntax before '{x1}' syntax." + )) + .with_labels([span2.into()]) +} + +fn sort_imports_alphabetically_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("eslint(sort-imports): Imports should be sorted alphabetically.") + .with_labels([span0.into()]) +} + +fn sort_members_alphabetically_diagnostic(x0: &str, span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("eslint(sort-imports): Member '{x0}' of the import declaration should be sorted alphabetically.")) + .with_labels([span0.into()]) +} + +#[derive(Debug, Default, Clone)] +pub struct SortImports(Box); + +#[derive(Debug, Default, Clone)] +pub struct SortImportsOptions { + ignore_case: bool, + ignore_declaration_sort: bool, + ignore_member_sort: bool, + allow_separated_groups: bool, + member_syntax_sort_order: MemberSyntaxSortOrder, +} + +impl std::ops::Deref for SortImports { + type Target = SortImportsOptions; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule checks all import declarations and verifies that all imports are first sorted + /// by the used member syntax and then alphabetically by the first member or alias name. + /// + /// When declaring multiple imports, a sorted list of import declarations make it easier for developers to read + /// the code and find necessary imports later. + /// + /// ### Why is this bad? + /// + /// ### Example + /// ```javascript + /// import {b, a, c} from 'foo.js' + /// + /// import d from 'foo.js'; + /// import e from 'bar.js'; + /// ``` + SortImports, + style +); + +impl Rule for SortImports { + fn from_configuration(value: serde_json::Value) -> Self { + let Some(config) = value.get(0) else { + return Self(Box::default()); + }; + + let ignore_case = + config.get("ignoreCase").and_then(serde_json::Value::as_bool).unwrap_or_default(); + let ignore_member_sort = + config.get("ignoreMemberSort").and_then(serde_json::Value::as_bool).unwrap_or_default(); + let ignore_declaration_sort = config + .get("ignoreDeclarationSort") + .and_then(serde_json::Value::as_bool) + .unwrap_or_default(); + let allow_separated_groups = config + .get("allowSeparatedGroups") + .and_then(serde_json::Value::as_bool) + .unwrap_or_default(); + + let member_syntax_sort_order = config + .get("memberSyntaxSortOrder") + .and_then(|v| v.as_array()) + .map(|arr| { + // memberSyntaxSortOrder in config file must have 4 items + if arr.len() != 4 { + return MemberSyntaxSortOrder::default(); + } + + let kinds: Vec = arr + .iter() + .filter_map(|v| v.as_str()) + .map(ImportKind::from_str) + .filter_map(Result::ok) + .unique() + .collect(); + + // 4 items must all unique and valid. + if kinds.len() != 4 { + return MemberSyntaxSortOrder::default(); + } + + MemberSyntaxSortOrder(kinds) + }) + .unwrap_or_default(); + + Self(Box::new(SortImportsOptions { + ignore_case, + ignore_declaration_sort, + ignore_member_sort, + allow_separated_groups, + member_syntax_sort_order, + })) + } + fn run_once(&self, ctx: &LintContext) { + let Some(root) = ctx.nodes().root_node() else { + return; + }; + let AstKind::Program(program) = root.kind() else { unreachable!() }; + + let mut import_declarations = vec![]; + + for statement in &program.body { + if let Statement::ImportDeclaration(decl) = statement { + import_declarations.push(decl); + } else { + break; + } + } + + let mut previous: Option<&ImportDeclaration> = None; + + for current in import_declarations { + if !self.ignore_declaration_sort { + // ```js + // import b from 'foo.js' + // + // import a from 'foo.js' + // ``` + // when visit line 3, line 2 will make `previous` to be `None` + if self.allow_separated_groups + && previous.is_some_and(|previous| { + get_number_of_lines_between(previous.span, current.span, ctx) > 0 + }) + { + previous = None; + } + + if let Some(previous) = previous { + self.check_syntax_order_and_member_order(previous, current, ctx); + } + + previous = Some(current); + } + + if !self.ignore_member_sort { + self.check_member_sort_with_fix(current, ctx); + } + } + } +} + +impl SortImports { + // Check between two import declarations. + // + // 1. syntax order + // ```js + // import a from 'foo.js' // single + // import from 'foo.js' // none + // ``` + // + // 2. member order. If the syntax order is the same, check the member order. + // + // ```js + // import { b } from 'foo.js' + // import { c } from 'foo.js' + // ``` + fn check_syntax_order_and_member_order( + &self, + previous: &ImportDeclaration, + current: &ImportDeclaration, + ctx: &LintContext, + ) { + let current_member_syntax_group_index = + self.member_syntax_sort_order.get_group_index_by_import_decl(current); + let previous_member_syntax_group_index = + self.member_syntax_sort_order.get_group_index_by_import_decl(previous); + + let mut current_local_member_name = get_first_local_member_name(current); + let mut previous_local_member_name = get_first_local_member_name(previous); + + if self.ignore_case { + current_local_member_name = + current_local_member_name.map(|name| name.to_lowercase()).map(CompactStr::from); + previous_local_member_name = + previous_local_member_name.map(|name| name.to_lowercase()).map(CompactStr::from); + } + + // "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] + // ```js + // import a from 'foo.js' + // import from 'foo.js' // <-- incorrect, 'none' should come before 'single' + // ``` + match current_member_syntax_group_index.cmp(&previous_member_syntax_group_index) { + std::cmp::Ordering::Less => { + let current_kind = + self.member_syntax_sort_order.get(current_member_syntax_group_index); + let previous_kind = + self.member_syntax_sort_order.get(previous_member_syntax_group_index); + if let Some((current_kind, previous_kind)) = current_kind.zip(previous_kind) { + ctx.diagnostic(unexpected_syntax_order_diagnostic( + current_kind, + previous_kind, + current.span, + )); + } + } + std::cmp::Ordering::Equal => { + // ```js + // import { b } from 'foo.js' + // import { a } from 'foo.js' // <-- incorrect, 'a' should come before 'b' + // ``` + if let Some((current_name, previous_name)) = + current_local_member_name.zip(previous_local_member_name) + { + let current_name = current_name.as_str(); + let previous_name = previous_name.as_str(); + if current_name < previous_name { + ctx.diagnostic(sort_imports_alphabetically_diagnostic(current.span)); + } + } + } + std::cmp::Ordering::Greater => {} + } + } + // Check member sort in a import declaration + // ```js + // import { b, a } from 'foo.js' + // ``` + fn check_member_sort_with_fix(&self, current: &ImportDeclaration, ctx: &LintContext) { + let Some(specifiers) = ¤t.specifiers else { + return; + }; + + let specifiers: Vec<_> = specifiers + .iter() + .filter_map(|specifier| { + if let ImportDeclarationSpecifier::ImportSpecifier(ref specifier) = specifier { + Some(specifier) + } else { + None + } + }) + .collect(); + + if specifiers.len() < 2 { + return; + } + + let unsorted = specifiers + .windows(2) + .find(|window| { + let a = window[0].local.name.as_str(); + let b = window[1].local.name.as_str(); + + if self.ignore_case { + a.to_lowercase() > b.to_lowercase() + } else { + a > b + } + }) + .map(|window| (window[1].local.name.as_str(), window[1].span)); + + let Some((unsorted_name, unsorted_span)) = unsorted else { + return; + }; + + // ESLint check if there are comments in the ImportSpecifier list, and don't rearrange the specifiers. + // We don't have a direct way to check if there are comments in the specifiers(may need lookahead/lookbehind source text). + // ```js + // // this is not fixable in ESLint + // import { /* comment */ a, b, c, d } from 'foo.js' + // ``` + // I use ImportStatement's span to check if there are comments between the specifiers. + let is_fixable = !ctx.semantic().trivias().has_comments_between(current.span); + + if is_fixable { + // Safe to index because we know that `specifiers` is at least 2 element long + let specifiers_span = specifiers[0].span.merge(&specifiers[specifiers.len() - 1].span); + ctx.diagnostic_with_fix( + sort_members_alphabetically_diagnostic(unsorted_name, unsorted_span), + || { + // import { a, b, c, d } from 'foo.js' + // ^ ^^^^^^ ^ + let mut paddings: Vec<&str> = specifiers + .windows(2) + .map(|window| { + let a = window[0].span; + let b = window[1].span; + + let padding = Span::new(a.end, b.start); + ctx.source_range(padding) + }) + .collect(); + + // add a empty string for zip with specifiers + paddings.push(""); + + let specifiers = specifiers.iter().sorted_by(|a, b| { + let a = a.local.name.as_str(); + let b = b.local.name.as_str(); + + if self.ignore_case { + a.to_lowercase().cmp(&b.to_lowercase()) + } else { + a.cmp(b) + } + }); + + let sorted_text = specifiers.zip(paddings).fold( + String::new(), + |mut acc, (specifier, padding)| { + let _ = acc.write_str(ctx.source_range(specifier.span)); + let _ = acc.write_str(padding); + acc + }, + ); + + Fix::new(sorted_text, specifiers_span) + }, + ); + } else { + ctx.diagnostic(sort_members_alphabetically_diagnostic(unsorted_name, unsorted_span)); + } + } +} + +#[derive(Debug, Clone)] +struct MemberSyntaxSortOrder(Vec); + +impl Default for MemberSyntaxSortOrder { + fn default() -> Self { + MemberSyntaxSortOrder(vec![ + ImportKind::None, + ImportKind::All, + ImportKind::Multiple, + ImportKind::Single, + ]) + } +} + +impl std::ops::Deref for MemberSyntaxSortOrder { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl MemberSyntaxSortOrder { + fn get_group_index_by_import_decl(&self, decl: &ImportDeclaration) -> usize { + // import "foo.js" -> ImportKind::None + // import * as foo from "foo.js" -> ImportKind::All + // import { a, b } from "foo.js" -> ImportKind::Multiple + // import a from "foo.js" -> ImportKind::Single + // import { a } from 'foo.js' -> ImportKind::Single + let import_kind = match &decl.specifiers { + Some(specifiers) => { + if specifiers.is_empty() { + ImportKind::None + } else if specifiers.len() == 1 { + if matches!( + specifiers[0], + ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) + ) { + ImportKind::All + } else { + ImportKind::Single + } + } else { + ImportKind::Multiple + } + } + None => ImportKind::None, + }; + + self.iter().position(|kind| kind == &import_kind).unwrap_or_default() + } +} + +#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)] +enum ImportKind { + // import from 'foo.js' + #[default] + None, + // import * from 'foo.js' + All, + // import { a, b } from 'foo.js' + Multiple, + // import a from 'foo.js' + Single, +} + +impl FromStr for ImportKind { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "none" => Ok(ImportKind::None), + "all" => Ok(ImportKind::All), + "multiple" => Ok(ImportKind::Multiple), + "single" => Ok(ImportKind::Single), + _ => Err("Invalid import kind"), + } + } +} + +impl Display for ImportKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ImportKind::None => write!(f, "None"), + ImportKind::All => write!(f, "All"), + ImportKind::Multiple => write!(f, "Multiple"), + ImportKind::Single => write!(f, "Single"), + } + } +} + +fn get_first_local_member_name(decl: &ImportDeclaration) -> Option { + let specifiers = decl.specifiers.as_ref()?; + specifiers.first().map(ImportDeclarationSpecifier::name) +} + +// Calculates number of lines between two nodes. It is assumed that the given `left` span appears before +// the given `right` span in the source code. Lines are counted from the end of the `left` span till the +// start of the `right` span. If the given span are on the same line, or `right` span is appears before `left` span, +// it returns `0`. +fn get_number_of_lines_between(left: Span, right: Span, ctx: &LintContext) -> usize { + if left.end >= right.start { + return 0; + } + let between_span = Span::new(left.end, right.start); + let count = ctx.source_range(between_span).lines().count(); + + // In same line + if count < 2 { + return 0; + } + + // In different lines, need to subtract 2 because the count includes the first and last line. + count - 2 +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + "import a from 'foo.js'; + import b from 'bar.js'; + import c from 'baz.js'; + ", + None, + ), + ( + "import * as B from 'foo.js'; + import A from 'bar.js';", + None, + ), + ( + "import * as B from 'foo.js'; + import {a, b} from 'bar.js';", + None, + ), + ( + "import {b, c} from 'bar.js'; + import A from 'foo.js';", + None, + ), + ( + "import A from 'bar.js'; + import {b, c} from 'foo.js';", + Some( + serde_json::json!([{ "memberSyntaxSortOrder": ["single", "multiple", "none", "all"] }]), + ), + ), + ( + "import {a, b} from 'bar.js'; + import {c, d} from 'foo.js';", + None, + ), + ( + "import A from 'foo.js'; + import B from 'bar.js';", + None, + ), + ( + "import A from 'foo.js'; + import a from 'bar.js';", + None, + ), + ( + "import a, * as b from 'foo.js'; + import c from 'bar.js';", + None, + ), + ( + "import 'foo.js'; + import a from 'bar.js';", + None, + ), + ( + "import B from 'foo.js'; + import a from 'bar.js';", + None, + ), + ( + "import a from 'foo.js'; + import B from 'bar.js';", + Some(serde_json::json!([{ "ignoreCase": true }])), + ), + ("import {a, b, c, d} from 'foo.js';", None), + ( + "import a from 'foo.js'; + import B from 'bar.js';", + Some(serde_json::json!([{ "ignoreDeclarationSort": true }])), + ), + ( + "import {b, A, C, d} from 'foo.js';", + Some(serde_json::json!([{ "ignoreMemberSort": true }])), + ), + ( + "import {B, a, C, d} from 'foo.js';", + Some(serde_json::json!([{ "ignoreMemberSort": true }])), + ), + ("import {a, B, c, D} from 'foo.js';", Some(serde_json::json!([{ "ignoreCase": true }]))), + ("import a, * as b from 'foo.js';", None), + ( + "import * as a from 'foo.js'; + + import b from 'bar.js';", + None, + ), + ( + "import * as bar from 'bar.js'; + import * as foo from 'foo.js';", + None, + ), + ( + "import 'foo'; + import bar from 'bar';", + Some(serde_json::json!([{ "ignoreCase": true }])), + ), + ("import React, {Component} from 'react';", None), + ( + "import b from 'b'; + + import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import a from 'a'; + + import 'b';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import { b } from 'b'; + + + import { a } from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import b from 'b'; + // comment + import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import b from 'b'; + foo(); + import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import { b } from 'b';/* + comment + */import { a } from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import b from + 'b'; + + import + a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import c from 'c'; + + import a from 'a'; + import b from 'b';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import c from 'c'; + + import b from 'b'; + + import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ]; + + let fail = vec![ + ( + "import a from 'foo.js'; + import A from 'bar.js';", + None, + ), + ( + "import b from 'foo.js'; + import a from 'bar.js';", + None, + ), + ( + "import {b, c} from 'foo.js'; + import {a, d} from 'bar.js';", + None, + ), + ( + "import * as foo from 'foo.js'; + import * as bar from 'bar.js';", + None, + ), + ( + "import a from 'foo.js'; + import {b, c} from 'bar.js';", + None, + ), + ( + "import a from 'foo.js'; + import * as b from 'bar.js';", + None, + ), + ( + "import a from 'foo.js'; + import 'bar.js';", + None, + ), + ( + "import b from 'bar.js'; + import * as a from 'foo.js';", + Some( + serde_json::json!([{ "memberSyntaxSortOrder": ["all", "single", "multiple", "none"] }]), + ), + ), + ("import {b, a, d, c} from 'foo.js';", None), + ( + "import {b, a, d, c} from 'foo.js'; + import {e, f, g, h} from 'bar.js';", + Some(serde_json::json!([{ "ignoreDeclarationSort": true }])), + ), + ("import {a, B, c, D} from 'foo.js';", None), + ("import {zzzzz, /* comment */ aaaaa} from 'foo.js';", None), + ("import {zzzzz /* comment */, aaaaa} from 'foo.js';", None), + ("import {/* comment */ zzzzz, aaaaa} from 'foo.js';", None), + ("import {zzzzz, aaaaa /* comment */} from 'foo.js';", None), + ( + " + import { + boop, + foo, + zoo, + baz as qux, + bar, + beep + } from 'foo.js'; + ", + None, + ), + ( + "import b from 'b'; + import a from 'a';", + None, + ), + ( + "import b from 'b'; + import a from 'a';", + Some(serde_json::json!([{}])), + ), + ( + "import b from 'b'; + import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import b from 'b';import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import b from 'b'; /* comment */ import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import b from 'b'; // comment + import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import b from 'b'; // comment 1 + /* comment 2 */import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import { b } from 'b'; /* comment line 1 + comment line 2 */ import { a } from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import b + from 'b'; import a + from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import { b } from + 'b'; /* comment */ import + { a } from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import { b } from + 'b'; + import + { a } from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": false }])), + ), + ( + "import c from 'c'; + + import b from 'b'; + import a from 'a';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ( + "import b from 'b'; + + import { c, a } from 'c';", + Some(serde_json::json!([{ "allowSeparatedGroups": true }])), + ), + ]; + + let fix = vec![ + ("import {b, a, d, c} from 'foo.js';", "import {a, b, c, d} from 'foo.js';", None), + ("import {a, B, c, D} from 'foo.js';", "import {B, D, a, c} from 'foo.js';", None), + ( + " import { + boop, + foo, + zoo, + baz as qux, + bar, + beep + } from 'foo.js';", + " import { + bar, + beep, + boop, + foo, + baz as qux, + zoo + } from 'foo.js';", + None, + ), + ( + " + import b from 'b'; + + import { c, a } from 'c';", + " + import b from 'b'; + + import { a, c } from 'c';", + None, + ), + // Not fixed due to comment + ( + "import {zzzzz, /* comment */ aaaaa} from 'foo.js';", + "import {zzzzz, /* comment */ aaaaa} from 'foo.js';", + None, + ), + ( + "import {zzzzz /* comment */, aaaaa} from 'foo.js';", + "import {zzzzz /* comment */, aaaaa} from 'foo.js';", + None, + ), + ( + "import {/* comment */ zzzzz, aaaaa} from 'foo.js';", + "import {/* comment */ zzzzz, aaaaa} from 'foo.js';", + None, + ), + ( + "import {zzzzz, aaaaa /* comment */} from 'foo.js';", + "import {zzzzz, aaaaa /* comment */} from 'foo.js';", + None, + ), + ]; + Tester::new(SortImports::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/sort_imports.snap b/crates/oxc_linter/src/snapshots/sort_imports.snap new file mode 100644 index 0000000000000..370423c127d3f --- /dev/null +++ b/crates/oxc_linter/src/snapshots/sort_imports.snap @@ -0,0 +1,199 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: sort_imports +--- + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:13] + 1 │ import a from 'foo.js'; + 2 │ import A from 'bar.js'; + · ─────────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:13] + 1 │ import b from 'foo.js'; + 2 │ import a from 'bar.js'; + · ─────────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:13] + 1 │ import {b, c} from 'foo.js'; + 2 │ import {a, d} from 'bar.js'; + · ──────────────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:13] + 1 │ import * as foo from 'foo.js'; + 2 │ import * as bar from 'bar.js'; + · ────────────────────────────── + ╰──── + + ⚠ eslint(sort-imports): Expected 'Multiple' syntax before 'Single' syntax. + ╭─[sort_imports.tsx:2:13] + 1 │ import a from 'foo.js'; + 2 │ import {b, c} from 'bar.js'; + · ──────────────────────────── + ╰──── + + ⚠ eslint(sort-imports): Expected 'All' syntax before 'Single' syntax. + ╭─[sort_imports.tsx:2:13] + 1 │ import a from 'foo.js'; + 2 │ import * as b from 'bar.js'; + · ──────────────────────────── + ╰──── + + ⚠ eslint(sort-imports): Expected 'None' syntax before 'Single' syntax. + ╭─[sort_imports.tsx:2:13] + 1 │ import a from 'foo.js'; + 2 │ import 'bar.js'; + · ──────────────── + ╰──── + + ⚠ eslint(sort-imports): Expected 'All' syntax before 'Single' syntax. + ╭─[sort_imports.tsx:2:13] + 1 │ import b from 'bar.js'; + 2 │ import * as a from 'foo.js'; + · ──────────────────────────── + ╰──── + + ⚠ eslint(sort-imports): Member 'a' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:1:12] + 1 │ import {b, a, d, c} from 'foo.js'; + · ─ + ╰──── + + ⚠ eslint(sort-imports): Member 'a' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:1:12] + 1 │ import {b, a, d, c} from 'foo.js'; + · ─ + 2 │ import {e, f, g, h} from 'bar.js'; + ╰──── + + ⚠ eslint(sort-imports): Member 'B' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:1:12] + 1 │ import {a, B, c, D} from 'foo.js'; + · ─ + ╰──── + + ⚠ eslint(sort-imports): Member 'aaaaa' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:1:30] + 1 │ import {zzzzz, /* comment */ aaaaa} from 'foo.js'; + · ───── + ╰──── + + ⚠ eslint(sort-imports): Member 'aaaaa' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:1:30] + 1 │ import {zzzzz /* comment */, aaaaa} from 'foo.js'; + · ───── + ╰──── + + ⚠ eslint(sort-imports): Member 'aaaaa' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:1:30] + 1 │ import {/* comment */ zzzzz, aaaaa} from 'foo.js'; + · ───── + ╰──── + + ⚠ eslint(sort-imports): Member 'aaaaa' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:1:16] + 1 │ import {zzzzz, aaaaa /* comment */} from 'foo.js'; + · ───── + ╰──── + + ⚠ eslint(sort-imports): Member 'qux' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:6:17] + 5 │ zoo, + 6 │ baz as qux, + · ────────── + 7 │ bar, + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:13] + 1 │ import b from 'b'; + 2 │ import a from 'a'; + · ────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:13] + 1 │ import b from 'b'; + 2 │ import a from 'a'; + · ────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:13] + 1 │ import b from 'b'; + 2 │ import a from 'a'; + · ────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:1:19] + 1 │ import b from 'b';import a from 'a'; + · ────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:1:34] + 1 │ import b from 'b'; /* comment */ import a from 'a'; + · ────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:13] + 1 │ import b from 'b'; // comment + 2 │ import a from 'a'; + · ────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:28] + 1 │ import b from 'b'; // comment 1 + 2 │ /* comment 2 */import a from 'a'; + · ────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:35] + 1 │ import { b } from 'b'; /* comment line 1 + 2 │ comment line 2 */ import { a } from 'a'; + · ────────────────────── + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:23] + 1 │ import b + 2 │ ╭─▶ from 'b'; import a + 3 │ ╰─▶ from 'a'; + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:2:32] + 1 │ import { b } from + 2 │ ╭─▶ 'b'; /* comment */ import + 3 │ ╰─▶ { a } from 'a'; + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:3:13] + 2 │ 'b'; + 3 │ ╭─▶ import + 4 │ ╰─▶ { a } from 'a'; + ╰──── + + ⚠ eslint(sort-imports): Imports should be sorted alphabetically. + ╭─[sort_imports.tsx:4:13] + 3 │ import b from 'b'; + 4 │ import a from 'a'; + · ────────────────── + ╰──── + + ⚠ eslint(sort-imports): Member 'a' of the import declaration should be sorted alphabetically. + ╭─[sort_imports.tsx:3:25] + 2 │ + 3 │ import { c, a } from 'c'; + · ─ + ╰────