From 9d0308febcff56fa2ac8f4e62e1738c570ed54f4 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Mon, 15 Dec 2025 21:28:57 +0800 Subject: [PATCH] fix(lint): refine dead-code exported handling - Treat module-level exported symbols as exported when configured - De-duplicate decl diagnostics across scopes - Replace docstring suppression with a softer hint --- crates/tinymist-lint/src/dead_code.rs | 28 ++++++++++--- .../tinymist-lint/src/dead_code/diagnostic.rs | 39 +++++-------------- crates/tinymist-lint/src/lib.rs | 5 +++ 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/crates/tinymist-lint/src/dead_code.rs b/crates/tinymist-lint/src/dead_code.rs index 7d5a1d605..668389650 100644 --- a/crates/tinymist-lint/src/dead_code.rs +++ b/crates/tinymist-lint/src/dead_code.rs @@ -68,20 +68,31 @@ pub fn check_dead_code( module_used_decls, } = compute_import_usage(&definitions, ei); - let mut seen_module_aliases = HashSet::new(); + let mut seen_decls = HashSet::new(); for def_info in definitions { - if matches!(def_info.decl.as_ref(), Decl::ModuleAlias(_)) - && !seen_module_aliases.insert(def_info.decl.clone()) + let def_info = if config.check_exported + && matches!(def_info.scope, DefScope::File) + && is_exported_symbol_candidate(def_info.decl.as_ref()) + && ei.is_exported(&def_info.decl) { - continue; - } + DefInfo { + scope: DefScope::Exported, + ..def_info + } + } else { + def_info + }; + if shadowed.contains(&def_info.decl) { continue; } if should_skip_definition(&def_info, config) { continue; } + if !seen_decls.insert(def_info.decl.clone()) { + continue; + } let is_unused = match def_info.decl.as_ref() { Decl::Import(_) | Decl::ImportAlias(_) => !used.contains(&def_info.decl), @@ -213,6 +224,13 @@ fn compute_import_usage(definitions: &[DefInfo], ei: &ExprInfo) -> ImportUsageIn } } +fn is_exported_symbol_candidate(decl: &Decl) -> bool { + matches!( + decl, + Decl::Func(_) | Decl::Var(_) | Decl::Module(_) | Decl::Closure(_) + ) +} + fn is_wildcard_module_import_decl(ei: &ExprInfo, decl: &Interned) -> bool { let span = decl.span(); if span.is_detached() { diff --git a/crates/tinymist-lint/src/dead_code/diagnostic.rs b/crates/tinymist-lint/src/dead_code/diagnostic.rs index 47ac8554f..2f849e94b 100644 --- a/crates/tinymist-lint/src/dead_code/diagnostic.rs +++ b/crates/tinymist-lint/src/dead_code/diagnostic.rs @@ -6,11 +6,11 @@ use tinymist_analysis::syntax::{Decl, DefKind, ExprInfo}; use tinymist_project::LspWorld; use typst::diag::{SourceDiagnostic, eco_format}; -use typst::syntax::ast::AstNode; -use typst::syntax::{LinkedNode, Span, ast}; use super::collector::{DefInfo, DefScope}; +use crate::DOCUMENTED_EXPORTED_FUNCTION_HINT; + /// Generates a diagnostic for an unused definition. /// /// Creates a warning with contextual information about the unused symbol, @@ -51,14 +51,12 @@ pub fn generate_diagnostic( } // Create the base diagnostic - let highlight_span = binding_span(def_info, ei).unwrap_or(def_info.span); - let mut diag = if is_module_import { - SourceDiagnostic::warning(highlight_span, eco_format!("unused module import")) + SourceDiagnostic::warning(def_info.span, eco_format!("unused module import")) } else if is_import_item { - SourceDiagnostic::warning(highlight_span, eco_format!("unused import: `{name}`")) + SourceDiagnostic::warning(def_info.span, eco_format!("unused import: `{name}`")) } else { - SourceDiagnostic::warning(highlight_span, eco_format!("unused {kind_str}: `{name}`")) + SourceDiagnostic::warning(def_info.span, eco_format!("unused {kind_str}: `{name}`")) }; // Add helpful hints based on the scope and kind @@ -84,12 +82,13 @@ pub fn generate_diagnostic( // Add kind-specific hints if let DefKind::Function = def_info.kind { - // Check if there's a docstring - documented functions might be intentional API if matches!(def_info.scope, DefScope::Exported) && ei.docstrings.contains_key(&def_info.decl) { - // Reduce severity for documented functions (they might be public API) - return None; + diag = diag.with_hint(DOCUMENTED_EXPORTED_FUNCTION_HINT); + diag = diag.with_hint( + "if this is intended public API, you can ignore this diagnostic; otherwise consider removing it", + ); } } @@ -99,23 +98,3 @@ pub fn generate_diagnostic( Some(diag) } - -fn binding_span(def_info: &DefInfo, ei: &ExprInfo) -> Option { - if !matches!(def_info.kind, DefKind::Variable | DefKind::Constant) { - return None; - } - if !matches!(def_info.scope, DefScope::File | DefScope::Local) { - return None; - } - - let node = LinkedNode::new(ei.source.root()).find(def_info.span)?; - let mut current = Some(node); - while let Some(node) = current { - if let Some(binding) = node.cast::() { - return Some(binding.span()); - } - current = node.parent().cloned(); - } - - None -} diff --git a/crates/tinymist-lint/src/lib.rs b/crates/tinymist-lint/src/lib.rs index d91391808..7640ac276 100644 --- a/crates/tinymist-lint/src/lib.rs +++ b/crates/tinymist-lint/src/lib.rs @@ -4,6 +4,11 @@ mod dead_code; pub use dead_code::DeadCodeConfig; +/// Hint added to diagnostics for documented exported functions, to mark them as +/// likely public API. +pub const DOCUMENTED_EXPORTED_FUNCTION_HINT: &str = + "this function is exported and documented; it may be part of the public API"; + use std::sync::Arc; use tinymist_analysis::{