diff --git a/crates/tinymist-lint/src/dead_code.rs b/crates/tinymist-lint/src/dead_code.rs index d6dee51d..79706536 100644 --- a/crates/tinymist-lint/src/dead_code.rs +++ b/crates/tinymist-lint/src/dead_code.rs @@ -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) -> 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); } diff --git a/crates/tinymist-lint/src/lib.rs b/crates/tinymist-lint/src/lib.rs index f3f72709..8c1fed86 100644 --- a/crates/tinymist-lint/src/lib.rs +++ b/crates/tinymist-lint/src/lib.rs @@ -41,10 +41,12 @@ pub fn lint_file( ei: &ExprInfo, ti: Arc, known_issues: KnownIssues, + has_references: impl Fn(&Interned) -> 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 { diff --git a/crates/tinymist-query/src/analysis/global.rs b/crates/tinymist-query/src/analysis/global.rs index d8886922..88b9c54d 100644 --- a/crates/tinymist-query/src/analysis/global.rs +++ b/crates/tinymist-query/src/analysis/global.rs @@ -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| -> 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, + current_file: TypstFileId, + current_ei: &ExprInfo, + ) -> FxHashSet> { + 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, func: Func) -> Signature { crate::log_debug_ct!("convert runtime func {func:?}"); analyze_signature(self, SignatureTarget::Convert(func)).unwrap() diff --git a/crates/tinymist-query/src/fixtures/dead_code/cross_module_usage.typ b/crates/tinymist-query/src/fixtures/dead_code/cross_module_usage.typ new file mode 100644 index 00000000..d210123a --- /dev/null +++ b/crates/tinymist-query/src/fixtures/dead_code/cross_module_usage.typ @@ -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 diff --git a/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@cross_module_usage.typ.snap b/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@cross_module_usage.typ.snap new file mode 100644 index 00000000..f76afdd5 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/dead_code/snaps/dead_code@cross_module_usage.typ.snap @@ -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 +--- +{}