use super::helpers::{fixture_path, AftProcess};
use aft::imports::{generate_import_line_with_namespace, parse_file_imports};
use aft::parser::LangId;
use std::fs;
fn temp_copy(fixture_name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let src = fixture_path(fixture_name);
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let (stem, ext) = fixture_name.rsplit_once('.').unwrap_or((fixture_name, ""));
let unique = if ext.is_empty() {
format!("{}_{}", stem, n)
} else {
format!("{}_{}.{}", stem, n, ext)
};
let dest = dir.path().join(unique);
fs::copy(&src, &dest).unwrap();
(dir, dest)
}
fn send_add_import(
aft: &mut AftProcess,
id: &str,
file: &str,
module: &str,
names: Option<&[&str]>,
default_import: Option<&str>,
type_only: bool,
) -> serde_json::Value {
let mut params = serde_json::json!({
"id": id,
"command": "add_import",
"file": file,
"module": module,
});
if let Some(names) = names {
params["names"] = serde_json::json!(names);
}
if let Some(def) = default_import {
params["default_import"] = serde_json::json!(def);
}
if type_only {
params["type_only"] = serde_json::json!(true);
}
aft.send(&serde_json::to_string(¶ms).unwrap())
}
#[test]
fn add_import_ts_external_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_ts.ts");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"imp-1",
&file_str,
"lodash",
Some(&["debounce"]),
None,
false,
);
assert_eq!(
resp["success"], true,
"add_import should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "external");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import { debounce } from 'lodash';"),
"should contain the new import. content:\n{}",
content
);
let lodash_pos = content.find("import { debounce } from 'lodash'").unwrap();
let relative_pos = content.find("import { helper } from './utils'").unwrap();
assert!(
lodash_pos < relative_pos,
"lodash import should be before relative imports"
);
assert_eq!(
resp["syntax_valid"], true,
"syntax should be valid after add, resp: {:?}",
resp
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_ts_relative_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_ts.ts");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"imp-2",
&file_str,
"./components",
Some(&["Button"]),
None,
false,
);
assert_eq!(
resp["success"], true,
"add_import should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "internal");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import { Button } from './components';"),
"should contain the new relative import. content:\n{}",
content
);
let button_pos = content
.find("import { Button } from './components'")
.unwrap();
let react_pos = content.find("import React from 'react'").unwrap();
assert!(
button_pos > react_pos,
"relative import should be after external imports"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_ts_allows_parent_relative_module() {
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("parent_relative.ts");
fs::write(&file, "export const x = 1;\n").unwrap();
let resp = send_add_import(
&mut aft,
"imp-parent-relative",
&file.display().to_string(),
"../config",
Some(&["Config"]),
None,
false,
);
assert_eq!(
resp["success"], true,
"relative add should succeed: {resp:?}"
);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import { Config } from '../config';"),
"single-parent ES relative imports must remain allowed:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_c_allows_parent_relative_include() {
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("relative_include.c");
fs::write(&file, "int main(void) { return 0; }\n").unwrap();
let resp = send_add_import(
&mut aft,
"imp-c-parent-relative",
&file.display().to_string(),
"\"../foo.h\"",
None,
None,
false,
);
assert_eq!(
resp["success"], true,
"parent-relative C include should succeed: {resp:?}"
);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("#include \"../foo.h\""),
"C include should preserve the parent-relative local path:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_solidity_allows_parent_relative_import() {
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("RelativeImport.sol");
fs::write(
&file,
"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract C {}\n",
)
.unwrap();
let resp = send_add_import(
&mut aft,
"imp-sol-parent-relative",
&file.display().to_string(),
"../lib/X.sol",
None,
None,
false,
);
assert_eq!(
resp["success"], true,
"parent-relative Solidity import should succeed: {resp:?}"
);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import \"../lib/X.sol\";"),
"Solidity import should preserve the parent-relative path:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_c_rejects_absolute_modules() {
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("unsafe_include.c");
let original = "int main(void) { return 0; }\n";
fs::write(&file, original).unwrap();
let file_str = file.display().to_string();
for (id, module) in [
("imp-c-posix-absolute", "/etc/passwd"),
("imp-c-drive-absolute", "C:\\evil"),
("imp-c-unc-absolute", "\\\\srv\\x"),
] {
let resp = send_add_import(&mut aft, id, &file_str, module, None, None, false);
assert_eq!(
resp["success"], false,
"absolute module {module:?} should be rejected: {resp:?}"
);
assert_eq!(resp["code"], "invalid_request");
assert_eq!(
fs::read_to_string(&file).unwrap(),
original,
"rejected module {module:?} must not mutate the file"
);
}
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_java_and_php_reject_filesystem_modules() {
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let java_file = dir.path().join("Unsafe.java");
let java_original = "package demo;\n\nclass Unsafe {}\n";
fs::write(&java_file, java_original).unwrap();
let java_file_str = java_file.display().to_string();
for (id, module) in [
("imp-java-parent", "../evil"),
("imp-java-slash", "/evil"),
("imp-java-drive", "C:\\evil"),
] {
let resp = send_add_import(&mut aft, id, &java_file_str, module, None, None, false);
assert_eq!(
resp["success"], false,
"Java filesystem module {module:?} should be rejected: {resp:?}"
);
assert_eq!(resp["code"], "invalid_request");
assert_eq!(fs::read_to_string(&java_file).unwrap(), java_original);
}
let php_file = dir.path().join("unsafe.php");
let php_original = "<?php\n\nnamespace Demo;\n\nclass C {}\n";
fs::write(&php_file, php_original).unwrap();
let php_file_str = php_file.display().to_string();
for (id, module) in [
("imp-php-parent", "..\\Evil"),
("imp-php-slash", "/tmp/Evil"),
("imp-php-drive", "C:\\evil"),
] {
let resp = send_add_import(&mut aft, id, &php_file_str, module, None, None, false);
assert_eq!(
resp["success"], false,
"PHP filesystem module {module:?} should be rejected: {resp:?}"
);
assert_eq!(resp["code"], "invalid_request");
assert_eq!(fs::read_to_string(&php_file).unwrap(), php_original);
}
fs::remove_file(&java_file).ok();
fs::remove_file(&php_file).ok();
aft.shutdown();
}
#[test]
fn add_import_ts_dedup() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_ts.ts");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"imp-3",
&file_str,
"react",
Some(&["useState"]),
None,
false,
);
assert_eq!(resp["success"], true);
assert_eq!(resp["added"], false, "should not add duplicate");
assert_eq!(resp["already_present"], true);
let original = fs::read_to_string(fixture_path("imports_ts.ts")).unwrap();
let current = fs::read_to_string(&file).unwrap();
assert_eq!(
original, current,
"file should not have been modified for duplicate"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_ts_alphabetizes() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_ts.ts");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"imp-4",
&file_str,
"axios",
None,
Some("axios"),
false,
);
assert_eq!(resp["success"], true);
assert_eq!(resp["added"], true);
let content = fs::read_to_string(&file).unwrap();
let axios_pos = content.find("import axios from 'axios'").unwrap();
let react_pos = content.find("import React from 'react'").unwrap();
assert!(
axios_pos < react_pos,
"axios should sort before react alphabetically. content:\n{}",
content
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_js_works() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_js.js");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"imp-5",
&file_str,
"cors",
None,
Some("cors"),
false,
);
assert_eq!(
resp["success"], true,
"add_import on JS should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "external");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import cors from 'cors';"),
"should contain the new JS import. content:\n{}",
content
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_empty_file() {
use std::sync::atomic::{AtomicU64, Ordering};
static EMPTY_COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = EMPTY_COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("empty_{}.ts", n));
fs::write(&file, "").unwrap();
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"imp-6",
&file_str,
"react",
Some(&["useState"]),
None,
false,
);
assert_eq!(
resp["success"], true,
"add_import on empty file should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import { useState } from 'react';"),
"should contain the import at top. content:\n{}",
content
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_missing_file_returns_error() {
let mut aft = AftProcess::spawn();
let resp = send_add_import(
&mut aft,
"imp-7",
"/tmp/nonexistent_aft_test.ts",
"react",
Some(&["useState"]),
None,
false,
);
assert_eq!(resp["success"], false, "should fail for missing file");
assert_eq!(resp["code"], "file_not_found");
aft.shutdown();
}
#[test]
fn add_import_unsupported_language_returns_error() {
use std::sync::atomic::{AtomicU64, Ordering};
static UNSUP_COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = UNSUP_COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("test_{}.txt", n));
fs::write(&file, "hello world").unwrap();
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"imp-8",
&file_str,
"react",
Some(&["useState"]),
None,
false,
);
assert_eq!(
resp["success"], false,
"should fail for unsupported language"
);
assert_eq!(
resp["code"], "unsupported_language",
"unsupported file type uses the actionable standardized code, not invalid_request"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_missing_params_returns_error() {
let mut aft = AftProcess::spawn();
let resp = aft.send(r#"{"id":"imp-9","command":"add_import","file":"/tmp/test.ts"}"#);
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "invalid_request");
assert!(
resp["message"].as_str().unwrap().contains("module"),
"error should mention missing 'module' param"
);
aft.shutdown();
}
#[test]
fn add_import_py_stdlib_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_py.py");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"py-1",
&file_str,
"pathlib",
Some(&["Path"]),
None,
false,
);
assert_eq!(
resp["success"], true,
"add_import py stdlib should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "stdlib");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("from pathlib import Path"),
"should contain the new stdlib import. content:\n{}",
content
);
let pathlib_pos = content.find("from pathlib import Path").unwrap();
let requests_pos = content.find("import requests").unwrap();
assert!(
pathlib_pos < requests_pos,
"stdlib import should be before third-party imports"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_py_third_party_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_py.py");
let file_str = file.display().to_string();
let resp = send_add_import(&mut aft, "py-2", &file_str, "click", None, None, false);
assert_eq!(
resp["success"], true,
"add_import py third-party should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "external");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import click"),
"should contain the new third-party import. content:\n{}",
content
);
let click_pos = content.find("import click").unwrap();
let os_pos = content.find("import os").unwrap();
let utils_pos = content.find("from . import utils").unwrap();
assert!(
click_pos > os_pos,
"third-party import should be after stdlib"
);
assert!(
click_pos < utils_pos,
"third-party import should be before local"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_py_local_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_py.py");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"py-3",
&file_str,
".models",
Some(&["User"]),
None,
false,
);
assert_eq!(
resp["success"], true,
"add_import py local should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "internal");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("from .models import User"),
"should contain the new local import. content:\n{}",
content
);
let models_pos = content.find("from .models import User").unwrap();
let requests_pos = content.find("import requests").unwrap();
assert!(
models_pos > requests_pos,
"local import should be after third-party"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_py_dedup() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_py.py");
let file_str = file.display().to_string();
let resp = send_add_import(&mut aft, "py-4", &file_str, "os", None, None, false);
assert_eq!(resp["success"], true);
assert_eq!(resp["added"], false, "should not add duplicate");
assert_eq!(resp["already_present"], true);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_rs_std_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_rs.rs");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"rs-1",
&file_str,
"std::fmt::Display",
None,
None,
false,
);
assert_eq!(
resp["success"], true,
"add_import rs std should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "stdlib");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("use std::fmt::Display;"),
"should contain the new std import. content:\n{}",
content
);
let fmt_pos = content.find("use std::fmt::Display;").unwrap();
let serde_pos = content.find("use serde").unwrap();
assert!(
fmt_pos < serde_pos,
"std import should be before external imports"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_rs_external_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_rs.rs");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"rs-2",
&file_str,
"anyhow::Result",
None,
None,
false,
);
assert_eq!(
resp["success"], true,
"add_import rs external should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "external");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("use anyhow::Result;"),
"should contain the new external import. content:\n{}",
content
);
let anyhow_pos = content.find("use anyhow::Result;").unwrap();
let std_pos = content.find("use std::").unwrap();
let crate_pos = content.find("use crate::").unwrap();
assert!(anyhow_pos > std_pos, "external import should be after std");
assert!(
anyhow_pos < crate_pos,
"external import should be before crate"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_rs_dedup() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_rs.rs");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"rs-3",
&file_str,
"std::collections::HashMap",
None,
None,
false,
);
assert_eq!(resp["success"], true);
assert_eq!(resp["added"], false, "should not add duplicate");
assert_eq!(resp["already_present"], true);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_go_stdlib_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_go.go");
let file_str = file.display().to_string();
let resp = send_add_import(&mut aft, "go-1", &file_str, "net/http", None, None, false);
assert_eq!(
resp["success"], true,
"add_import go stdlib should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "stdlib");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("\"net/http\""),
"should contain the new stdlib import. content:\n{}",
content
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_go_external_group() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_go.go");
let file_str = file.display().to_string();
let resp = send_add_import(
&mut aft,
"go-2",
&file_str,
"golang.org/x/tools",
None,
None,
false,
);
assert_eq!(
resp["success"], true,
"add_import go external should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert_eq!(resp["group"], "external");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("\"golang.org/x/tools\""),
"should contain the new external import. content:\n{}",
content
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_import_go_dedup() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_go.go");
let file_str = file.display().to_string();
let resp = send_add_import(&mut aft, "go-3", &file_str, "fmt", None, None, false);
assert_eq!(resp["success"], true);
assert_eq!(resp["added"], false, "should not add duplicate");
assert_eq!(resp["already_present"], true);
fs::remove_file(&file).ok();
aft.shutdown();
}
fn send_remove_import(
aft: &mut AftProcess,
id: &str,
file: &str,
module: &str,
name: Option<&str>,
) -> serde_json::Value {
let mut params = serde_json::json!({
"id": id,
"command": "remove_import",
"file": file,
"module": module,
});
if let Some(n) = name {
params["name"] = serde_json::json!(n);
}
aft.send(&serde_json::to_string(¶ms).unwrap())
}
fn send_organize_imports(aft: &mut AftProcess, id: &str, file: &str) -> serde_json::Value {
let params = serde_json::json!({
"id": id,
"command": "organize_imports",
"file": file,
});
aft.send(&serde_json::to_string(¶ms).unwrap())
}
#[test]
fn remove_import_entire_statement_ts() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_ts.ts");
let file_str = file.display().to_string();
let resp = send_remove_import(&mut aft, "rm-1", &file_str, "zod", None);
assert_eq!(
resp["success"], true,
"remove_import should succeed: {:?}",
resp
);
assert_eq!(resp["removed"], true);
assert_eq!(resp["module"], "zod");
let content = fs::read_to_string(&file).unwrap();
assert!(
!content.contains("from 'zod'"),
"zod import should be removed. content:\n{}",
content
);
assert!(
content.contains("from 'react'"),
"react imports should remain"
);
assert_eq!(resp["syntax_valid"], true);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn remove_import_specific_name_from_multi_ts() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_ts.ts");
let file_str = file.display().to_string();
let resp = send_remove_import(&mut aft, "rm-2", &file_str, "react", Some("useState"));
assert_eq!(
resp["success"], true,
"remove_import should succeed: {:?}",
resp
);
assert_eq!(resp["removed"], true);
assert_eq!(resp["name"], "useState");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("useEffect") && content.contains("react"),
"useEffect import from react should remain. content:\n{}",
content
);
assert!(
!content.contains("import { useState, useEffect }"),
"the original multi-name import should be modified. content:\n{}",
content
);
assert_eq!(resp["syntax_valid"], true);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn remove_import_missing_module_reports_not_removed() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_ts.ts");
let file_str = file.display().to_string();
let resp = send_remove_import(&mut aft, "rm-3", &file_str, "nonexistent-module", None);
assert_eq!(resp["success"], true, "request should complete: {resp:?}");
assert_eq!(resp["removed"], false, "nothing should be removed");
assert_eq!(resp["reason"], "module_not_found");
assert_eq!(resp["no_op"], true, "no-match removes must report no_op");
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn remove_import_missing_name_reports_no_op() {
let mut aft = AftProcess::spawn();
let (_dir, file) = temp_copy("imports_ts.ts");
let file_str = file.display().to_string();
let resp = send_remove_import(
&mut aft,
"rm-missing-name",
&file_str,
"react",
Some("useMemo"),
);
assert_eq!(resp["success"], true, "request should complete: {resp:?}");
assert_eq!(resp["removed"], false, "nothing should be removed");
assert_eq!(resp["reason"], "name_not_found");
assert_eq!(resp["name"], "useMemo");
assert_eq!(resp["no_op"], true, "name misses must report no_op");
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn remove_import_preserves_default_when_named_removed() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join(format!(
"remove_default_named_{}.ts",
COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(
&file,
"import React, { useState } from 'react';\n\nexport const App = React.Fragment;\n",
)
.unwrap();
let resp = send_remove_import(
&mut aft,
"rm-preserve-default",
&file.display().to_string(),
"react",
Some("useState"),
);
assert_eq!(resp["success"], true, "remove should succeed: {resp:?}");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import React from 'react';"),
"default import should remain:\n{content}"
);
assert!(
!content.contains("useState"),
"named import should be removed"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_without_imports_reports_no_op() {
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("no_imports.ts");
let original = "export const x = 1;\n";
fs::write(&file, original).unwrap();
let resp = send_organize_imports(&mut aft, "org-no-imports", &file.display().to_string());
assert_eq!(resp["success"], true, "organize should succeed: {resp:?}");
assert_eq!(resp["groups"].as_array().unwrap().len(), 0);
assert_eq!(resp["removed_duplicates"], 0);
assert_eq!(resp["no_op"], true, "no-import organize must report no_op");
assert_eq!(fs::read_to_string(&file).unwrap(), original);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_rejects_multi_namespace_php_without_mutating() {
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("multi_namespace.php");
let original = r#"<?php
namespace Foo {
use Zed\Last;
class X {}
}
namespace Bar {
use App\Alpha;
class Y {}
}
"#;
fs::write(&file, original).unwrap();
let resp = send_organize_imports(&mut aft, "org-multi-php", &file.display().to_string());
assert_eq!(
resp["success"], false,
"multi-namespace PHP organize should be refused: {resp:?}"
);
assert_eq!(resp["code"], "multi_region_imports");
assert!(
resp["message"]
.as_str()
.unwrap_or_default()
.contains("span multiple code regions"),
"error should explain the multi-region refusal: {resp:?}"
);
assert_eq!(
fs::read_to_string(&file).unwrap(),
original,
"refused organize must leave the PHP file byte-for-byte unchanged"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_regroups_and_sorts() {
use std::sync::atomic::{AtomicU64, Ordering};
static ORG_COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = ORG_COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("organize_ts_{}.ts", n));
fs::write(
&file,
"\
import { helper } from './utils';
import { z } from 'zod';
import React from 'react';
import { Config } from '../config';
import { useState } from 'react';
export function App() {}
",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "org-1", &file_str);
assert_eq!(
resp["success"], true,
"organize_imports should succeed: {:?}",
resp
);
let content = fs::read_to_string(&file).unwrap();
let react_pos = content.find("react").unwrap();
let utils_pos = content.find("./utils").unwrap();
assert!(
react_pos < utils_pos,
"external imports should come before internal. content:\n{}",
content
);
let zod_pos = content.find("zod").unwrap();
assert!(
react_pos < zod_pos,
"react should come before zod (alphabetical). content:\n{}",
content
);
assert_eq!(resp["syntax_valid"], true);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_preserves_side_effect_order() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join(format!(
"organize_side_effects_{}.ts",
COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(
&file,
"import 'polyfill-b';\nimport z from 'zod';\nimport 'polyfill-a';\nimport React from 'react';\n\nexport const x = 1;\n",
)
.unwrap();
let resp = send_organize_imports(&mut aft, "org-side-effects", &file.display().to_string());
assert_eq!(resp["success"], true, "organize should succeed: {resp:?}");
let content = fs::read_to_string(&file).unwrap();
let polyfill_b_pos = content.find("import 'polyfill-b';").unwrap();
let polyfill_a_pos = content.find("import 'polyfill-a';").unwrap();
let react_pos = content.find("import React from 'react';").unwrap();
let zod_pos = content.find("import z from 'zod';").unwrap();
assert!(
polyfill_b_pos < polyfill_a_pos,
"side-effect imports keep original relative order (b before a):\n{content}"
);
assert!(
zod_pos < polyfill_a_pos,
"value imports before a side-effect barrier must not cross it:\n{content}"
);
assert!(
polyfill_a_pos < react_pos,
"side-effects keep their source position relative to following value imports:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_preserves_inter_import_comments() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join(format!(
"organize_comment_gap_{}.ts",
COUNTER.fetch_add(1, Ordering::SeqCst)
));
let original =
"import A from 'a';\n// keep me\nimport B from 'b';\n\nexport const x = A || B;\n";
fs::write(&file, original).unwrap();
let resp = send_organize_imports(&mut aft, "org-comment-gap", &file.display().to_string());
assert_eq!(resp["success"], true, "organize should succeed: {resp:?}");
let content = fs::read_to_string(&file).unwrap();
assert_eq!(content, original, "comment gap must be preserved");
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn add_remove_import_refuse_csharp_and_php_multi_region_imports() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let cs_file = dir.path().join(format!("multi_region_{n}.cs"));
fs::write(
&cs_file,
"namespace A {\nusing Common;\nclass A {}\n}\nnamespace B {\nusing Common;\nclass B {}\n}\n",
)
.unwrap();
let cs_file_str = cs_file.display().to_string();
let add_cs = send_add_import(
&mut aft,
"cs-multi-add",
&cs_file_str,
"System",
None,
None,
false,
);
assert_eq!(add_cs["success"], false, "C# add should refuse: {add_cs:?}");
assert_eq!(add_cs["code"], "multi_region_imports");
let remove_cs = send_remove_import(&mut aft, "cs-multi-remove", &cs_file_str, "Common", None);
assert_eq!(
remove_cs["success"], false,
"C# remove should refuse: {remove_cs:?}"
);
assert_eq!(remove_cs["code"], "multi_region_imports");
let php_file = dir.path().join(format!("multi_region_{n}.php"));
fs::write(
&php_file,
"<?php\nnamespace A {\nuse Common\\Thing;\nclass A {}\n}\nnamespace B {\nuse Common\\Thing;\nclass B {}\n}\n",
)
.unwrap();
let php_file_str = php_file.display().to_string();
let add_php = send_add_import(
&mut aft,
"php-multi-add",
&php_file_str,
"Other\\Thing",
None,
None,
false,
);
assert_eq!(
add_php["success"], false,
"PHP add should refuse: {add_php:?}"
);
assert_eq!(add_php["code"], "multi_region_imports");
let remove_php = send_remove_import(
&mut aft,
"php-multi-remove",
&php_file_str,
"Common\\Thing",
None,
);
assert_eq!(
remove_php["success"], false,
"PHP remove should refuse: {remove_php:?}"
);
assert_eq!(remove_php["code"], "multi_region_imports");
fs::remove_file(&cs_file).ok();
fs::remove_file(&php_file).ok();
aft.shutdown();
}
#[test]
fn php_grouped_use_refuses_memberwise_add_and_remove() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join(format!(
"php_grouped_use_{}.php",
COUNTER.fetch_add(1, Ordering::SeqCst)
));
let original = "<?php\nuse App\\{Foo, Bar as Baz};\n\nclass C {}\n";
fs::write(&file, original).unwrap();
let file_str = file.display().to_string();
let add_resp = send_add_import(
&mut aft,
"php-group-add",
&file_str,
"App\\Foo",
None,
None,
false,
);
assert_eq!(
add_resp["success"], false,
"add should refuse: {add_resp:?}"
);
assert_eq!(add_resp["code"], "unsupported_grouped_import");
let remove_resp = send_remove_import(&mut aft, "php-group-remove", &file_str, "App\\Foo", None);
assert_eq!(
remove_resp["success"], false,
"remove should refuse: {remove_resp:?}"
);
assert_eq!(remove_resp["code"], "unsupported_grouped_import");
assert_eq!(fs::read_to_string(&file).unwrap(), original);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_go_grouped_block_refuses_internal_comments() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join(format!(
"organize_go_grouped_comments_{}.go",
COUNTER.fetch_add(1, Ordering::SeqCst)
));
let original = "package main\n\nimport (\n\t\"fmt\"\n\t// keep me with this block\n\t\"os\"\n)\n\nfunc main() {}\n";
fs::write(&file, original).unwrap();
let resp = send_organize_imports(
&mut aft,
"org-go-grouped-comments",
&file.display().to_string(),
);
assert_eq!(
resp["success"], false,
"commented Go grouped imports should be refused: {resp:?}"
);
assert_eq!(resp["code"], "unsupported_import_comments");
assert_eq!(
fs::read_to_string(&file).unwrap(),
original,
"refused organize must leave the Go file byte-for-byte unchanged"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_go_grouped_block_parses() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join(format!(
"organize_go_grouped_{}.go",
COUNTER.fetch_add(1, Ordering::SeqCst)
));
fs::write(
&file,
"package main\n\nimport (\n\t\"os\"\n\t\"fmt\"\n)\n\nfunc main() {}\n",
)
.unwrap();
let resp = send_organize_imports(&mut aft, "org-go-grouped", &file.display().to_string());
assert_eq!(resp["success"], true, "organize should succeed: {resp:?}");
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import (\n\t\"fmt\"\n\t\"os\"\n)"),
"grouped block should be regenerated and sorted:\n{content}"
);
let mut parser = tree_sitter::Parser::new();
let language: tree_sitter::Language = tree_sitter_go::LANGUAGE.into();
parser.set_language(&language).expect("set go grammar");
let tree = parser.parse(&content, None).expect("parse go");
assert!(
!tree.root_node().has_error(),
"Go output should parse:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_deduplicates() {
use std::sync::atomic::{AtomicU64, Ordering};
static DEDUP_COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = DEDUP_COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("organize_dedup_{}.ts", n));
fs::write(
&file,
"\
import { z } from 'zod';
import { z } from 'zod';
import React from 'react';
import React from 'react';
export function App() {}
",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "org-2", &file_str);
assert_eq!(
resp["success"], true,
"organize_imports should succeed: {:?}",
resp
);
assert!(
resp["removed_duplicates"].as_u64().unwrap() >= 2,
"should remove at least 2 duplicates: {:?}",
resp
);
let content = fs::read_to_string(&file).unwrap();
let zod_count = content.matches("'zod'").count();
assert_eq!(
zod_count, 1,
"should have exactly one zod import. content:\n{}",
content
);
let react_count = content.matches("from 'react'").count();
assert_eq!(
react_count, 1,
"should have exactly one react import. content:\n{}",
content
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_py_isort_grouping() {
use std::sync::atomic::{AtomicU64, Ordering};
static PY_ORG_COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = PY_ORG_COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("organize_py_{}.py", n));
fs::write(
&file,
"\
from . import utils
import requests
import os
import sys
from ..config import Settings
def main():
pass
",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "org-3", &file_str);
assert_eq!(
resp["success"], true,
"organize_imports should succeed: {:?}",
resp
);
let groups = resp["groups"].as_array().unwrap();
assert!(
groups.len() >= 2,
"should have at least 2 groups: {:?}",
groups
);
assert_eq!(groups[0]["name"], "stdlib", "first group should be stdlib");
let content = fs::read_to_string(&file).unwrap();
let os_pos = content.find("import os").unwrap();
let requests_pos = content.find("import requests").unwrap();
assert!(
os_pos < requests_pos,
"stdlib should come before external. content:\n{}",
content
);
let utils_pos = content.find("utils").unwrap();
assert!(
requests_pos < utils_pos,
"external should come before internal. content:\n{}",
content
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_rs_merges_common_prefix() {
use std::sync::atomic::{AtomicU64, Ordering};
static RS_ORG_COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = RS_ORG_COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("organize_rs_{}.rs", n));
fs::write(
&file,
"\
use std::path::PathBuf;
use std::path::Path;
use std::collections::HashMap;
use serde::Deserialize;
use serde::Serialize;
use crate::config::Settings;
fn main() {}
",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "org-4", &file_str);
assert_eq!(
resp["success"], true,
"organize_imports should succeed: {:?}",
resp
);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("use std::path::{Path, PathBuf};"),
"should merge std::path imports into a use tree. content:\n{}",
content
);
assert!(
content.contains("use serde::{Deserialize, Serialize};"),
"should merge serde imports into a use tree. content:\n{}",
content
);
let std_pos = content.find("use std::").unwrap();
let serde_pos = content.find("use serde::").unwrap();
let crate_pos = content.find("use crate::").unwrap();
assert!(std_pos < serde_pos, "stdlib before external");
assert!(serde_pos < crate_pos, "external before internal");
assert_eq!(resp["syntax_valid"], true);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_rs_preserves_nested_use_tree() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("organize_rs_nested_{}.rs", n));
fs::write(
&file,
"\
use std::collections::{hash_map::{Entry, HashMap}, BTreeMap};
use std::fmt;
fn main() {}
",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "org-rs-nested", &file_str);
assert_eq!(
resp["success"], true,
"organize_imports should succeed: {resp:?}"
);
assert_eq!(
resp["syntax_valid"], true,
"organized Rust must stay syntactically valid: {resp:?}"
);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("hash_map::{Entry, HashMap}"),
"nested subtree must be preserved intact. content:\n{content}"
);
assert!(
!content.contains("}, hash_map::{Entry};")
&& !content.contains("{BTreeMap, HashMap}, hash_map"),
"must not produce invalid comma-joined brace trees. content:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_rs_preserves_pub_use_and_private_use_pair() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("organize_rs_pub_private_{}.rs", n));
fs::write(
&file,
"\
pub use serde;
use serde;
fn main() {}
",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "org-rs-pub-private", &file_str);
assert_eq!(
resp["success"], true,
"organize_imports should succeed: {:?}",
resp
);
let content = fs::read_to_string(&file).unwrap();
assert!(content.contains("pub use serde;"), "content:\n{content}");
assert!(content.contains("use serde;"), "content:\n{content}");
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_preserves_named_aliases() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("alias_ts_{}.ts", n));
fs::write(
&file,
"import { stdin as input, stdout as output } from 'node:process'\n\
\n\
const rl = createInterface({ input, output })\n",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "alias-ts", &file_str);
assert_eq!(resp["success"], true, "organize succeeded: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("stdin as input"),
"alias `stdin as input` must survive organize. got:\n{content}"
);
assert!(
content.contains("stdout as output"),
"alias `stdout as output` must survive organize. got:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_preserves_per_name_type_prefix() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("typeprefix_ts_{}.ts", n));
fs::write(
&file,
"import { type Foo, Bar, baz as qux } from './a'\n\
\n\
export type X = Foo\n\
export const y: typeof Bar = baz\n\
qux()\n",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "typeprefix-ts", &file_str);
assert_eq!(resp["success"], true, "organize succeeded: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("type Foo"),
"per-name `type Foo` modifier must survive. got:\n{content}"
);
assert!(
content.contains("Bar"),
"non-type `Bar` import must survive. got:\n{content}"
);
assert!(
content.contains("baz as qux"),
"alias `baz as qux` must survive alongside type modifiers. got:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_aliased_and_bare_are_not_duplicates() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("alias_dedup_ts_{}.ts", n));
fs::write(
&file,
"import { Foo } from './a'\n\
import { Foo as Bar } from './a'\n\
\n\
export const x = Foo\n\
export const y = Bar\n",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "alias-dedup-ts", &file_str);
assert_eq!(resp["success"], true, "organize succeeded: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("Foo as Bar"),
"aliased import `Foo as Bar` must not be dedup'd away by bare `Foo`. got:\n{content}"
);
assert!(
content.matches("Foo").count() >= 2,
"bare `Foo` and `Foo as Bar` must both survive. got:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_preserves_namespace_and_side_effect_imports() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir
.path()
.join(format!("namespace_side_effect_ts_{}.ts", n));
fs::write(
&file,
"import 'fs'\n\
import * as fs from 'fs'\n\
\n\
export const exists = fs.existsSync\n",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "namespace-side-effect-ts", &file_str);
assert_eq!(resp["success"], true, "organize succeeded: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import 'fs';"),
"side-effect import must survive alongside namespace import. got:\n{content}"
);
assert!(
content.contains("import * as fs from 'fs';"),
"namespace import must not be dedup'd as a side-effect import. got:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_preserves_side_effect_and_namespace_imports_reverse_order() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir
.path()
.join(format!("side_effect_namespace_ts_{}.ts", n));
fs::write(
&file,
"import * as fs from 'fs'\n\
import 'fs'\n\
\n\
export const exists = fs.existsSync\n",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "side-effect-namespace-ts", &file_str);
assert_eq!(resp["success"], true, "organize succeeded: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import 'fs';"),
"side-effect import must survive when namespace import appears first. got:\n{content}"
);
assert!(
content.contains("import * as fs from 'fs';"),
"namespace import must survive when side-effect import appears second. got:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_dedupes_identical_namespace_imports() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("namespace_dedup_ts_{}.ts", n));
fs::write(
&file,
"import * as fs from 'fs'\n\
import * as fs from 'fs'\n\
\n\
export const exists = fs.existsSync\n",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "namespace-dedup-ts", &file_str);
assert_eq!(resp["success"], true, "organize succeeded: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
assert_eq!(
content.matches("import * as fs from 'fs';").count(),
1,
"identical namespace imports should dedupe. got:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_keeps_distinct_namespace_aliases() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("namespace_aliases_ts_{}.ts", n));
fs::write(
&file,
"import * as foo from 'fs'\n\
import * as bar from 'fs'\n\
\n\
export const a = foo.existsSync\n\
export const b = bar.readFileSync\n",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "namespace-aliases-ts", &file_str);
assert_eq!(resp["success"], true, "organize succeeded: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import * as foo from 'fs';"),
"namespace alias `foo` must survive. got:\n{content}"
);
assert!(
content.contains("import * as bar from 'fs';"),
"namespace alias `bar` must survive. got:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn organize_imports_ts_sorts_named_specifiers_by_imported_name() {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let file = dir.path().join(format!("specifier_sort_ts_{}.ts", n));
fs::write(
&file,
"import { useState, type Foo, stdin as input, type Bar } from 'x'\n\
\n\
export const value = [input, useState]\n",
)
.unwrap();
let file_str = file.display().to_string();
let resp = send_organize_imports(&mut aft, "specifier-sort-ts", &file_str);
assert_eq!(resp["success"], true, "organize succeeded: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
assert!(
content.contains("import { type Bar, type Foo, stdin as input, useState } from 'x';"),
"named specifiers should sort by imported name, ignoring `type` and aliases. got:\n{content}"
);
fs::remove_file(&file).ok();
aft.shutdown();
}
#[test]
fn generate_ts_namespace_import_line_round_trips_namespace_only() {
let tmp = tempfile::tempdir().expect("create temp dir");
let file = tmp.path().join("namespace.ts");
fs::write(&file, "import * as ns from './mod';\n").expect("write import file");
let (_, _, block) = parse_file_imports(&file, LangId::TypeScript).expect("parse imports");
let import = block.imports.first().expect("parsed import");
let line = generate_import_line_with_namespace(
LangId::TypeScript,
&import.module_path,
&import.names,
import.default_import.as_deref(),
import.namespace_import.as_deref(),
false,
);
assert_eq!(line, "import * as ns from './mod';");
}
#[test]
fn generate_ts_namespace_import_line_round_trips_default_and_namespace() {
let tmp = tempfile::tempdir().expect("create temp dir");
let file = tmp.path().join("default_namespace.ts");
fs::write(&file, "import Foo, * as ns from './mod';\n").expect("write import file");
let (_, _, block) = parse_file_imports(&file, LangId::TypeScript).expect("parse imports");
let import = block.imports.first().expect("parsed import");
let line = generate_import_line_with_namespace(
LangId::TypeScript,
&import.module_path,
&import.names,
import.default_import.as_deref(),
import.namespace_import.as_deref(),
false,
);
assert_eq!(line, "import Foo, * as ns from './mod';");
}
#[test]
fn add_import_merges_into_existing_same_module_named_import() {
let mut aft = AftProcess::spawn();
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("merge.ts");
fs::write(
&file,
"import { foo } from \"lib\";\n\nexport function use() {\n return foo();\n}\n",
)
.unwrap();
aft.send(&format!(
r#"{{"id":"cfg","command":"configure","harness":"opencode","project_root":{}}}"#,
crate::helpers::json_string(&dir.path().display())
));
let resp = send_add_import(
&mut aft,
"merge1",
file.to_str().unwrap(),
"lib",
Some(&["baz"]),
None,
false,
);
assert_eq!(
resp["success"], true,
"merge add should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true, "should report added=true: {:?}", resp);
let content = fs::read_to_string(&file).unwrap();
let from_lib_count =
content.matches("from \"lib\"").count() + content.matches("from 'lib'").count();
assert_eq!(
from_lib_count, 1,
"should have exactly one statement importing from 'lib':\n{}",
content
);
assert!(
content.contains("baz") && content.contains("foo"),
"merged statement should contain both names:\n{}",
content
);
aft.shutdown();
}