Skip to content

Commit 95d1b0b

Browse files
committed
mason: preserve ES import attribute clauses during regeneration
Model verbatim with/assert clauses in parsing, deduplication, and every ES import rewrite path. Biome preserves the attribute while reordering the same fixture, so stripping it is not an accepted organizer tradeoff like evaluation ordering.
1 parent d0d5156 commit 95d1b0b

7 files changed

Lines changed: 371 additions & 31 deletions

File tree

crates/aft/src/commands/add_import.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -332,11 +332,15 @@ pub fn handle_add_import(req: &RawRequest, ctx: &AppContext) -> Response {
332332
LangId::TypeScript | LangId::Tsx | LangId::JavaScript | LangId::Python | LangId::Rust
333333
) {
334334
block.imports.iter().find(|imp| {
335+
let es_import = matches!(
336+
lang,
337+
LangId::TypeScript | LangId::Tsx | LangId::JavaScript | LangId::Vue
338+
);
335339
imp.module_path == module
336340
&& imp.kind == target_kind
337341
&& imp.namespace_import.is_none()
338-
&& imp.default_import.is_none()
339-
&& !imp.names.is_empty()
342+
&& (es_import || imp.default_import.is_none())
343+
&& (es_import || !imp.names.is_empty())
340344
&& (lang != LangId::Python
341345
|| matches!(
342346
imp.form,
@@ -374,12 +378,14 @@ pub fn handle_add_import(req: &RawRequest, ctx: &AppContext) -> Response {
374378
// Build the merged named-import list: union of existing + new, sorted.
375379
let merged_names = merge_named_import_specifiers(&existing.names, &names);
376380

377-
let merged_line = imports::generate_import_line(
381+
let merged_line = imports::generate_import_line_with_namespace_and_attribute_clause(
378382
lang,
379383
&existing.module_path,
380384
&merged_names,
381-
None,
385+
existing.default_import.as_deref(),
386+
existing.namespace_import.as_deref(),
382387
type_only,
388+
imports::es_import_attribute_clause(existing),
383389
);
384390
(
385391
existing.byte_range.start,
@@ -388,13 +394,14 @@ pub fn handle_add_import(req: &RawRequest, ctx: &AppContext) -> Response {
388394
true,
389395
)
390396
} else if let Some(existing) = namespace_merge_target {
391-
let merged_line = imports::generate_import_line_with_namespace(
397+
let merged_line = imports::generate_import_line_with_namespace_and_attribute_clause(
392398
lang,
393399
&existing.module_path,
394400
&existing.names,
395401
existing.default_import.as_deref(),
396402
namespace.as_deref(),
397403
type_only,
404+
imports::es_import_attribute_clause(existing),
398405
);
399406
(
400407
existing.byte_range.start,
@@ -462,6 +469,30 @@ pub fn handle_add_import(req: &RawRequest, ctx: &AppContext) -> Response {
462469
let import_line = if matches!(lang, LangId::Go) {
463470
let in_group = imports::go_offset_is_in_grouped_import(&source, &tree, insert_offset);
464471
imports::generate_go_import_line_pub(module, default_import.as_deref(), in_group)
472+
} else if matches!(
473+
lang,
474+
LangId::TypeScript | LangId::Tsx | LangId::JavaScript | LangId::Vue
475+
) {
476+
// Import attributes select the module type. A new sibling import of
477+
// an already-attributed module must carry the same clause or runtimes
478+
// such as Node reject that sibling before loading the module.
479+
let mut clauses = block
480+
.imports
481+
.iter()
482+
.filter(|imp| imp.module_path == module)
483+
.filter_map(imports::es_import_attribute_clause);
484+
let inherited_clause = clauses
485+
.next()
486+
.filter(|first| clauses.all(|item| item == *first));
487+
imports::generate_import_line_with_namespace_and_attribute_clause(
488+
lang,
489+
module,
490+
&names,
491+
default_import.as_deref(),
492+
namespace.as_deref(),
493+
type_only,
494+
inherited_clause,
495+
)
465496
} else {
466497
imports::generate_import(lang, &import_request)
467498
};

crates/aft/src/commands/move_symbol.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,22 +1202,29 @@ fn rewrite_consumer_imports(
12021202
};
12031203

12041204
let type_only = imp.kind == imports::ImportKind::Type;
1205-
let moved_import =
1206-
generate_import_for_bindings(lang, &new_import_path, &moved_bindings, type_only);
1205+
let attribute_clause = imports::es_import_attribute_clause(imp);
1206+
let moved_import = generate_import_for_bindings(
1207+
lang,
1208+
&new_import_path,
1209+
&moved_bindings,
1210+
type_only,
1211+
attribute_clause,
1212+
);
12071213

12081214
if remaining_names.is_empty()
12091215
&& remaining_default.is_none()
12101216
&& remaining_namespace.is_none()
12111217
{
12121218
edits.push((imp.byte_range.clone(), moved_import));
12131219
} else {
1214-
let kept_import = imports::generate_import_line_with_namespace(
1220+
let kept_import = imports::generate_import_line_with_namespace_and_attribute_clause(
12151221
lang,
12161222
&imp.module_path,
12171223
&remaining_names,
12181224
remaining_default.as_deref(),
12191225
remaining_namespace.as_deref(),
12201226
type_only,
1227+
attribute_clause,
12211228
);
12221229
edits.push((
12231230
imp.byte_range.clone(),
@@ -1734,14 +1741,16 @@ fn generate_import_for_bindings(
17341741
module_path: &str,
17351742
bindings: &MovedImportBindings,
17361743
type_only: bool,
1744+
attribute_clause: Option<&str>,
17371745
) -> String {
1738-
imports::generate_import_line_with_namespace(
1746+
imports::generate_import_line_with_namespace_and_attribute_clause(
17391747
lang,
17401748
module_path,
17411749
&bindings.named,
17421750
bindings.default_import.as_deref(),
17431751
bindings.namespace_import.as_deref(),
17441752
type_only,
1753+
attribute_clause,
17451754
)
17461755
}
17471756

@@ -1783,10 +1792,11 @@ fn build_add_moved_import_edit(
17831792
// imported. Preserve the existing module spelling (for example `./dest.js`).
17841793
if !moved_symbol_is_default {
17851794
if let Some(existing) = block.imports.iter().find(|import| {
1795+
let es_import = matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript);
17861796
import.kind == imports::ImportKind::Value
17871797
&& import.namespace_import.is_none()
1788-
&& import.default_import.is_none()
1789-
&& !import.names.is_empty()
1798+
&& (es_import || import.default_import.is_none())
1799+
&& (es_import || !import.names.is_empty())
17901800
&& import_path_matches_file(&import.module_path, consumer_file, dest_file)
17911801
}) {
17921802
let requested = &names[0];
@@ -1801,12 +1811,14 @@ fn build_add_moved_import_edit(
18011811

18021812
let merged_names =
18031813
super::add_import::merge_named_import_specifiers(&existing.names, &names);
1804-
let merged_line = imports::generate_import_line(
1814+
let merged_line = imports::generate_import_line_with_namespace_and_attribute_clause(
18051815
lang,
18061816
&existing.module_path,
18071817
&merged_names,
1808-
None,
1818+
existing.default_import.as_deref(),
1819+
existing.namespace_import.as_deref(),
18091820
false,
1821+
imports::es_import_attribute_clause(existing),
18101822
);
18111823
return Some((existing.byte_range.clone(), merged_line));
18121824
}
@@ -1815,8 +1827,29 @@ fn build_add_moved_import_edit(
18151827
let group = imports::classify_group(lang, &new_import_path);
18161828
let (insert_offset, needs_blank_before, needs_blank_after) =
18171829
imports::find_insertion_point(content, block, group, &new_import_path, false);
1818-
let import_line =
1819-
imports::generate_import_line(lang, &new_import_path, &names, default_import, false);
1830+
let import_line = if matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
1831+
let mut clauses = block
1832+
.imports
1833+
.iter()
1834+
.filter(|import| {
1835+
import_path_matches_file(&import.module_path, consumer_file, dest_file)
1836+
})
1837+
.filter_map(imports::es_import_attribute_clause);
1838+
let inherited_clause = clauses
1839+
.next()
1840+
.filter(|first| clauses.all(|item| item == *first));
1841+
imports::generate_import_line_with_namespace_and_attribute_clause(
1842+
lang,
1843+
&new_import_path,
1844+
&names,
1845+
default_import,
1846+
None,
1847+
false,
1848+
inherited_clause,
1849+
)
1850+
} else {
1851+
imports::generate_import_line(lang, &new_import_path, &names, default_import, false)
1852+
};
18201853

18211854
let mut insert_text = String::new();
18221855
if needs_blank_before {

crates/aft/src/commands/organize_imports.rs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ fn organized_from_statement(imp: &ImportStatement, lang: LangId) -> OrganizedImp
643643
default_import: imp.default_import.clone(),
644644
namespace_import: imp.namespace_import.clone(),
645645
kind: imp.kind,
646+
attribute_clause: imports::es_import_attribute_clause(imp).map(str::to_string),
646647
raw_override,
647648
}
648649
}
@@ -681,6 +682,7 @@ struct OrganizedImport {
681682
default_import: Option<String>,
682683
namespace_import: Option<String>,
683684
kind: ImportKind,
685+
attribute_clause: Option<String>,
684686
/// When set, the import is rendered verbatim from this string instead of
685687
/// being regenerated from the structured fields. Used by dialect-sensitive
686688
/// languages (e.g. Scala) where re-rendering would normalize across
@@ -732,22 +734,23 @@ fn organize_generic_group(
732734
side_effects.extend(sorted);
733735

734736
for imp in side_effects {
735-
// Build dedup key: module_path + kind + sorted names + default + namespace.
736-
// Namespace imports introduce local bindings, so different aliases are
737-
// distinct and side-effect imports are not duplicates of namespace
738-
// imports from the same module.
737+
// Build dedup key from every semantic part of the import. Import
738+
// attributes select the module type, so attributed and unattributed
739+
// imports of the same path must remain distinct.
739740
let names_key = {
740741
let mut n = imp.names.clone();
741742
sort_named_specifiers(&mut n);
742743
n.join(",")
743744
};
745+
let attribute_clause = imports::es_import_attribute_clause(imp);
744746
let dedup_key = format!(
745-
"{}|{:?}|{}|{}|{}",
747+
"{}|{:?}|{}|{}|{}|{}",
746748
imp.module_path,
747749
imp.kind,
748750
names_key,
749751
imp.default_import.as_deref().unwrap_or(""),
750-
imp.namespace_import.as_deref().unwrap_or("")
752+
imp.namespace_import.as_deref().unwrap_or(""),
753+
attribute_clause.unwrap_or("")
751754
);
752755

753756
if seen.contains(&dedup_key) {
@@ -765,6 +768,7 @@ fn organize_generic_group(
765768
default_import: imp.default_import.clone(),
766769
namespace_import: imp.namespace_import.clone(),
767770
kind: imp.kind,
771+
attribute_clause: attribute_clause.map(str::to_string),
768772
raw_override: None,
769773
});
770774
}
@@ -815,6 +819,7 @@ fn organize_raw_preserving_group(
815819
default_import: imp.default_import.clone(),
816820
namespace_import: imp.namespace_import.clone(),
817821
kind: imp.kind,
822+
attribute_clause: imports::es_import_attribute_clause(imp).map(str::to_string),
818823
raw_override: Some(imp.raw_text.trim().to_string()),
819824
})
820825
.collect();
@@ -994,6 +999,7 @@ fn organize_rust_group(imps: &[&ImportStatement]) -> (Vec<OrganizedImport>, usiz
994999
default_import: up.visibility.clone(),
9951000
namespace_import: None,
9961001
kind: up.kind,
1002+
attribute_clause: None,
9971003
raw_override: None,
9981004
});
9991005
}
@@ -1025,6 +1031,7 @@ fn organize_rust_group(imps: &[&ImportStatement]) -> (Vec<OrganizedImport>, usiz
10251031
default_import: visibility,
10261032
namespace_import: None,
10271033
kind,
1034+
attribute_clause: None,
10281035
raw_override: None,
10291036
});
10301037
}
@@ -1143,13 +1150,16 @@ fn generate_organized_line(imp: &OrganizedImport, lang: LangId) -> String {
11431150
format!("import \"{}\"", imp.module_path)
11441151
}
11451152
}
1146-
LangId::TypeScript | LangId::Tsx | LangId::JavaScript
1147-
if imp.names.is_empty()
1148-
&& imp.default_import.is_none()
1149-
&& imp.namespace_import.is_some() =>
1150-
{
1151-
let namespace = imp.namespace_import.as_deref().unwrap_or_default();
1152-
format!("import * as {} from '{}';", namespace, imp.module_path)
1153+
LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
1154+
imports::generate_import_line_with_namespace_and_attribute_clause(
1155+
lang,
1156+
&imp.module_path,
1157+
&imp.names,
1158+
imp.default_import.as_deref(),
1159+
imp.namespace_import.as_deref(),
1160+
imp.kind == ImportKind::Type,
1161+
imp.attribute_clause.as_deref(),
1162+
)
11531163
}
11541164
_ => {
11551165
// TS/JS/TSX/Python — use the standard generator

crates/aft/src/commands/remove_import.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,13 +362,14 @@ fn remove_name_from_imports(
362362
edits.push((range, String::new()));
363363
} else {
364364
// Other bindings remain — regenerate without target
365-
let new_line = imports::generate_import_line_with_namespace(
365+
let new_line = imports::generate_import_line_with_namespace_and_attribute_clause(
366366
lang,
367367
&imp.module_path,
368368
&new_names,
369369
imp.default_import.as_deref(),
370370
imp.namespace_import.as_deref(),
371371
imp.kind == imports::ImportKind::Type,
372+
imports::es_import_attribute_clause(imp),
372373
);
373374
edits.push((imp.byte_range.clone(), new_line));
374375
}
@@ -380,13 +381,14 @@ fn remove_name_from_imports(
380381
edits.push((range, String::new()));
381382
} else {
382383
// Has named or namespace imports too — regenerate without default
383-
let new_line = imports::generate_import_line_with_namespace(
384+
let new_line = imports::generate_import_line_with_namespace_and_attribute_clause(
384385
lang,
385386
&imp.module_path,
386387
&imp.names,
387388
None,
388389
imp.namespace_import.as_deref(),
389390
imp.kind == imports::ImportKind::Type,
391+
imports::es_import_attribute_clause(imp),
390392
);
391393
edits.push((imp.byte_range.clone(), new_line));
392394
}
@@ -398,13 +400,14 @@ fn remove_name_from_imports(
398400
edits.push((range, String::new()));
399401
} else {
400402
// Has default or named imports too — regenerate without namespace
401-
let new_line = imports::generate_import_line_with_namespace(
403+
let new_line = imports::generate_import_line_with_namespace_and_attribute_clause(
402404
lang,
403405
&imp.module_path,
404406
&imp.names,
405407
imp.default_import.as_deref(),
406408
None,
407409
imp.kind == imports::ImportKind::Type,
410+
imports::es_import_attribute_clause(imp),
408411
);
409412
edits.push((imp.byte_range.clone(), new_line));
410413
}

0 commit comments

Comments
 (0)