#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Error,
Warning,
Information,
Hint,
}
impl std::fmt::Display for DiagnosticSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DiagnosticSeverity::Error => write!(f, "error"),
DiagnosticSeverity::Warning => write!(f, "warning"),
DiagnosticSeverity::Information => write!(f, "info"),
DiagnosticSeverity::Hint => write!(f, "hint"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Diagnostic {
pub code: String,
pub severity: DiagnosticSeverity,
pub message: String,
pub url: Option<String>,
pub location: Option<SourceLocation>,
pub related: Vec<RelatedLocation>,
}
impl Diagnostic {
pub fn new(code: &str, message: &str) -> Self {
Self {
code: code.to_string(),
severity: DiagnosticSeverity::Error,
message: message.to_string(),
url: None,
location: None,
related: Vec::new(),
}
}
pub fn error(code: &str, message: &str) -> Self {
Self::new(code, message)
}
pub fn warning(code: &str, message: &str) -> Self {
Self {
code: code.to_string(),
severity: DiagnosticSeverity::Warning,
message: message.to_string(),
url: None,
location: None,
related: Vec::new(),
}
}
pub fn is_error(&self) -> bool {
self.severity == DiagnosticSeverity::Error
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SourceLocation {
pub file: String,
pub start: usize,
pub end: usize,
pub is_synthetic: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RelatedLocation {
pub message: String,
pub location: Option<SourceLocation>,
}
#[derive(Debug, Clone, Default)]
pub struct DiagnosticCollector {
diagnostics: Vec<Diagnostic>,
}
impl DiagnosticCollector {
pub fn new() -> Self {
Self {
diagnostics: Vec::new(),
}
}
pub fn add(&mut self, diagnostic: Diagnostic) {
self.diagnostics.push(diagnostic);
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn clear(&mut self) {
self.diagnostics.clear();
}
pub fn has_diagnostics(&self) -> bool {
!self.diagnostics.is_empty()
}
pub fn has_error(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.severity == DiagnosticSeverity::Error)
}
pub fn pipe<T>(&mut self, result: (T, Vec<Diagnostic>)) -> T {
let (value, diags) = result;
for diag in diags {
self.add(diag);
}
value
}
}
pub fn ignore_diagnostics<T>(result: (T, Vec<Diagnostic>)) -> T {
result.0
}
pub fn create_synthetic_source_location(loc: &str) -> SourceLocation {
SourceLocation {
file: loc.to_string(),
start: 0,
end: 0,
is_synthetic: true,
}
}
pub fn get_related_locations(_diagnostic: &Diagnostic) -> Vec<RelatedLocation> {
Vec::new()
}
pub fn get_source_location(diagnostic: &Diagnostic) -> Option<SourceLocation> {
diagnostic.location.clone()
}
pub fn get_diagnostic_template_instantiation_trace(
_diagnostic: &Diagnostic,
) -> Vec<SourceLocation> {
Vec::new()
}
#[derive(Debug, Clone)]
pub struct DiagnosticMessageDefinition {
pub text: String,
}
#[derive(Debug, Clone)]
pub struct DiagnosticDefinition {
pub severity: DiagnosticSeverity,
pub messages: Vec<(String, DiagnosticMessageDefinition)>,
pub url: Option<String>,
}
impl DiagnosticDefinition {
pub fn error(text: &str) -> Self {
Self {
severity: DiagnosticSeverity::Error,
messages: vec![(
"default".to_string(),
DiagnosticMessageDefinition {
text: text.to_string(),
},
)],
url: None,
}
}
pub fn warning(text: &str) -> Self {
Self {
severity: DiagnosticSeverity::Warning,
messages: vec![(
"default".to_string(),
DiagnosticMessageDefinition {
text: text.to_string(),
},
)],
url: None,
}
}
pub fn error_with_messages(messages: Vec<(&str, &str)>) -> Self {
Self {
severity: DiagnosticSeverity::Error,
messages: messages
.into_iter()
.map(|(id, text)| {
(
id.to_string(),
DiagnosticMessageDefinition {
text: text.to_string(),
},
)
})
.collect(),
url: None,
}
}
pub fn warning_with_messages(messages: Vec<(&str, &str)>) -> Self {
Self {
severity: DiagnosticSeverity::Warning,
messages: messages
.into_iter()
.map(|(id, text)| {
(
id.to_string(),
DiagnosticMessageDefinition {
text: text.to_string(),
},
)
})
.collect(),
url: None,
}
}
pub fn get_message(&self, message_id: &str) -> Option<&str> {
self.messages
.iter()
.find(|(id, _)| id == message_id)
.map(|(_, def)| def.text.as_str())
}
}
pub type DiagnosticMap = std::collections::HashMap<String, DiagnosticDefinition>;
#[derive(Debug, Clone)]
pub struct DiagnosticCreator {
diagnostics: DiagnosticMap,
library_name: Option<String>,
}
impl DiagnosticCreator {
pub fn new(diagnostics: DiagnosticMap, library_name: Option<String>) -> Self {
Self {
diagnostics,
library_name,
}
}
pub fn create_diagnostic(
&self,
code: &str,
message_id: Option<&str>,
format: &[(String, String)],
) -> Diagnostic {
let def = match self.diagnostics.get(code) {
Some(d) => d,
None => {
let codes: String = self
.diagnostics
.keys()
.map(|c| format!(" - {}", c))
.collect::<Vec<_>>()
.join("\n");
let error_msg = match &self.library_name {
Some(lib) => format!(
"Unexpected diagnostic code '{}'. It must match one of the code defined in the library '{}'. Defined codes:\n{}",
code, lib, codes
),
None => format!(
"Unexpected diagnostic code '{}'. It must match one of the code defined in the compiler. Defined codes:\n{}",
code, codes
),
};
panic!("{}", error_msg);
}
};
let msg_id = message_id.unwrap_or("default");
let message_text = def.get_message(msg_id).unwrap_or_else(|| {
let msgs: String = def.messages.iter().map(|(id, _)| format!(" - {}", id)).collect::<Vec<_>>().join("\n");
let error_msg = match &self.library_name {
Some(lib) => format!("Unexpected message id '{}' for code '{}' in library '{}'. Defined messages:\n{}", msg_id, code, lib, msgs),
None => format!("Unexpected message id '{}' for code '{}'. Defined messages:\n{}", msg_id, code, msgs),
};
panic!("{}", error_msg);
});
let mut message = message_text.to_string();
for (key, value) in format {
message = message.replace(&format!("{{{}}}", key), value);
}
let full_code = match &self.library_name {
Some(lib) => format!("{}/{}", lib, code),
None => code.to_string(),
};
let mut diag = Diagnostic::new(&full_code, &message);
diag.severity = def.severity;
if let Some(url) = &def.url {
diag.url = Some(url.clone());
}
diag
}
pub fn diagnostics(&self) -> &DiagnosticMap {
&self.diagnostics
}
pub fn len(&self) -> usize {
self.diagnostics.len()
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn library_name(&self) -> Option<&str> {
self.library_name.as_deref()
}
}
#[derive(Debug, Clone, Default)]
pub struct CompilerOptions {
pub misc_options: std::collections::HashMap<String, String>,
pub output_dir: Option<String>,
pub config: Option<String>,
pub emit: Option<Vec<String>>,
pub list_files: bool,
pub options: std::collections::HashMap<String, std::collections::HashMap<String, String>>,
pub ignore_deprecated: bool,
pub nostdlib: bool,
pub no_emit: bool,
pub dry_run: bool,
pub additional_imports: Option<Vec<String>>,
pub warning_as_error: bool,
pub design_time_build: bool,
pub trace: Option<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_diagnostic_new() {
let diag = Diagnostic::new("test-code", "Test message");
assert_eq!(diag.code, "test-code");
assert_eq!(diag.message, "Test message");
assert_eq!(diag.severity, DiagnosticSeverity::Error);
}
#[test]
fn test_diagnostic_error() {
let diag = Diagnostic::error("err-code", "Error message");
assert_eq!(diag.severity, DiagnosticSeverity::Error);
}
#[test]
fn test_diagnostic_warning() {
let diag = Diagnostic::warning("warn-code", "Warning message");
assert_eq!(diag.severity, DiagnosticSeverity::Warning);
}
#[test]
fn test_diagnostic_collector_new() {
let collector = DiagnosticCollector::new();
assert!(!collector.has_diagnostics());
}
#[test]
fn test_diagnostic_collector_add() {
let mut collector = DiagnosticCollector::new();
collector.add(Diagnostic::error("code", "message"));
assert!(collector.has_diagnostics());
assert_eq!(collector.diagnostics().len(), 1);
}
#[test]
fn test_diagnostic_collector_clear() {
let mut collector = DiagnosticCollector::new();
collector.add(Diagnostic::error("code", "message"));
collector.clear();
assert!(!collector.has_diagnostics());
}
#[test]
fn test_diagnostic_collector_pipe() {
let mut collector = DiagnosticCollector::new();
let result = collector.pipe((42, vec![Diagnostic::error("code", "msg")]));
assert_eq!(result, 42);
assert_eq!(collector.diagnostics().len(), 1);
}
#[test]
fn test_ignore_diagnostics() {
let result = ignore_diagnostics((42, vec![Diagnostic::error("code", "msg")]));
assert_eq!(result, 42);
}
#[test]
fn test_source_location() {
let loc = SourceLocation {
file: "test.tsp".to_string(),
start: 10,
end: 20,
is_synthetic: false,
};
assert_eq!(loc.file, "test.tsp");
assert_eq!(loc.start, 10);
assert_eq!(loc.end, 20);
}
#[test]
fn test_severity_display() {
assert_eq!(format!("{}", DiagnosticSeverity::Error), "error");
assert_eq!(format!("{}", DiagnosticSeverity::Warning), "warning");
assert_eq!(format!("{}", DiagnosticSeverity::Information), "info");
assert_eq!(format!("{}", DiagnosticSeverity::Hint), "hint");
}
#[test]
fn test_diagnostic_definition_error() {
let def = DiagnosticDefinition::error("Something went wrong");
assert_eq!(def.severity, DiagnosticSeverity::Error);
assert_eq!(def.get_message("default"), Some("Something went wrong"));
assert!(def.url.is_none());
}
#[test]
fn test_diagnostic_definition_warning() {
let def = DiagnosticDefinition::warning("Be careful");
assert_eq!(def.severity, DiagnosticSeverity::Warning);
assert_eq!(def.get_message("default"), Some("Be careful"));
}
#[test]
fn test_diagnostic_definition_with_url() {
let def = DiagnosticDefinition {
severity: DiagnosticSeverity::Error,
messages: vec![(
"default".to_string(),
DiagnosticMessageDefinition {
text: "Error".to_string(),
},
)],
url: Some("https://example.com/docs".to_string()),
};
assert_eq!(def.url, Some("https://example.com/docs".to_string()));
}
#[test]
fn test_diagnostic_definition_multiple_messages() {
let def = DiagnosticDefinition {
severity: DiagnosticSeverity::Error,
messages: vec![
(
"default".to_string(),
DiagnosticMessageDefinition {
text: "Default message".to_string(),
},
),
(
"atPath".to_string(),
DiagnosticMessageDefinition {
text: "Error at path {path}".to_string(),
},
),
],
url: None,
};
assert_eq!(def.get_message("default"), Some("Default message"));
assert_eq!(def.get_message("atPath"), Some("Error at path {path}"));
assert_eq!(def.get_message("nonexistent"), None);
}
#[test]
fn test_diagnostic_creator_simple() {
let creator = DiagnosticCreator::new(
HashMap::from([(
"invalid-argument".to_string(),
DiagnosticDefinition::error("Invalid argument"),
)]),
None,
);
let diag = creator.create_diagnostic("invalid-argument", None, &[]);
assert_eq!(diag.code, "invalid-argument");
assert_eq!(diag.message, "Invalid argument");
assert_eq!(diag.severity, DiagnosticSeverity::Error);
}
#[test]
fn test_diagnostic_creator_with_library_name() {
let creator = DiagnosticCreator::new(
HashMap::from([(
"invalid-argument".to_string(),
DiagnosticDefinition::error("Invalid argument"),
)]),
Some("myLib".to_string()),
);
let diag = creator.create_diagnostic("invalid-argument", None, &[]);
assert_eq!(diag.code, "myLib/invalid-argument");
}
#[test]
fn test_diagnostic_creator_with_format() {
let creator = DiagnosticCreator::new(
HashMap::from([(
"wrong-type".to_string(),
DiagnosticDefinition::error("Expected {expected} but got {actual}"),
)]),
None,
);
let diag = creator.create_diagnostic(
"wrong-type",
None,
&[
("expected".to_string(), "string".to_string()),
("actual".to_string(), "number".to_string()),
],
);
assert_eq!(diag.message, "Expected string but got number");
}
#[test]
fn test_diagnostic_creator_warning() {
let creator = DiagnosticCreator::new(
HashMap::from([(
"deprecated".to_string(),
DiagnosticDefinition::warning("'{name}' is deprecated"),
)]),
None,
);
let diag = creator.create_diagnostic(
"deprecated",
None,
&[("name".to_string(), "oldApi".to_string())],
);
assert_eq!(diag.severity, DiagnosticSeverity::Warning);
assert_eq!(diag.message, "'oldApi' is deprecated");
}
#[test]
fn test_diagnostic_creator_with_message_id() {
let creator = DiagnosticCreator::new(
HashMap::from([(
"invalid-value".to_string(),
DiagnosticDefinition {
severity: DiagnosticSeverity::Error,
messages: vec![
(
"default".to_string(),
DiagnosticMessageDefinition {
text: "Invalid value".to_string(),
},
),
(
"atPath".to_string(),
DiagnosticMessageDefinition {
text: "Invalid value at path {path}".to_string(),
},
),
],
url: None,
},
)]),
None,
);
let diag = creator.create_diagnostic(
"invalid-value",
Some("atPath"),
&[("path".to_string(), "foo.bar".to_string())],
);
assert_eq!(diag.message, "Invalid value at path foo.bar");
}
#[test]
#[should_panic(expected = "Unexpected diagnostic code")]
fn test_diagnostic_creator_unknown_code() {
let creator = DiagnosticCreator::new(
HashMap::from([(
"known-code".to_string(),
DiagnosticDefinition::error("Known"),
)]),
None,
);
creator.create_diagnostic("unknown-code", None, &[]);
}
#[test]
fn test_diagnostic_creator_accessors() {
let creator = DiagnosticCreator::new(
HashMap::from([("test".to_string(), DiagnosticDefinition::error("Test"))]),
Some("myLib".to_string()),
);
assert_eq!(creator.library_name(), Some("myLib"));
assert_eq!(creator.diagnostics().len(), 1);
}
#[test]
fn test_compiler_options_default() {
let opts = CompilerOptions::default();
assert!(opts.output_dir.is_none());
assert!(opts.emit.is_none());
assert!(!opts.list_files);
assert!(!opts.nostdlib);
assert!(!opts.no_emit);
assert!(!opts.dry_run);
assert!(!opts.ignore_deprecated);
assert!(!opts.warning_as_error);
assert!(!opts.design_time_build);
}
#[test]
fn test_compiler_options_with_values() {
let opts = CompilerOptions {
output_dir: Some("./tsp-output".to_string()),
emit: Some(vec!["@typespec/openapi".to_string()]),
nostdlib: true,
warning_as_error: true,
..Default::default()
};
assert_eq!(opts.output_dir.as_deref(), Some("./tsp-output"));
assert_eq!(opts.emit.as_ref().map(|v| v.len()), Some(1));
assert!(opts.nostdlib);
assert!(opts.warning_as_error);
}
#[test]
fn test_compiler_options_additional_imports() {
let opts = CompilerOptions {
additional_imports: Some(vec!["../common/main.tsp".to_string()]),
..Default::default()
};
assert_eq!(opts.additional_imports.as_ref().map(|v| v.len()), Some(1));
}
#[test]
fn test_compiler_options_trace() {
let opts = CompilerOptions {
trace: Some(vec!["binder".to_string(), "checker".to_string()]),
..Default::default()
};
assert_eq!(opts.trace.as_ref().map(|v| v.len()), Some(2));
}
#[test]
fn test_get_related_locations_no_location() {
let diag = Diagnostic::error("test", "msg");
let related = get_related_locations(&diag);
assert!(related.is_empty());
}
#[test]
fn test_get_source_location_with_location() {
let mut diag = Diagnostic::error("test", "msg");
diag.location = Some(SourceLocation {
file: "test.tsp".to_string(),
start: 0,
end: 10,
is_synthetic: false,
});
let loc = get_source_location(&diag);
assert!(loc.is_some());
assert_eq!(loc.unwrap().file, "test.tsp");
}
#[test]
fn test_get_source_location_without_location() {
let diag = Diagnostic::error("test", "msg");
let loc = get_source_location(&diag);
assert!(loc.is_none());
}
#[test]
fn test_get_diagnostic_template_instantiation_trace() {
let diag = Diagnostic::error("test", "msg");
let trace = get_diagnostic_template_instantiation_trace(&diag);
assert!(trace.is_empty());
}
}