diff --git a/crates/tinymist-lint/src/dead_code.rs b/crates/tinymist-lint/src/dead_code.rs index 3ddcdf92..39e36691 100644 --- a/crates/tinymist-lint/src/dead_code.rs +++ b/crates/tinymist-lint/src/dead_code.rs @@ -55,7 +55,14 @@ pub fn check_dead_code( let (import_usage, shadowed_imports, module_children) = compute_import_usage(world, &definitions, ei); + let mut seen_module_aliases = HashSet::new(); + for def_info in definitions { + if matches!(def_info.decl.as_ref(), Decl::ModuleAlias(_)) + && !seen_module_aliases.insert(def_info.decl.clone()) + { + continue; + } if shadowed_imports.contains(&def_info.decl) { continue; } @@ -65,7 +72,7 @@ pub fn check_dead_code( let is_unused = match def_info.decl.as_ref() { Decl::Import(_) | Decl::ImportAlias(_) => !import_usage.contains(&def_info.decl), - Decl::ModuleImport(_) => { + Decl::ModuleImport(_) | Decl::ModuleAlias(_) => { let children_used = module_children.get(&def_info.decl).is_some_and(|children| { children.iter().any(|child| import_usage.contains(child)) }); @@ -99,6 +106,7 @@ fn compute_import_usage( range: Range, } + let text = ei.source.text(); let module_spans: Vec = definitions .iter() .filter_map(|def| match def.decl.as_ref() { @@ -114,6 +122,7 @@ fn compute_import_usage( let mut alias_links: HashMap, Interned> = HashMap::new(); let mut shadowed = HashSet::new(); let mut module_children: HashMap, HashSet>> = HashMap::new(); + let mut alias_item_ranges: Vec<(Range, Interned)> = Vec::new(); for def in definitions { if matches!(def.decl.as_ref(), Decl::ImportAlias(_)) { @@ -124,6 +133,16 @@ fn compute_import_usage( } } } + if matches!(def.decl.as_ref(), Decl::ModuleAlias(_)) { + if let Some(alias_range) = world.source_range(def.span) { + if let Some(items_range) = alias_items_range(text, &alias_range) { + alias_item_ranges.push((items_range, def.decl.clone())); + } + } + } + } + + for def in definitions { if matches!(def.decl.as_ref(), Decl::Import(_) | Decl::ImportAlias(_)) { if let Some(child_range) = world.source_range(def.span) { if let Some(module) = module_spans @@ -135,6 +154,16 @@ fn compute_import_usage( .or_default() .insert(def.decl.clone()); } + + if let Some((_, alias_decl)) = alias_item_ranges + .iter() + .find(|(range, _)| range.contains(&child_range.start)) + { + module_children + .entry(alias_decl.clone()) + .or_default() + .insert(def.decl.clone()); + } } } } @@ -167,6 +196,26 @@ fn contains_range(outer: &Range, inner: &Range) -> bool { outer.start <= inner.start && outer.end >= inner.end } +fn alias_items_range(text: &str, alias_range: &Range) -> Option> { + let bytes = text.as_bytes(); + let mut idx = alias_range.end; + while idx < bytes.len() && matches!(bytes[idx], b' ' | b'\t') { + idx += 1; + } + if idx >= bytes.len() || bytes[idx] != b':' { + return None; + } + idx += 1; + while idx < bytes.len() && matches!(bytes[idx], b' ' | b'\t') { + idx += 1; + } + let mut end = idx; + while end < bytes.len() && bytes[end] != b'\n' && bytes[end] != b'\r' { + end += 1; + } + Some(idx..end) +} + fn should_skip_definition(def_info: &DefInfo, config: &DeadCodeConfig) -> bool { if matches!(def_info.scope, DefScope::Exported) && !config.check_exported { return true; diff --git a/crates/tinymist-lint/src/dead_code/diagnostic.rs b/crates/tinymist-lint/src/dead_code/diagnostic.rs index 1dea7b1b..484ea7bb 100644 --- a/crates/tinymist-lint/src/dead_code/diagnostic.rs +++ b/crates/tinymist-lint/src/dead_code/diagnostic.rs @@ -24,11 +24,13 @@ pub fn generate_diagnostic( } let is_module_import = matches!(def_info.decl.as_ref(), Decl::ModuleImport(..)); + let is_module_alias = matches!(def_info.decl.as_ref(), Decl::ModuleAlias(_)); let is_import_item = matches!( def_info.decl.as_ref(), - Decl::Import(_) | Decl::ImportAlias(_) + Decl::Import(_) | Decl::ImportAlias(_) | Decl::ModuleAlias(_) ); - let is_module_like = is_module_import || matches!(def_info.kind, DefKind::Module); + let is_module_like = + is_module_import || matches!(def_info.kind, DefKind::Module) && !is_module_alias; let kind_str = match def_info.kind { DefKind::Function => "function", diff --git a/crates/tinymist-query/src/analysis/code_action.rs b/crates/tinymist-query/src/analysis/code_action.rs index 55699afc..2d0c7528 100644 --- a/crates/tinymist-query/src/analysis/code_action.rs +++ b/crates/tinymist-query/src/analysis/code_action.rs @@ -447,7 +447,8 @@ impl<'a> CodeActionWorker<'a> { // Calculate the range to remove, expand to cover the whole import item // (e.g. `foo as bar`) and include trailing comma if present. let mut remove_range = self - .find_import_item_range(root, name_range) + .module_alias_remove_range(root, name_range) + .or_else(|| self.find_import_item_range(root, name_range)) .unwrap_or_else(|| name_range.clone()); let bytes = self.source.text().as_bytes(); @@ -496,6 +497,74 @@ impl<'a> CodeActionWorker<'a> { }) } + fn module_alias_remove_range( + &self, + root: &LinkedNode<'_>, + name_range: &Range, + ) -> Option> { + if name_range.is_empty() { + return None; + } + + let cursor = (name_range.start + name_range.end) / 2; + let node = root.leaf_at_compat(cursor)?; + + let mut in_module_import = false; + for ancestor in node_ancestors(&node) { + match ancestor.kind() { + SyntaxKind::RenamedImportItem => return None, + SyntaxKind::ModuleImport => { + in_module_import = true; + break; + } + _ => {} + } + } + + if !in_module_import { + return None; + } + + let bytes = self.source.text().as_bytes(); + if name_range.end > bytes.len() || name_range.start > bytes.len() { + return None; + } + + let mut idx = name_range.start; + while idx > 0 && matches!(bytes[idx - 1], b' ' | b'\t') { + idx -= 1; + } + + if idx < 2 { + return None; + } + + let as_end = idx; + let as_start = as_end - 2; + if &bytes[as_start..as_end] != b"as" { + return None; + } + + if as_start > 0 && is_ascii_ident(bytes[as_start - 1]) { + return None; + } + if as_end < bytes.len() && is_ascii_ident(bytes[as_end]) { + return None; + } + + let mut removal_start = as_start; + while removal_start > 0 && matches!(bytes[removal_start - 1], b' ' | b'\t') { + removal_start -= 1; + } + + let mut removal_end = name_range.end; + while removal_end < bytes.len() && matches!(bytes[removal_end], b' ' | b'\t') { + removal_end += 1; + } + + Some(removal_start..removal_end) + } + /// Starts to work. pub fn scoped(&mut self, root: &LinkedNode, range: &Range) -> Option<()> { let cursor = (range.start + 1).min(self.source.text().len()); @@ -857,6 +926,10 @@ fn match_autofix_kind(source: &str, msg: &str) -> Option { None } +fn is_ascii_ident(ch: u8) -> bool { + matches!(ch, b'_' | b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') +} + fn is_plain_identifier(name: &str) -> bool { let mut chars = name.chars(); let Some(first) = chars.next() else { diff --git a/crates/tinymist-query/src/fixtures/dead_code/import_module_alias_mixed.typ b/crates/tinymist-query/src/fixtures/dead_code/import_module_alias_mixed.typ new file mode 100644 index 00000000..f21b1792 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/dead_code/import_module_alias_mixed.typ @@ -0,0 +1,15 @@ +/// path: u.typ + +#let foo() = "foo" +#let bar() = "bar" + +----- +/// path: main.typ +/// compile: true + +#import "u.typ" as util: foo + +#let value = foo() +#value + + diff --git a/crates/tinymist-query/src/fixtures/dead_code/import_module_alias_unused.typ b/crates/tinymist-query/src/fixtures/dead_code/import_module_alias_unused.typ new file mode 100644 index 00000000..27852151 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/dead_code/import_module_alias_unused.typ @@ -0,0 +1,15 @@ +/// path: u.typ + +#let foo() = "foo" +#let bar() = "bar" + +----- +/// path: main.typ +/// compile: true + +#import "u.typ" as util + +#let answer = 42 +#answer + + diff --git a/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@import_module_alias_mixed.typ.snap b/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@import_module_alias_mixed.typ.snap new file mode 100644 index 00000000..0be75c2b --- /dev/null +++ b/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@import_module_alias_mixed.typ.snap @@ -0,0 +1,6 @@ +--- +source: crates/tinymist-query/src/analysis.rs +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/dead_code/import_module_alias_mixed.typ +--- +{} diff --git a/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@import_module_alias_unused.typ.snap b/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@import_module_alias_unused.typ.snap new file mode 100644 index 00000000..57c42d91 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@import_module_alias_unused.typ.snap @@ -0,0 +1,15 @@ +--- +source: crates/tinymist-query/src/analysis.rs +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/dead_code/import_module_alias_unused.typ +--- +{ + "main.typ": [ + { + "message": "unused import: `util`\nHint: consider removing this unused import", + "range": "2:19:2:23", + "severity": 2, + "source": "typst" + } + ] +} diff --git a/crates/tinymist-query/src/fixtures/dead_code_code_action/snaps/run_dead_code_code_action_snapshots@import_module_alias_mixed.typ.snap b/crates/tinymist-query/src/fixtures/dead_code_code_action/snaps/run_dead_code_code_action_snapshots@import_module_alias_mixed.typ.snap new file mode 100644 index 00000000..4582124d --- /dev/null +++ b/crates/tinymist-query/src/fixtures/dead_code_code_action/snaps/run_dead_code_code_action_snapshots@import_module_alias_mixed.typ.snap @@ -0,0 +1,7 @@ +--- +source: crates/tinymist-query/src/code_action.rs +description: Dead code code actions in /dummy-root/main.typ +expression: "JsonRepr::new_pure(ordered_entries)" +input_file: crates/tinymist-query/src/fixtures/dead_code/import_module_alias_mixed.typ +--- +[] diff --git a/crates/tinymist-query/src/fixtures/dead_code_code_action/snaps/run_dead_code_code_action_snapshots@import_module_alias_unused.typ.snap b/crates/tinymist-query/src/fixtures/dead_code_code_action/snaps/run_dead_code_code_action_snapshots@import_module_alias_unused.typ.snap new file mode 100644 index 00000000..95467763 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/dead_code_code_action/snaps/run_dead_code_code_action_snapshots@import_module_alias_unused.typ.snap @@ -0,0 +1,29 @@ +--- +source: crates/tinymist-query/src/code_action.rs +description: Dead code code actions in /dummy-root/main.typ +expression: "JsonRepr::new_pure(ordered_entries)" +input_file: crates/tinymist-query/src/fixtures/dead_code/import_module_alias_unused.typ +--- +[ + { + "actions": [ + { + "edit": { + "changes": { + "main.typ": [ + { + "insertTextFormat": 1, + "newText": "", + "range": "2:15:2:23" + } + ] + } + }, + "kind": "quickfix", + "title": "Remove unused import" + } + ], + "message": "unused import: `util`\nHint: consider removing this unused import", + "range": "2:19:2:23" + } +]