use super::functions::*;
use crate::lsp::{analyze_document, Diagnostic, DiagnosticSeverity, Document, Range, TextEdit};
use oxilean_kernel::{Environment, Name};
use oxilean_parse::{Lexer, TokenKind};
use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct ExtendedDiagnostic {
pub rich: RichDiagnostic,
pub priority: DiagnosticPriority,
pub fix_count: usize,
pub auto_fixable: bool,
pub tags: Vec<String>,
}
#[allow(dead_code)]
impl ExtendedDiagnostic {
pub fn new(rich: RichDiagnostic) -> Self {
let priority = match rich.diagnostic.severity {
DiagnosticSeverity::Error => DiagnosticPriority::High,
DiagnosticSeverity::Warning => DiagnosticPriority::Normal,
_ => DiagnosticPriority::Low,
};
Self {
rich,
priority,
fix_count: 0,
auto_fixable: false,
tags: Vec::new(),
}
}
pub fn with_fix_count(mut self, count: usize) -> Self {
self.fix_count = count;
self.auto_fixable = count > 0;
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_priority(mut self, priority: DiagnosticPriority) -> Self {
self.priority = priority;
self
}
pub fn has_tag(&self, tag: &str) -> bool {
self.tags.iter().any(|t| t == tag)
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct DiagnosticSubscription {
pub uri_pattern: Option<String>,
pub min_severity: DiagnosticSeverity,
}
impl DiagnosticSubscription {
#[allow(dead_code)]
pub fn all() -> Self {
Self {
uri_pattern: None,
min_severity: DiagnosticSeverity::Hint,
}
}
#[allow(dead_code)]
pub fn errors_and_warnings(uri: impl Into<String>) -> Self {
Self {
uri_pattern: Some(uri.into()),
min_severity: DiagnosticSeverity::Warning,
}
}
#[allow(dead_code)]
pub fn matches(&self, uri: &str, diag: &Diagnostic) -> bool {
if let Some(ref pattern) = self.uri_pattern {
if !uri.contains(pattern.as_str()) {
return false;
}
}
let rank = |s: &DiagnosticSeverity| match s {
DiagnosticSeverity::Error => 0,
DiagnosticSeverity::Warning => 1,
DiagnosticSeverity::Information => 2,
DiagnosticSeverity::Hint => 3,
};
rank(&diag.severity) <= rank(&self.min_severity)
}
}
#[allow(dead_code)]
pub struct DiagnosticCache {
entries: std::collections::HashMap<String, (String, Vec<Diagnostic>)>,
max_size: usize,
}
impl DiagnosticCache {
#[allow(dead_code)]
pub fn new(max_size: usize) -> Self {
Self {
entries: std::collections::HashMap::new(),
max_size,
}
}
fn key(uri: &str, version: &str) -> String {
format!("{}:{}", uri, version)
}
#[allow(dead_code)]
pub fn store(&mut self, uri: String, version: String, diags: Vec<Diagnostic>) {
if self.entries.len() >= self.max_size {
let first = self.entries.keys().next().cloned();
if let Some(k) = first {
self.entries.remove(&k);
}
}
let k = Self::key(&uri, &version);
self.entries.insert(k, (uri, diags));
}
#[allow(dead_code)]
pub fn get(&self, uri: &str, version: &str) -> Option<&Vec<Diagnostic>> {
let k = Self::key(uri, version);
self.entries.get(&k).map(|(_, d)| d)
}
#[allow(dead_code)]
pub fn invalidate_uri(&mut self, uri: &str) {
self.entries.retain(|_, (u, _)| u != uri);
}
}
#[derive(Clone, Debug)]
pub struct QuickFix {
pub title: String,
pub edits: Vec<TextEdit>,
pub diagnostic: Diagnostic,
}
#[derive(Clone, Debug, Default)]
pub struct DiagnosticFilter {
pub min_severity: Option<DiagnosticSeverity>,
pub suppressed_codes: Vec<DiagnosticCode>,
pub max_count: Option<usize>,
}
impl DiagnosticFilter {
pub fn accept_all() -> Self {
Self::default()
}
pub fn errors_only() -> Self {
Self {
min_severity: Some(DiagnosticSeverity::Error),
..Default::default()
}
}
pub fn suppress(mut self, code: DiagnosticCode) -> Self {
self.suppressed_codes.push(code);
self
}
pub fn limit(mut self, n: usize) -> Self {
self.max_count = Some(n);
self
}
pub fn accepts(&self, d: &RichDiagnostic) -> bool {
if self.suppressed_codes.contains(&d.code) {
return false;
}
if let Some(min) = &self.min_severity {
if &d.diagnostic.severity > min {
return false;
}
}
true
}
pub fn apply<'a>(&self, diagnostics: &'a [RichDiagnostic]) -> Vec<&'a RichDiagnostic> {
let filtered: Vec<&'a RichDiagnostic> =
diagnostics.iter().filter(|d| self.accepts(d)).collect();
match self.max_count {
Some(n) => filtered.into_iter().take(n).collect(),
None => filtered,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct DiagnosticBatch {
pub items: Vec<RichDiagnostic>,
}
impl DiagnosticBatch {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, d: RichDiagnostic) {
self.items.push(d);
}
pub fn error_count(&self) -> usize {
self.items
.iter()
.filter(|d| d.diagnostic.severity == DiagnosticSeverity::Error)
.count()
}
pub fn warning_count(&self) -> usize {
self.items
.iter()
.filter(|d| d.diagnostic.severity == DiagnosticSeverity::Warning)
.count()
}
pub fn has_errors(&self) -> bool {
self.error_count() > 0
}
pub fn filter(&self, f: &DiagnosticFilter) -> Vec<&RichDiagnostic> {
f.apply(&self.items)
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum DiagnosticPriority {
Low = 0,
Normal = 1,
High = 2,
Critical = 3,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default)]
pub struct DiagnosticAggregator {
errors: std::collections::HashMap<String, usize>,
warnings: std::collections::HashMap<String, usize>,
pub total_decls: usize,
}
#[allow(dead_code)]
impl DiagnosticAggregator {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, uri: &str, diagnostics: &[RichDiagnostic]) {
let errors = diagnostics
.iter()
.filter(|d| d.diagnostic.severity == DiagnosticSeverity::Error)
.count();
let warnings = diagnostics
.iter()
.filter(|d| d.diagnostic.severity == DiagnosticSeverity::Warning)
.count();
*self.errors.entry(uri.to_string()).or_insert(0) += errors;
*self.warnings.entry(uri.to_string()).or_insert(0) += warnings;
}
pub fn total_errors(&self) -> usize {
self.errors.values().sum()
}
pub fn total_warnings(&self) -> usize {
self.warnings.values().sum()
}
pub fn files_with_errors(&self) -> Vec<&str> {
self.errors
.iter()
.filter(|(_, &c)| c > 0)
.map(|(uri, _)| uri.as_str())
.collect()
}
pub fn worst_file(&self) -> Option<(&str, usize)> {
self.errors
.iter()
.max_by_key(|(_, &c)| c)
.map(|(uri, &c)| (uri.as_str(), c))
}
pub fn summary(&self) -> String {
format!(
"{} file(s), {} error(s), {} warning(s), {} declaration(s) checked",
self.errors.len(),
self.total_errors(),
self.total_warnings(),
self.total_decls
)
}
}
#[derive(Clone, Debug)]
pub struct RichDiagnostic {
pub diagnostic: Diagnostic,
pub code: DiagnosticCode,
pub related: Vec<RelatedInfo>,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct DiagnosticThreshold {
pub promote_info_to_warning: bool,
pub promote_warnings_to_errors: bool,
pub demote_errors_to_warnings: bool,
}
impl DiagnosticThreshold {
#[allow(dead_code)]
pub fn apply(&self, severity: DiagnosticSeverity) -> DiagnosticSeverity {
match severity {
DiagnosticSeverity::Information if self.promote_info_to_warning => {
DiagnosticSeverity::Warning
}
DiagnosticSeverity::Warning if self.promote_warnings_to_errors => {
DiagnosticSeverity::Error
}
DiagnosticSeverity::Error if self.demote_errors_to_warnings => {
DiagnosticSeverity::Warning
}
other => other,
}
}
}
#[allow(dead_code)]
pub struct DiagnosticDiffTracker {
previous: Vec<String>,
}
impl DiagnosticDiffTracker {
#[allow(dead_code)]
pub fn new() -> Self {
Self { previous: vec![] }
}
fn key(diag: &Diagnostic) -> String {
format!(
"{}:{}:{}",
diag.range.start.line, diag.range.start.character, diag.message
)
}
#[allow(dead_code)]
pub fn update(&mut self, current: &[Diagnostic]) -> (usize, usize) {
let current_keys: std::collections::HashSet<String> =
current.iter().map(Self::key).collect();
let previous_keys: std::collections::HashSet<String> =
self.previous.iter().cloned().collect();
let new_count = current_keys.difference(&previous_keys).count();
let resolved_count = previous_keys.difference(¤t_keys).count();
self.previous = current_keys.into_iter().collect();
(new_count, resolved_count)
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DiagnosticOutputFormat {
Text,
Json,
Compact,
Annotated,
}
#[allow(dead_code)]
pub struct DiagnosticWorkspaceAggregator {
per_file: std::collections::HashMap<String, Vec<Diagnostic>>,
}
impl DiagnosticWorkspaceAggregator {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
per_file: std::collections::HashMap::new(),
}
}
#[allow(dead_code)]
pub fn set_for_file(&mut self, uri: String, diags: Vec<Diagnostic>) {
self.per_file.insert(uri, diags);
}
#[allow(dead_code)]
pub fn clear_file(&mut self, uri: &str) {
self.per_file.remove(uri);
}
#[allow(dead_code)]
pub fn total_errors(&self) -> usize {
self.per_file
.values()
.flat_map(|diags| diags.iter())
.filter(|d| matches!(d.severity, DiagnosticSeverity::Error))
.count()
}
#[allow(dead_code)]
pub fn worst_file(&self) -> Option<&str> {
self.per_file
.iter()
.max_by_key(|(_, diags)| diags.len())
.map(|(uri, _)| uri.as_str())
}
}
#[allow(dead_code)]
pub struct DiagnosticBudget {
pub max_errors: usize,
pub max_warnings: usize,
pub max_total: usize,
}
impl DiagnosticBudget {
#[allow(dead_code)]
pub fn apply(&self, diagnostics: Vec<Diagnostic>) -> (Vec<Diagnostic>, usize) {
let truncated = diagnostics.len().saturating_sub(self.max_total);
let trimmed: Vec<Diagnostic> = diagnostics.into_iter().take(self.max_total).collect();
(trimmed, truncated)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DiagnosticCode {
LexError,
ParseError,
TypeError,
UnusedVariable,
Shadowing,
Deprecation,
UnresolvedName,
MissingImport,
RedundantImport,
StyleWarning,
}
impl DiagnosticCode {
pub fn as_str(self) -> &'static str {
match self {
DiagnosticCode::LexError => "E001",
DiagnosticCode::ParseError => "E002",
DiagnosticCode::TypeError => "E003",
DiagnosticCode::UnusedVariable => "W001",
DiagnosticCode::Shadowing => "W002",
DiagnosticCode::Deprecation => "W003",
DiagnosticCode::UnresolvedName => "E004",
DiagnosticCode::MissingImport => "E005",
DiagnosticCode::RedundantImport => "W004",
DiagnosticCode::StyleWarning => "W005",
}
}
pub fn description(self) -> &'static str {
match self {
DiagnosticCode::LexError => "lexer error",
DiagnosticCode::ParseError => "parse error",
DiagnosticCode::TypeError => "type error",
DiagnosticCode::UnusedVariable => "unused variable",
DiagnosticCode::Shadowing => "shadowing",
DiagnosticCode::Deprecation => "deprecation",
DiagnosticCode::UnresolvedName => "unresolved name",
DiagnosticCode::MissingImport => "missing import",
DiagnosticCode::RedundantImport => "redundant import",
DiagnosticCode::StyleWarning => "style warning",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CodeActionKind {
QuickFix,
Refactor,
Source,
RefactorExtract,
RefactorInline,
}
impl CodeActionKind {
pub fn as_str(&self) -> &str {
match self {
CodeActionKind::QuickFix => "quickfix",
CodeActionKind::Refactor => "refactor",
CodeActionKind::Source => "source",
CodeActionKind::RefactorExtract => "refactor.extract",
CodeActionKind::RefactorInline => "refactor.inline",
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct InlineAnnotation {
pub line: u32,
pub message: String,
pub severity: DiagnosticSeverity,
}
#[allow(dead_code)]
pub struct DiagnosticRateLimiter {
counts: std::collections::HashMap<String, usize>,
limit: usize,
}
#[allow(dead_code)]
impl DiagnosticRateLimiter {
pub fn new(limit: usize) -> Self {
Self {
counts: std::collections::HashMap::new(),
limit,
}
}
pub fn allow(&mut self, uri: &str) -> bool {
let count = self.counts.entry(uri.to_string()).or_insert(0);
if *count < self.limit {
*count += 1;
true
} else {
false
}
}
pub fn reset(&mut self, uri: &str) {
self.counts.remove(uri);
}
pub fn reset_all(&mut self) {
self.counts.clear();
}
pub fn count_for(&self, uri: &str) -> usize {
self.counts.get(uri).copied().unwrap_or(0)
}
}
#[allow(dead_code)]
pub struct DiagnosticEnricher {
pub add_source_snippets: bool,
pub add_fix_hints: bool,
}
impl DiagnosticEnricher {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
add_source_snippets: true,
add_fix_hints: true,
}
}
#[allow(dead_code)]
pub fn enrich_message(&self, code: DiagnosticCode, message: &str) -> String {
let hint = if self.add_fix_hints {
match code {
DiagnosticCode::UnusedVariable => " [hint: prefix with _ to suppress]",
DiagnosticCode::TypeError => " [hint: check type annotations]",
DiagnosticCode::UnresolvedName => " [hint: check imports or spelling]",
_ => "",
}
} else {
""
};
format!("{}{}", message, hint)
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default)]
pub struct DiagnosticReport {
pub uri: String,
pub groups: Vec<DiagnosticGroup>,
pub total_errors: usize,
pub total_warnings: usize,
}
#[allow(dead_code)]
impl DiagnosticReport {
pub fn new(uri: impl Into<String>) -> Self {
Self {
uri: uri.into(),
groups: Vec::new(),
total_errors: 0,
total_warnings: 0,
}
}
pub fn add_group(&mut self, group: DiagnosticGroup) {
self.total_errors += group.error_count();
self.total_warnings += group.warning_count();
self.groups.push(group);
}
pub fn all_diagnostics(&self) -> Vec<&RichDiagnostic> {
self.groups.iter().flat_map(|g| g.items.iter()).collect()
}
pub fn is_clean(&self) -> bool {
self.total_errors == 0 && self.total_warnings == 0
}
pub fn summary(&self) -> String {
if self.is_clean() {
return format!("{}: no issues", self.uri);
}
format!(
"{}: {} error(s), {} warning(s)",
self.uri, self.total_errors, self.total_warnings
)
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default)]
pub struct DiagnosticGroup {
pub label: String,
pub items: Vec<RichDiagnostic>,
}
#[allow(dead_code)]
impl DiagnosticGroup {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
items: Vec::new(),
}
}
pub fn add(&mut self, d: RichDiagnostic) {
self.items.push(d);
}
pub fn error_count(&self) -> usize {
self.items
.iter()
.filter(|d| d.diagnostic.severity == DiagnosticSeverity::Error)
.count()
}
pub fn warning_count(&self) -> usize {
self.items
.iter()
.filter(|d| d.diagnostic.severity == DiagnosticSeverity::Warning)
.count()
}
pub fn has_errors(&self) -> bool {
self.error_count() > 0
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
#[allow(dead_code)]
pub struct DiagnosticPipeline {
stages: Vec<DiagnosticPipelineStage>,
}
impl DiagnosticPipeline {
#[allow(dead_code)]
pub fn default_pipeline() -> Self {
Self {
stages: vec![
DiagnosticPipelineStage::new("collect"),
DiagnosticPipelineStage::new("deduplicate"),
DiagnosticPipelineStage::new("sort"),
DiagnosticPipelineStage::new("suppress"),
DiagnosticPipelineStage::new("enrich"),
DiagnosticPipelineStage::new("publish"),
],
}
}
#[allow(dead_code)]
pub fn stage_names(&self) -> Vec<&str> {
self.stages.iter().map(|s| s.name.as_str()).collect()
}
#[allow(dead_code)]
pub fn enabled_stages(&self) -> Vec<&DiagnosticPipelineStage> {
self.stages.iter().filter(|s| s.enabled).collect()
}
}
#[derive(Clone, Debug)]
pub struct CodeAction {
pub title: String,
pub kind: CodeActionKind,
pub diagnostics: Vec<Diagnostic>,
pub edit: Option<Vec<TextEdit>>,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct DiagnosticSnapshot {
pub uri: String,
pub diagnostics: Vec<(String, String, u32)>,
}
#[allow(dead_code)]
impl DiagnosticSnapshot {
pub fn capture(uri: &str, diagnostics: &[RichDiagnostic]) -> Self {
let entries = diagnostics
.iter()
.map(|d| {
(
d.code.as_str().to_string(),
d.diagnostic.message.clone(),
d.diagnostic.range.start.line,
)
})
.collect();
Self {
uri: uri.to_string(),
diagnostics: entries,
}
}
pub fn diff(&self, other: &Self) -> Vec<String> {
let mut diffs = Vec::new();
let self_set: std::collections::HashSet<_> = self.diagnostics.iter().collect();
let other_set: std::collections::HashSet<_> = other.diagnostics.iter().collect();
for item in self_set.difference(&other_set) {
diffs.push(format!(
"- removed: [{}] {} at line {}",
item.0, item.1, item.2
));
}
for item in other_set.difference(&self_set) {
diffs.push(format!(
"+ added: [{}] {} at line {}",
item.0, item.1, item.2
));
}
diffs
}
pub fn count_by_code(&self, code: &str) -> usize {
self.diagnostics
.iter()
.filter(|(c, _, _)| c == code)
.count()
}
}
#[derive(Clone, Debug)]
pub struct RelatedInfo {
pub message: String,
pub uri: String,
pub range: Range,
}
pub struct DiagnosticCollector<'a> {
env: &'a Environment,
max_diagnostics: usize,
}
impl<'a> DiagnosticCollector<'a> {
pub fn new(env: &'a Environment, max_diagnostics: usize) -> Self {
Self {
env,
max_diagnostics,
}
}
pub fn collect_diagnostics(&self, doc: &Document) -> Vec<RichDiagnostic> {
let mut diagnostics = Vec::new();
diagnostics.extend(self.collect_lex_errors(&doc.content));
diagnostics.extend(self.collect_parse_errors(&doc.content));
diagnostics.extend(self.collect_type_errors(doc));
diagnostics.extend(self.collect_warnings(doc));
diagnostics.truncate(self.max_diagnostics);
diagnostics
}
pub fn collect_lex_errors(&self, content: &str) -> Vec<RichDiagnostic> {
let mut diagnostics = Vec::new();
let mut lexer = Lexer::new(content);
let tokens = lexer.tokenize();
for token in &tokens {
if let TokenKind::Error(msg) = &token.kind {
let line = if token.span.line > 0 {
token.span.line as u32 - 1
} else {
0
};
let col = if token.span.column > 0 {
token.span.column as u32 - 1
} else {
0
};
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::error(
Range::single_line(line, col, col + 1),
format!("lexer error: {}", msg),
),
code: DiagnosticCode::LexError,
related: Vec::new(),
});
}
}
diagnostics
}
pub fn collect_parse_errors(&self, content: &str) -> Vec<RichDiagnostic> {
let mut diagnostics = Vec::new();
let mut lexer = Lexer::new(content);
let tokens = lexer.tokenize();
let mut paren_stack: Vec<(char, u32, u32)> = Vec::new();
for token in &tokens {
let line = if token.span.line > 0 {
token.span.line as u32 - 1
} else {
0
};
let col = if token.span.column > 0 {
token.span.column as u32 - 1
} else {
0
};
match &token.kind {
TokenKind::LParen => paren_stack.push(('(', line, col)),
TokenKind::LBracket => paren_stack.push(('[', line, col)),
TokenKind::LBrace => paren_stack.push(('{', line, col)),
TokenKind::RParen => {
if let Some((ch, _, _)) = paren_stack.last() {
if *ch == '(' {
paren_stack.pop();
} else {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::error(
Range::single_line(line, col, col + 1),
format!(
"mismatched ')'; expected '{}' to close",
closing_for(*ch)
),
),
code: DiagnosticCode::ParseError,
related: Vec::new(),
});
}
} else {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::error(
Range::single_line(line, col, col + 1),
"unmatched ')'".to_string(),
),
code: DiagnosticCode::ParseError,
related: Vec::new(),
});
}
}
TokenKind::RBracket => {
if let Some((ch, _, _)) = paren_stack.last() {
if *ch == '[' {
paren_stack.pop();
} else {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::error(
Range::single_line(line, col, col + 1),
format!(
"mismatched ']'; expected '{}' to close",
closing_for(*ch)
),
),
code: DiagnosticCode::ParseError,
related: Vec::new(),
});
}
} else {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::error(
Range::single_line(line, col, col + 1),
"unmatched ']'".to_string(),
),
code: DiagnosticCode::ParseError,
related: Vec::new(),
});
}
}
TokenKind::RBrace => {
if let Some((ch, _, _)) = paren_stack.last() {
if *ch == '{' {
paren_stack.pop();
} else {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::error(
Range::single_line(line, col, col + 1),
format!(
"mismatched '}}'; expected '{}' to close",
closing_for(*ch)
),
),
code: DiagnosticCode::ParseError,
related: Vec::new(),
});
}
} else {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::error(
Range::single_line(line, col, col + 1),
"unmatched '}'".to_string(),
),
code: DiagnosticCode::ParseError,
related: Vec::new(),
});
}
}
_ => {}
}
}
for (ch, line, col) in &paren_stack {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::error(
Range::single_line(*line, *col, *col + 1),
format!("unclosed '{}'", ch),
),
code: DiagnosticCode::ParseError,
related: Vec::new(),
});
}
diagnostics
}
pub fn collect_type_errors(&self, doc: &Document) -> Vec<RichDiagnostic> {
let mut diagnostics = Vec::new();
let analysis = analyze_document(&doc.uri, &doc.content, self.env);
for diag in &analysis.diagnostics {
let code = if diag.message.contains("shadows") {
DiagnosticCode::Shadowing
} else if diag.message.contains("lexer") {
DiagnosticCode::LexError
} else {
DiagnosticCode::TypeError
};
diagnostics.push(RichDiagnostic {
diagnostic: diag.clone(),
code,
related: Vec::new(),
});
}
diagnostics
}
pub fn collect_warnings(&self, doc: &Document) -> Vec<RichDiagnostic> {
let mut diagnostics = Vec::new();
let analysis = analyze_document(&doc.uri, &doc.content, self.env);
let mut defined_names: Vec<(String, Range)> = Vec::new();
for def in &analysis.definitions {
defined_names.push((def.name.clone(), def.range.clone()));
}
let mut lexer = Lexer::new(&doc.content);
let tokens = lexer.tokenize();
let mut usage_counts: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for token in &tokens {
if let TokenKind::Ident(name) = &token.kind {
*usage_counts.entry(name.clone()).or_insert(0) += 1;
}
}
for (name, range) in &defined_names {
if !name.starts_with('_') {
if let Some(&count) = usage_counts.get(name) {
if count <= 1 {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::warning(
range.clone(),
format!("unused variable '{}'", name),
),
code: DiagnosticCode::UnusedVariable,
related: Vec::new(),
});
}
}
}
}
for def in &analysis.definitions {
let kernel_name = Name::str(&def.name);
if self.env.contains(&kernel_name) {
diagnostics.push(RichDiagnostic {
diagnostic: Diagnostic::warning(
def.range.clone(),
format!("'{}' shadows existing declaration in environment", def.name),
),
code: DiagnosticCode::Shadowing,
related: Vec::new(),
});
}
}
diagnostics
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct DiagnosticPipelineStage {
pub name: String,
pub enabled: bool,
}
impl DiagnosticPipelineStage {
#[allow(dead_code)]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
enabled: true,
}
}
}