lint: highlight entire unused let bindings and keep code actions working

This commit is contained in:
Hong Jiarong 2025-12-09 16:53:45 +08:00
parent ef76937bb7
commit c398ef6316
35 changed files with 117 additions and 108 deletions

View file

@ -6,6 +6,8 @@
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};
@ -49,12 +51,14 @@ 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(def_info.span, eco_format!("unused module import"))
SourceDiagnostic::warning(highlight_span, eco_format!("unused module import"))
} else if is_import_item {
SourceDiagnostic::warning(def_info.span, eco_format!("unused import: `{name}`"))
SourceDiagnostic::warning(highlight_span, eco_format!("unused import: `{name}`"))
} else {
SourceDiagnostic::warning(def_info.span, eco_format!("unused {kind_str}: `{name}`"))
SourceDiagnostic::warning(highlight_span, eco_format!("unused {kind_str}: `{name}`"))
};
// Add helpful hints based on the scope and kind
@ -95,3 +99,23 @@ 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

@ -94,9 +94,15 @@ impl<'a> CodeActionWorker<'a> {
if diag.message.starts_with("unused import:") {
self.autofix_remove_unused_import(root, &diag_range);
} else {
self.autofix_unused_symbol(&diag_range);
self.autofix_replace_with_placeholder(root, &diag_range);
self.autofix_remove_declaration(root, &diag_range);
let Some(binding_range) =
self.binding_range_for_diag(root, &diag_range, diag)
else {
continue;
};
self.autofix_unused_symbol(&binding_range);
self.autofix_replace_with_placeholder(root, &binding_range);
self.autofix_remove_declaration(root, &binding_range);
}
}
_ => {}
@ -507,6 +513,41 @@ impl<'a> CodeActionWorker<'a> {
.cloned()
}
fn binding_range_for_diag(
&mut self,
root: &LinkedNode<'_>,
diag_range: &Range<usize>,
diag: &lsp_types::Diagnostic,
) -> Option<Range<usize>> {
if diag_range.is_empty() {
return None;
}
if let Some(text) = self.source.text().get(diag_range.clone()) {
if is_plain_identifier(text) {
return Some(diag_range.clone());
}
}
let name = extract_backticked_name(&diag.message)?;
let cursor = (diag_range.start + 1).min(self.source.text().len());
let node = root.leaf_at_compat(cursor)?;
let decl_node = self.find_declaration_ancestor(&node)?;
if decl_node.kind() == SyntaxKind::LetBinding {
let let_binding = decl_node.cast::<ast::LetBinding>()?;
for binding in let_binding.kind().bindings() {
let binding_node = decl_node.find(binding.span())?;
let range = binding_node.range();
if self.source.text().get(range.clone())? == name {
return Some(range);
}
}
}
None
}
fn expand_import_item_range(&self, mut range: Range<usize>) -> Range<usize> {
let bytes = self.source.text().as_bytes();
let len = bytes.len();
@ -994,6 +1035,13 @@ fn match_autofix_kind(source: &str, msg: &str) -> Option<AutofixKind> {
None
}
fn extract_backticked_name(message: &str) -> Option<&str> {
let start = message.find('`')?;
let rest = &message[start + 1..];
let end = rest.find('`')?;
Some(&rest[..end])
}
fn is_ascii_ident(ch: u8) -> bool {
matches!(ch, b'_' | b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9')
}

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/array_dict_usage.typ
"s0.typ": [
{
"message": "unused variable: `unused`\nHint: consider removing this variable or prefixing with underscore: `_unused`",
"range": "3:5:3:11",
"range": "3:1:3:15",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/closure_capture.typ
"s0.typ": [
{
"message": "unused variable: `unused_not_captured`\nHint: consider removing this variable or prefixing with underscore: `_unused_not_captured`",
"range": "2:5:2:24",
"range": "2:1:2:30",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/conditional_usage.typ
"s0.typ": [
{
"message": "unused variable: `unused`\nHint: consider removing this variable or prefixing with underscore: `_unused`",
"range": "4:5:4:11",
"range": "4:1:4:16",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/contextual_usage.typ
"s0.typ": [
{
"message": "unused variable: `unused_ctx`\nHint: consider removing this variable or prefixing with underscore: `_unused_ctx`",
"range": "2:5:2:15",
"range": "2:1:2:20",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_array.typ
"s0.typ": [
{
"message": "unused variable: `unused_x`\nHint: consider removing this variable or prefixing with underscore: `_unused_x`",
"range": "6:15:6:23",
"range": "6:1:6:46",
"severity": 4,
"source": "typst",
"tags": [
@ -16,7 +16,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_array.typ
},
{
"message": "unused variable: `c2`\nHint: consider removing this variable or prefixing with underscore: `_c2`",
"range": "6:31:6:33",
"range": "6:1:6:46",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_dict.typ
"s0.typ": [
{
"message": "unused variable: `unused_x`\nHint: consider removing this variable or prefixing with underscore: `_unused_x`",
"range": "6:18:6:26",
"range": "6:1:6:52",
"severity": 4,
"source": "typst",
"tags": [
@ -16,7 +16,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_dict.typ
},
{
"message": "unused variable: `c2`\nHint: consider removing this variable or prefixing with underscore: `_c2`",
"range": "6:37:6:39",
"range": "6:1:6:52",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_spread.ty
"s0.typ": [
{
"message": "unused variable: `unused_rest`\nHint: consider removing this variable or prefixing with underscore: `_unused_rest`",
"range": "4:11:4:22",
"range": "4:1:4:30",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/method_chain.typ
"s0.typ": [
{
"message": "unused variable: `unused_obj`\nHint: consider removing this variable or prefixing with underscore: `_unused_obj`",
"range": "8:5:8:15",
"range": "8:1:8:24",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/multiple_unused.typ
"s0.typ": [
{
"message": "unused variable: `unused1`\nHint: consider removing this variable or prefixing with underscore: `_unused1`",
"range": "1:5:1:12",
"range": "1:1:1:16",
"severity": 4,
"source": "typst",
"tags": [
@ -16,7 +16,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/multiple_unused.typ
},
{
"message": "unused variable: `unused2`\nHint: consider removing this variable or prefixing with underscore: `_unused2`",
"range": "2:5:2:12",
"range": "2:1:2:16",
"severity": 4,
"source": "typst",
"tags": [
@ -25,7 +25,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/multiple_unused.typ
},
{
"message": "unused variable: `unused3`\nHint: consider removing this variable or prefixing with underscore: `_unused3`",
"range": "4:5:4:12",
"range": "4:1:4:16",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/nested_scope.typ
"s0.typ": [
{
"message": "unused variable: `inner_unused`\nHint: consider removing this variable or prefixing with underscore: `_inner_unused`",
"range": "4:6:4:18",
"range": "4:2:4:22",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/pattern_destructure.typ
"s0.typ": [
{
"message": "unused variable: `unused_b`\nHint: consider removing this variable or prefixing with underscore: `_unused_b`",
"range": "1:14:1:22",
"range": "1:1:1:43",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/shadowing.typ
"s0.typ": [
{
"message": "unused variable: `x`\nHint: consider removing this variable or prefixing with underscore: `_x`",
"range": "1:5:1:6",
"range": "1:1:1:10",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/show_set_rules.typ
"s0.typ": [
{
"message": "unused variable: `unused_style`\nHint: consider removing this variable or prefixing with underscore: `_unused_style`",
"range": "1:5:1:17",
"range": "1:1:1:23",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/underscore_prefix.typ
"s0.typ": [
{
"message": "unused variable: `normal_unused`\nHint: consider removing this variable or prefixing with underscore: `_normal_unused`",
"range": "4:5:4:18",
"range": "4:1:4:24",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -7,7 +7,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/unused_variable.typ
"s0.typ": [
{
"message": "unused variable: `unused_var`\nHint: consider removing this variable or prefixing with underscore: `_unused_var`",
"range": "1:5:1:15",
"range": "1:1:1:20",
"severity": 4,
"source": "typst",
"tags": [

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/array_dict_usage.typ
}
],
"message": "unused variable: `unused`\nHint: consider removing this variable or prefixing with underscore: `_unused`",
"range": "3:5:3:11"
"range": "3:1:3:15"
}
]

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/closure_capture.typ
}
],
"message": "unused variable: `unused_not_captured`\nHint: consider removing this variable or prefixing with underscore: `_unused_not_captured`",
"range": "2:5:2:24"
"range": "2:1:2:30"
}
]

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/conditional_usage.typ
}
],
"message": "unused variable: `unused`\nHint: consider removing this variable or prefixing with underscore: `_unused`",
"range": "4:5:4:11"
"range": "4:1:4:16"
}
]

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/contextual_usage.typ
}
],
"message": "unused variable: `unused_ctx`\nHint: consider removing this variable or prefixing with underscore: `_unused_ctx`",
"range": "2:5:2:15"
"range": "2:1:2:20"
}
]

View file

@ -1,6 +1,5 @@
---
source: crates/tinymist-query/src/code_action.rs
assertion_line: 180
description: Dead code code actions in /dummy-root/s0.typ
expression: "JsonRepr::new_pure(ordered_entries)"
input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_array.typ
@ -40,7 +39,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_array.typ
}
],
"message": "unused variable: `unused_x`\nHint: consider removing this variable or prefixing with underscore: `_unused_x`",
"range": "6:15:6:23"
"range": "6:1:6:46"
},
{
"actions": [
@ -76,6 +75,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_array.typ
}
],
"message": "unused variable: `c2`\nHint: consider removing this variable or prefixing with underscore: `_c2`",
"range": "6:31:6:33"
"range": "6:1:6:46"
}
]

View file

@ -1,6 +1,5 @@
---
source: crates/tinymist-query/src/code_action.rs
assertion_line: 180
description: Dead code code actions in /dummy-root/s0.typ
expression: "JsonRepr::new_pure(ordered_entries)"
input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_dict.typ
@ -40,7 +39,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_dict.typ
}
],
"message": "unused variable: `unused_x`\nHint: consider removing this variable or prefixing with underscore: `_unused_x`",
"range": "6:18:6:26"
"range": "6:1:6:52"
},
{
"actions": [
@ -76,6 +75,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_dict.typ
}
],
"message": "unused variable: `c2`\nHint: consider removing this variable or prefixing with underscore: `_c2`",
"range": "6:37:6:39"
"range": "6:1:6:52"
}
]

View file

@ -24,6 +24,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/destructuring_spread.ty
}
],
"message": "unused variable: `unused_rest`\nHint: consider removing this variable or prefixing with underscore: `_unused_rest`",
"range": "4:11:4:22"
"range": "4:1:4:30"
}
]

View file

@ -7,21 +7,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/import_module_unused.ty
[
{
"actions": [
{
"edit": {
"changes": {
"main.typ": [
{
"insertTextFormat": 1,
"newText": "",
"range": "2:0:3:0"
}
]
}
},
"kind": "quickfix",
"title": "Remove unused declaration"
},
{
"edit": {
"changes": {

View file

@ -5,27 +5,6 @@ expression: "JsonRepr::new_pure(ordered_entries)"
input_file: crates/tinymist-query/src/fixtures/dead_code/imported_unused.typ
---
[
{
"actions": [
{
"edit": {
"changes": {
"main.typ": [
{
"insertTextFormat": 1,
"newText": "",
"range": "2:0:3:0"
}
]
}
},
"kind": "quickfix",
"title": "Remove unused declaration"
}
],
"message": "unused module import\nHint: imported modules should be used or the import should be removed",
"range": "2:1:2:33"
},
{
"actions": [
{

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/method_chain.typ
}
],
"message": "unused variable: `unused_obj`\nHint: consider removing this variable or prefixing with underscore: `_unused_obj`",
"range": "8:5:8:15"
"range": "8:1:8:24"
}
]

View file

@ -1,30 +1,7 @@
---
source: crates/tinymist-query/src/code_action.rs
assertion_line: 183
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/module_item_wildcard_unused.typ
---
[
{
"actions": [
{
"edit": {
"changes": {
"main.typ": [
{
"insertTextFormat": 1,
"newText": "",
"range": "2:0:3:0"
}
]
}
},
"kind": "quickfix",
"title": "Remove unused declaration"
}
],
"message": "unused module import\nHint: imported modules should be used or the import should be removed",
"range": "2:1:2:18"
}
]
[]

View file

@ -54,7 +54,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/multiple_unused.typ
}
],
"message": "unused variable: `unused1`\nHint: consider removing this variable or prefixing with underscore: `_unused1`",
"range": "1:5:1:12"
"range": "1:1:1:16"
},
{
"actions": [
@ -105,7 +105,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/multiple_unused.typ
}
],
"message": "unused variable: `unused2`\nHint: consider removing this variable or prefixing with underscore: `_unused2`",
"range": "2:5:2:12"
"range": "2:1:2:16"
},
{
"actions": [
@ -156,7 +156,7 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/multiple_unused.typ
}
],
"message": "unused variable: `unused3`\nHint: consider removing this variable or prefixing with underscore: `_unused3`",
"range": "4:5:4:12"
"range": "4:1:4:16"
},
{
"actions": [

View file

@ -1,6 +1,5 @@
---
source: crates/tinymist-query/src/code_action.rs
assertion_line: 180
description: Dead code code actions in /dummy-root/s0.typ
expression: "JsonRepr::new_pure(ordered_entries)"
input_file: crates/tinymist-query/src/fixtures/dead_code/nested_scope.typ
@ -55,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/nested_scope.typ
}
],
"message": "unused variable: `inner_unused`\nHint: consider removing this variable or prefixing with underscore: `_inner_unused`",
"range": "4:6:4:18"
"range": "4:2:4:22"
}
]

View file

@ -1,6 +1,5 @@
---
source: crates/tinymist-query/src/code_action.rs
assertion_line: 180
description: Dead code code actions in /dummy-root/s0.typ
expression: "JsonRepr::new_pure(ordered_entries)"
input_file: crates/tinymist-query/src/fixtures/dead_code/pattern_destructure.typ
@ -40,6 +39,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/pattern_destructure.typ
}
],
"message": "unused variable: `unused_b`\nHint: consider removing this variable or prefixing with underscore: `_unused_b`",
"range": "1:14:1:22"
"range": "1:1:1:43"
}
]

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/shadowing.typ
}
],
"message": "unused variable: `x`\nHint: consider removing this variable or prefixing with underscore: `_x`",
"range": "1:5:1:6"
"range": "1:1:1:10"
}
]

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/show_set_rules.typ
}
],
"message": "unused variable: `unused_style`\nHint: consider removing this variable or prefixing with underscore: `_unused_style`",
"range": "1:5:1:17"
"range": "1:1:1:23"
}
]

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/underscore_prefix.typ
}
],
"message": "unused variable: `normal_unused`\nHint: consider removing this variable or prefixing with underscore: `_normal_unused`",
"range": "4:5:4:18"
"range": "4:1:4:24"
}
]

View file

@ -54,6 +54,6 @@ input_file: crates/tinymist-query/src/fixtures/dead_code/unused_variable.typ
}
],
"message": "unused variable: `unused_var`\nHint: consider removing this variable or prefixing with underscore: `_unused_var`",
"range": "1:5:1:15"
"range": "1:1:1:20"
}
]