feat: cross-module reference tracking

This commit is contained in:
Hong Jiarong 2025-11-15 16:32:48 +08:00
parent 977fb2a219
commit 2c18389aa2
5 changed files with 104 additions and 14 deletions

View file

@ -3,7 +3,10 @@
mod collector;
mod diagnostic;
use tinymist_analysis::syntax::{Decl, ExprInfo};
use tinymist_analysis::{
adt::interner::Interned,
syntax::{Decl, ExprInfo},
};
use tinymist_project::LspWorld;
use typst::ecow::EcoVec;
@ -32,7 +35,12 @@ impl Default for DeadCodeConfig {
}
}
pub fn check_dead_code(world: &LspWorld, ei: &ExprInfo, config: &DeadCodeConfig) -> DiagnosticVec {
pub fn check_dead_code(
world: &LspWorld,
ei: &ExprInfo,
has_references: impl Fn(&Interned<Decl>) -> bool,
config: &DeadCodeConfig,
) -> DiagnosticVec {
let mut diagnostics = EcoVec::new();
let definitions = collect_definitions(ei);
@ -46,14 +54,7 @@ pub fn check_dead_code(world: &LspWorld, ei: &ExprInfo, config: &DeadCodeConfig)
continue;
}
let has_refs = {
let target_decl = def_info.decl.clone();
ei.get_refs(target_decl.clone())
.any(|(_, r)| r.as_ref().decl != target_decl)
};
if !has_refs {
if !has_references(&def_info.decl) {
if let Some(diag) = generate_diagnostic(&def_info, world, ei) {
diagnostics.push(diag);
}

View file

@ -41,10 +41,12 @@ pub fn lint_file(
ei: &ExprInfo,
ti: Arc<TypeInfo>,
known_issues: KnownIssues,
has_references: impl Fn(&Interned<Decl>) -> bool,
) -> LintInfo {
let mut diagnostics = Linter::new(world, ei.clone(), ti, known_issues).lint(ei.source.root());
let dead_code_diags = dead_code::check_dead_code(world, ei, &DeadCodeConfig::default());
let dead_code_diags =
dead_code::check_dead_code(world, ei, has_references, &DeadCodeConfig::default());
diagnostics.extend(dead_code_diags);
LintInfo {

View file

@ -7,7 +7,8 @@ use std::{collections::HashSet, ops::Deref};
use comemo::{Track, Tracked};
use lsp_types::Url;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};
use tinymist_analysis::adt::interner::Interned;
use tinymist_analysis::docs::DocString;
use tinymist_analysis::stats::AllocStats;
use tinymist_analysis::syntax::classify_def_loosely;
@ -39,7 +40,7 @@ use crate::analysis::{
};
use crate::docs::{DefDocs, TidyModuleDocs};
use crate::syntax::{
Decl, DefKind, ExprInfo, ExprRoute, LexicalScope, ModuleDependency, SyntaxClass,
Decl, DefKind, Expr, ExprInfo, ExprRoute, LexicalScope, ModuleDependency, SyntaxClass,
classify_syntax, construct_module_dependencies, is_mark, resolve_id_by_path,
scan_workspace_files,
};
@ -828,10 +829,77 @@ impl SharedContext {
let guard = self.query_stat(source.id(), "lint");
self.slot.lint.compute(hash128(&(&ei, &ti, issues)), |_| {
guard.miss();
tinymist_lint::lint_file(self.world(), &ei, ti, issues.clone())
let cross_file_refs = self.compute_cross_file_references(source.id(), &ei);
let has_references = |decl: &Interned<Decl>| -> bool {
if ei
.get_refs(decl.clone())
.any(|(_, r)| r.as_ref().decl != *decl)
{
return true;
}
cross_file_refs.contains(decl)
};
tinymist_lint::lint_file(self.world(), &ei, ti, issues.clone(), has_references)
})
}
/// Computes which declarations from the current file are referenced by other files.
fn compute_cross_file_references(
self: &Arc<Self>,
current_file: TypstFileId,
current_ei: &ExprInfo,
) -> FxHashSet<Interned<Decl>> {
let mut referenced_decls = FxHashSet::default();
let files: Vec<_> = self.world().depended_files().into_iter().collect();
let mut all_decls = Vec::new();
for (_, expr) in current_ei.exports.iter() {
if let Expr::Decl(decl) = expr {
all_decls.push(decl.clone());
}
}
for (_, ref_expr) in current_ei.resolves.iter() {
all_decls.push(ref_expr.decl.clone());
}
for &file_id in &files {
if file_id == current_file {
continue;
}
let src = match self.source_by_id(file_id) {
Ok(src) => src,
Err(e) => {
log::debug!(
"failed to load source file {:?} for cross-file reference check: {:?}",
file_id,
e
);
continue;
}
};
let file_ei = self.expr_stage(&src);
for decl in &all_decls {
if file_ei
.get_refs(decl.clone())
.any(|(_, r)| r.as_ref().decl != *decl)
{
referenced_decls.insert(decl.clone());
}
}
}
referenced_decls
}
pub(crate) fn type_of_func(self: &Arc<Self>, func: Func) -> Signature {
crate::log_debug_ct!("convert runtime func {func:?}");
analyze_signature(self, SignatureTarget::Convert(func)).unwrap()

View file

@ -0,0 +1,12 @@
/// path: /main.typ
// Cross-module usage should count as a reference for dead code analysis.
#import "ieee.typ": ieee
#show: ieee
-----
/// path: /ieee.typ
/// compile: main.typ
#let ieee(body) = body

View file

@ -0,0 +1,7 @@
---
source: crates/tinymist-query/src/analysis.rs
assertion_line: 662
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/dead_code/cross_module_usage.typ
---
{}