use std::sync::Arc;
use super::resolve::{ResolveResult, Resolver, SymbolIndex};
use super::symbols::{HirSymbol, SymbolKind};
use crate::base::FileId;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Severity {
Error,
Warning,
Info,
Hint,
}
impl Severity {
pub fn to_lsp(&self) -> u32 {
match self {
Severity::Error => 1,
Severity::Warning => 2,
Severity::Info => 3,
Severity::Hint => 4,
}
}
}
#[derive(Clone, Debug)]
pub struct Diagnostic {
pub file: FileId,
pub start_line: u32,
pub start_col: u32,
pub end_line: u32,
pub end_col: u32,
pub severity: Severity,
pub code: Option<Arc<str>>,
pub message: Arc<str>,
pub related: Vec<RelatedInfo>,
}
#[derive(Clone, Debug)]
pub struct RelatedInfo {
pub file: FileId,
pub line: u32,
pub col: u32,
pub message: Arc<str>,
}
impl Diagnostic {
pub fn error(file: FileId, line: u32, col: u32, message: impl Into<Arc<str>>) -> Self {
Self {
file,
start_line: line,
start_col: col,
end_line: line,
end_col: col,
severity: Severity::Error,
code: None,
message: message.into(),
related: Vec::new(),
}
}
pub fn warning(file: FileId, line: u32, col: u32, message: impl Into<Arc<str>>) -> Self {
Self {
file,
start_line: line,
start_col: col,
end_line: line,
end_col: col,
severity: Severity::Warning,
code: None,
message: message.into(),
related: Vec::new(),
}
}
pub fn with_span(mut self, end_line: u32, end_col: u32) -> Self {
self.end_line = end_line;
self.end_col = end_col;
self
}
pub fn with_code(mut self, code: impl Into<Arc<str>>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_related(mut self, info: RelatedInfo) -> Self {
self.related.push(info);
self
}
}
#[allow(dead_code)]
pub mod codes {
pub const UNDEFINED_REFERENCE: &str = "E0001";
pub const AMBIGUOUS_REFERENCE: &str = "E0002";
pub const TYPE_MISMATCH: &str = "E0003";
pub const DUPLICATE_DEFINITION: &str = "E0004";
pub const MISSING_REQUIRED: &str = "E0005";
pub const INVALID_SPECIALIZATION: &str = "E0006";
pub const CIRCULAR_DEPENDENCY: &str = "E0007";
pub const INVALID_TYPE: &str = "E0008";
pub const INVALID_REDEFINITION: &str = "E0009";
pub const INVALID_SUBSETTING: &str = "E0010";
pub const CONSTRAINT_VIOLATION: &str = "E0011";
pub const INVALID_FEATURE_CONTEXT: &str = "E0012";
pub const ABSTRACT_INSTANTIATION: &str = "E0013";
pub const INVALID_IMPORT: &str = "E0014";
pub const UNUSED_SYMBOL: &str = "W0001";
pub const DEPRECATED: &str = "W0002";
pub const NAMING_CONVENTION: &str = "W0003";
}
#[derive(Clone, Debug, Default)]
pub struct DiagnosticCollector {
diagnostics: Vec<Diagnostic>,
}
impl DiagnosticCollector {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, diagnostic: Diagnostic) {
self.diagnostics.push(diagnostic);
}
pub fn undefined_reference(&mut self, file: FileId, symbol: &HirSymbol, name: &str) {
self.add(
Diagnostic::error(
file,
symbol.start_line,
symbol.start_col,
format!("undefined reference: '{}'", name),
)
.with_span(symbol.end_line, symbol.end_col)
.with_code(codes::UNDEFINED_REFERENCE),
);
}
pub fn ambiguous_reference(
&mut self,
file: FileId,
symbol: &HirSymbol,
name: &str,
candidates: &[HirSymbol],
) {
let candidate_names: Vec<_> = candidates
.iter()
.map(|c| c.qualified_name.as_ref())
.collect();
let mut diag = Diagnostic::error(
file,
symbol.start_line,
symbol.start_col,
format!(
"ambiguous reference: '{}' could be: {}",
name,
candidate_names.join(", ")
),
)
.with_span(symbol.end_line, symbol.end_col)
.with_code(codes::AMBIGUOUS_REFERENCE);
for candidate in candidates {
diag = diag.with_related(RelatedInfo {
file: candidate.file,
line: candidate.start_line,
col: candidate.start_col,
message: Arc::from(format!("candidate: {}", candidate.qualified_name)),
});
}
self.add(diag);
}
pub fn duplicate_definition(&mut self, file: FileId, symbol: &HirSymbol, existing: &HirSymbol) {
self.add(
Diagnostic::error(
file,
symbol.start_line,
symbol.start_col,
format!("duplicate definition: '{}' is already defined", symbol.name),
)
.with_span(symbol.end_line, symbol.end_col)
.with_code(codes::DUPLICATE_DEFINITION)
.with_related(RelatedInfo {
file: existing.file,
line: existing.start_line,
col: existing.start_col,
message: Arc::from(format!("previous definition of '{}'", existing.name)),
}),
);
}
pub fn type_mismatch(&mut self, file: FileId, symbol: &HirSymbol, expected: &str, found: &str) {
self.add(
Diagnostic::error(
file,
symbol.start_line,
symbol.start_col,
format!("type mismatch: expected '{}', found '{}'", expected, found),
)
.with_span(symbol.end_line, symbol.end_col)
.with_code(codes::TYPE_MISMATCH),
);
}
pub fn unused_symbol(&mut self, symbol: &HirSymbol) {
self.add(
Diagnostic::warning(
symbol.file,
symbol.start_line,
symbol.start_col,
format!("unused {}: '{}'", symbol.kind.display(), symbol.name),
)
.with_span(symbol.end_line, symbol.end_col)
.with_code(codes::UNUSED_SYMBOL),
);
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn diagnostics_for_file(&self, file: FileId) -> Vec<&Diagnostic> {
self.diagnostics.iter().filter(|d| d.file == file).collect()
}
pub fn error_count(&self) -> usize {
self.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.count()
}
pub fn warning_count(&self) -> usize {
self.diagnostics
.iter()
.filter(|d| d.severity == Severity::Warning)
.count()
}
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.severity == Severity::Error)
}
pub fn take(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
pub fn clear(&mut self) {
self.diagnostics.clear();
}
}
pub struct SemanticChecker<'a> {
index: &'a SymbolIndex,
collector: DiagnosticCollector,
referenced: std::collections::HashSet<Arc<str>>,
}
impl<'a> SemanticChecker<'a> {
pub fn new(index: &'a SymbolIndex) -> Self {
Self {
index,
collector: DiagnosticCollector::new(),
referenced: std::collections::HashSet::new(),
}
}
pub fn check_file(&mut self, file: FileId) {
let symbols = self.index.symbols_in_file(file);
for symbol in &symbols {
self.check_symbol(symbol);
}
self.check_duplicates(file, &symbols);
}
pub fn check_all(&mut self) {
let all_symbols: Vec<_> = self.index.all_symbols().cloned().collect();
for symbol in &all_symbols {
self.check_symbol(symbol);
}
}
fn check_symbol(&mut self, symbol: &HirSymbol) {
for supertype in &symbol.supertypes {
self.check_type_reference(symbol, supertype);
}
self.check_type_refs(symbol);
}
fn check_type_refs(&mut self, symbol: &HirSymbol) {
use crate::hir::symbols::TypeRefKind;
for type_ref in &symbol.type_refs {
match type_ref {
TypeRefKind::Simple(tr) => {
if let Some(ref resolved) = tr.resolved_target {
self.referenced.insert(resolved.clone());
continue;
}
if tr.kind.is_type_reference() {
self.check_type_reference(symbol, &tr.target);
} else if tr.kind.is_feature_reference() {
self.check_feature_reference(symbol, &tr.target);
}
}
TypeRefKind::Chain(chain) => {
for part in &chain.parts {
if let Some(ref resolved) = part.resolved_target {
self.referenced.insert(resolved.clone());
}
}
}
}
}
}
fn check_feature_reference(&mut self, symbol: &HirSymbol, target: &str) {
if let Some(idx) = target.rfind("::") {
let prefix = &target[..idx];
let member = &target[idx + 2..];
let scope = Self::extract_scope(&symbol.qualified_name);
let resolver = Resolver::new(self.index).with_scope(scope);
match resolver.resolve(prefix) {
ResolveResult::Found(prefix_sym) => {
if let Some(found) = self
.index
.find_member_in_scope(&prefix_sym.qualified_name, member)
{
self.referenced.insert(found.qualified_name.clone());
} else if !Self::is_builtin_type(target) {
self.collector
.undefined_reference(symbol.file, symbol, target);
}
}
ResolveResult::NotFound => {
if !Self::is_builtin_type(prefix) {
self.collector
.undefined_reference(symbol.file, symbol, target);
}
}
ResolveResult::Ambiguous(_) => {
}
}
return;
}
let member_qname = format!("{}::{}", symbol.qualified_name, target);
if self.index.lookup_qualified(&member_qname).is_some() {
self.referenced.insert(Arc::from(member_qname));
return;
}
for supertype in &symbol.supertypes {
let scope = Self::extract_scope(&symbol.qualified_name);
let resolver = Resolver::new(self.index).with_scope(scope);
if let ResolveResult::Found(super_sym) = resolver.resolve(supertype) {
if let Some(found) = self
.index
.find_member_in_scope(&super_sym.qualified_name, target)
{
self.referenced.insert(found.qualified_name.clone());
return;
}
}
}
if !Self::is_builtin_type(target) && !symbol.supertypes.is_empty() {
}
}
fn check_type_reference(&mut self, symbol: &HirSymbol, name: &str) {
let scope = Self::extract_scope(&symbol.qualified_name);
let resolver = Resolver::new(self.index).with_scope(scope);
match resolver.resolve(name) {
ResolveResult::Found(resolved) => {
self.referenced.insert(resolved.qualified_name.clone());
}
ResolveResult::Ambiguous(candidates) => {
self.collector
.ambiguous_reference(symbol.file, symbol, name, &candidates);
}
ResolveResult::NotFound => {
if !Self::is_builtin_type(name) && !name.contains('.') {
self.collector
.undefined_reference(symbol.file, symbol, name);
}
}
}
}
fn check_duplicates(&mut self, file: FileId, symbols: &[&HirSymbol]) {
use std::collections::HashMap;
let mut by_qname: HashMap<&str, Vec<&HirSymbol>> = HashMap::new();
for symbol in symbols {
if symbol.kind == SymbolKind::Import || symbol.kind == SymbolKind::Alias {
continue;
}
by_qname
.entry(symbol.qualified_name.as_ref())
.or_default()
.push(symbol);
}
for (_qname, defs) in by_qname {
if defs.len() > 1 {
let first = defs[0];
for dup in &defs[1..] {
self.collector.duplicate_definition(file, dup, first);
}
}
}
}
#[allow(dead_code)]
fn check_unused(&mut self, symbols: &[HirSymbol]) {
for symbol in symbols {
if !symbol.kind.is_definition() {
continue;
}
if symbol.kind == SymbolKind::Package {
continue;
}
if self.referenced.contains(&symbol.qualified_name) {
continue;
}
if !symbol.supertypes.is_empty() {
continue;
}
self.collector.unused_symbol(symbol);
}
}
fn is_builtin_type(name: &str) -> bool {
if matches!(
name,
"Boolean"
| "String"
| "Integer"
| "Real"
| "Natural"
| "Positive"
| "UnlimitedNatural"
| "Complex"
| "ScalarValues"
| "Base"
| "Anything"
) {
return true;
}
let stdlib_prefixes = [
"ISQ::", "SI::", "USCustomaryUnits::",
"Quantities::",
"MeasurementReferences::",
"QuantityCalculations::",
"TensorMeasurements::",
"TrigFunctions::",
"BaseFunctions::",
"DataFunctions::",
"ControlFunctions::",
"NumericalFunctions::",
"VectorFunctions::",
"SequenceFunctions::",
"CollectionFunctions::",
"Performances::",
"ScalarValues::",
"RealFunctions::",
"Time::",
"Collections::",
"Links::",
"Occurrences::",
"Objects::",
"Items::",
"Parts::",
"Ports::",
"Connections::",
"Interfaces::",
"Allocations::",
"Actions::",
"Calculations::",
"Constraints::",
"Requirements::",
"Cases::",
"AnalysisCases::",
"Metadata::",
"KerML::",
"SysML::",
];
for prefix in stdlib_prefixes {
if name.starts_with(prefix) {
return true;
}
}
let stdlib_types = [
"ISQ",
"SI",
"USCustomaryUnits",
"Quantities",
"MassValue",
"LengthValue",
"TimeValue",
"VelocityValue",
"AccelerationValue",
"ForceValue",
"EnergyValue",
"PowerValue",
"PressureValue",
"TemperatureValue",
"ElectricCurrentValue",
"TorqueValue",
"AreaValue",
"VolumeValue",
"DensityValue",
"AngleValue",
"AngularVelocityValue",
"AngularAccelerationValue",
"kg",
"m",
"s",
"A",
"K",
"mol",
"cd",
"N",
"J",
"W",
"Pa",
"distancePerVolume",
"length",
"time",
"mass",
"power",
"SampledFunction",
"SamplePair",
"TradeStudy",
"evaluationFunction",
"mop",
"status",
];
stdlib_types.contains(&name)
}
fn extract_scope(qualified_name: &str) -> String {
if let Some(pos) = qualified_name.rfind("::") {
qualified_name[..pos].to_string()
} else {
String::new()
}
}
pub fn finish(self) -> Vec<Diagnostic> {
let mut seen = std::collections::HashSet::new();
self.collector
.diagnostics
.into_iter()
.filter(|d| {
let key = (d.file, d.start_line, d.start_col, d.message.clone());
seen.insert(key)
})
.collect()
}
}
pub fn check_file(index: &SymbolIndex, file: FileId) -> Vec<Diagnostic> {
let mut checker = SemanticChecker::new(index);
checker.check_file(file);
checker.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::{SymbolKind, new_element_id};
fn make_symbol(name: &str, qualified: &str, kind: SymbolKind, file: u32) -> HirSymbol {
HirSymbol {
name: Arc::from(name),
short_name: None,
qualified_name: Arc::from(qualified),
element_id: new_element_id(),
kind,
file: FileId::new(file),
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
short_name_start_line: None,
short_name_start_col: None,
short_name_end_line: None,
short_name_end_col: None,
doc: None,
supertypes: Vec::new(),
relationships: Vec::new(),
type_refs: Vec::new(),
is_public: false,
view_data: None,
metadata_annotations: Vec::new(),
}
}
#[test]
fn test_diagnostic_error() {
let diag = Diagnostic::error(FileId::new(0), 10, 5, "test error");
assert_eq!(diag.severity, Severity::Error);
assert_eq!(diag.start_line, 10);
assert_eq!(diag.start_col, 5);
}
#[test]
fn test_diagnostic_with_code() {
let diag =
Diagnostic::error(FileId::new(0), 0, 0, "test").with_code(codes::UNDEFINED_REFERENCE);
assert_eq!(diag.code.as_deref(), Some("E0001"));
}
#[test]
fn test_collector_counts() {
let mut collector = DiagnosticCollector::new();
collector.add(Diagnostic::error(FileId::new(0), 0, 0, "error 1"));
collector.add(Diagnostic::error(FileId::new(0), 0, 0, "error 2"));
collector.add(Diagnostic::warning(FileId::new(0), 0, 0, "warning 1"));
assert_eq!(collector.error_count(), 2);
assert_eq!(collector.warning_count(), 1);
assert!(collector.has_errors());
}
#[test]
fn test_collector_by_file() {
let mut collector = DiagnosticCollector::new();
collector.add(Diagnostic::error(FileId::new(0), 0, 0, "file 0"));
collector.add(Diagnostic::error(FileId::new(1), 0, 0, "file 1"));
collector.add(Diagnostic::error(FileId::new(0), 0, 0, "file 0 again"));
let file0_diags = collector.diagnostics_for_file(FileId::new(0));
assert_eq!(file0_diags.len(), 2);
let file1_diags = collector.diagnostics_for_file(FileId::new(1));
assert_eq!(file1_diags.len(), 1);
}
#[test]
fn test_severity_to_lsp() {
assert_eq!(Severity::Error.to_lsp(), 1);
assert_eq!(Severity::Warning.to_lsp(), 2);
assert_eq!(Severity::Info.to_lsp(), 3);
assert_eq!(Severity::Hint.to_lsp(), 4);
}
#[test]
fn test_semantic_checker_undefined_reference() {
let mut index = SymbolIndex::new();
let mut symbol = make_symbol("wheel", "Vehicle::wheel", SymbolKind::PartUsage, 0);
symbol.supertypes = vec![Arc::from("NonExistent")];
index.add_file(FileId::new(0), vec![symbol]);
let diagnostics = check_file(&index, FileId::new(0));
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.contains("undefined reference"));
}
#[test]
fn test_semantic_checker_valid_reference() {
let mut index = SymbolIndex::new();
let wheel_def = make_symbol("Wheel", "Wheel", SymbolKind::PartDef, 0);
let mut wheel_usage = make_symbol("wheel", "Vehicle::wheel", SymbolKind::PartUsage, 0);
wheel_usage.supertypes = vec![Arc::from("Wheel")];
index.add_file(FileId::new(0), vec![wheel_def, wheel_usage]);
let diagnostics = check_file(&index, FileId::new(0));
assert_eq!(diagnostics.len(), 0);
}
}