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
This commit is contained in:
Hong Jiarong 2025-12-15 21:28:57 +08:00
parent 8d0361735e
commit 9d0308febc
3 changed files with 37 additions and 35 deletions

View file

@ -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<Decl>) -> bool {
let span = decl.span();
if span.is_detached() {

View file

@ -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<Span> {
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::<ast::LetBinding>() {
return Some(binding.span());
}
current = node.parent().cloned();
}
None
}

View file

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