mod alias tmp fix

This commit is contained in:
Hong Jiarong 2025-11-16 12:58:16 +08:00
parent f527c3509f
commit 5676e481ca
9 changed files with 215 additions and 4 deletions

View file

@ -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<usize>,
}
let text = ei.source.text();
let module_spans: Vec<ModuleSpan> = definitions
.iter()
.filter_map(|def| match def.decl.as_ref() {
@ -114,6 +122,7 @@ fn compute_import_usage(
let mut alias_links: HashMap<Interned<Decl>, Interned<Decl>> = HashMap::new();
let mut shadowed = HashSet::new();
let mut module_children: HashMap<Interned<Decl>, HashSet<Interned<Decl>>> = HashMap::new();
let mut alias_item_ranges: Vec<(Range<usize>, Interned<Decl>)> = 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<usize>, inner: &Range<usize>) -> bool {
outer.start <= inner.start && outer.end >= inner.end
}
fn alias_items_range(text: &str, alias_range: &Range<usize>) -> Option<Range<usize>> {
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;

View file

@ -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",

View file

@ -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<usize>,
) -> Option<Range<usize>> {
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<usize>) -> Option<()> {
let cursor = (range.start + 1).min(self.source.text().len());
@ -857,6 +926,10 @@ fn match_autofix_kind(source: &str, msg: &str) -> Option<AutofixKind> {
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 {

View file

@ -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

View file

@ -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

View file

@ -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
---
{}

View file

@ -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"
}
]
}

View file

@ -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
---
[]

View file

@ -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"
}
]