Merge pull request #753 from aiken-lang/lsp/quickfix-unknowns

Lsp/quickfix unknowns
This commit is contained in:
Matthias Benkort 2023-10-21 21:23:56 +02:00 committed by GitHub
commit d6f74b5932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 563 additions and 13 deletions

View File

@ -78,6 +78,34 @@ impl TypedModule {
.find_map(|definition| definition.find_node(byte_index)) .find_map(|definition| definition.find_node(byte_index))
} }
pub fn has_definition(&self, name: &str) -> bool {
self.definitions.iter().any(|def| match def {
Definition::Fn(f) => f.public && f.name == name,
Definition::TypeAlias(alias) => alias.public && alias.alias == name,
Definition::ModuleConstant(cst) => cst.public && cst.name == name,
Definition::DataType(t) => t.public && t.name == name,
Definition::Use(_) => false,
Definition::Test(_) => false,
Definition::Validator(_) => false,
})
}
pub fn has_constructor(&self, name: &str) -> bool {
self.definitions.iter().any(|def| match def {
Definition::DataType(t) if t.public && !t.opaque => t
.constructors
.iter()
.any(|constructor| constructor.name == name),
Definition::DataType(_) => false,
Definition::Fn(_) => false,
Definition::TypeAlias(_) => false,
Definition::ModuleConstant(_) => false,
Definition::Use(_) => false,
Definition::Test(_) => false,
Definition::Validator(_) => false,
})
}
pub fn validate_module_name(&self) -> Result<(), Error> { pub fn validate_module_name(&self) -> Result<(), Error> {
if self.name == "aiken" || self.name == "aiken/builtin" { if self.name == "aiken" || self.name == "aiken/builtin" {
return Err(Error::ReservedModuleName { return Err(Error::ReservedModuleName {

View File

@ -0,0 +1,3 @@
pub trait ExtraData {
fn extra_data(&self) -> Option<String>;
}

View File

@ -5,6 +5,7 @@ use std::sync::{
pub mod ast; pub mod ast;
pub mod builtins; pub mod builtins;
pub mod error;
pub mod expr; pub mod expr;
pub mod format; pub mod format;
pub mod gen_uplc; pub mod gen_uplc;

View File

@ -309,10 +309,13 @@ impl<'a> Environment<'a> {
location: Span, location: Span,
) -> Result<&ValueConstructor, Error> { ) -> Result<&ValueConstructor, Error> {
match module { match module {
None => self.scope.get(name).ok_or_else(|| Error::UnknownVariable { None => self
.scope
.get(name)
.ok_or_else(|| Error::UnknownTypeConstructor {
location, location,
name: name.to_string(), name: name.to_string(),
variables: self.local_value_names(), constructors: self.local_constructor_names(),
}), }),
Some(m) => { Some(m) => {
@ -577,6 +580,14 @@ impl<'a> Environment<'a> {
.collect() .collect()
} }
pub fn local_constructor_names(&self) -> Vec<String> {
self.scope
.keys()
.filter(|&t| t.chars().next().unwrap_or_default().is_uppercase())
.map(|t| t.to_string())
.collect()
}
fn make_type_vars( fn make_type_vars(
&mut self, &mut self,
args: &[String], args: &[String],

View File

@ -1,4 +1,5 @@
use super::Type; use super::Type;
use crate::error::ExtraData;
use crate::{ use crate::{
ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedPattern}, ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedPattern},
expr::{self, UntypedExpr}, expr::{self, UntypedExpr},
@ -798,7 +799,7 @@ Perhaps, try the following:
suggest_neighbor(name, constructors.iter(), "Did you forget to import it?") suggest_neighbor(name, constructors.iter(), "Did you forget to import it?")
))] ))]
UnknownTypeConstructor { UnknownTypeConstructor {
#[label] #[label("unknown constructor")]
location: Span, location: Span,
name: String, name: String,
constructors: Vec<String>, constructors: Vec<String>,
@ -922,6 +923,64 @@ The best thing to do from here is to remove it."#))]
}, },
} }
impl ExtraData for Error {
fn extra_data(&self) -> Option<String> {
match self {
Error::CastDataNoAnn { .. }
| Error::CouldNotUnify { .. }
| Error::CyclicTypeDefinitions { .. }
| Error::DuplicateArgument { .. }
| Error::DuplicateConstName { .. }
| Error::DuplicateField { .. }
| Error::DuplicateImport { .. }
| Error::DuplicateName { .. }
| Error::DuplicateTypeName { .. }
| Error::DuplicateVarInPattern { .. }
| Error::ExtraVarInAlternativePattern { .. }
| Error::FunctionTypeInData { .. }
| Error::ImplicitlyDiscardedExpression { .. }
| Error::IncorrectFieldsArity { .. }
| Error::IncorrectFunctionCallArity { .. }
| Error::IncorrectPatternArity { .. }
| Error::IncorrectTupleArity { .. }
| Error::IncorrectTypeArity { .. }
| Error::IncorrectValidatorArity { .. }
| Error::KeywordInModuleName { .. }
| Error::LastExpressionIsAssignment { .. }
| Error::LogicalOpChainMissingExpr { .. }
| Error::MissingVarInAlternativePattern { .. }
| Error::MultiValidatorEqualArgs { .. }
| Error::NonLocalClauseGuardVariable { .. }
| Error::NotATuple { .. }
| Error::NotExhaustivePatternMatch { .. }
| Error::NotFn { .. }
| Error::PositionalArgumentAfterLabeled { .. }
| Error::PrivateTypeLeak { .. }
| Error::RecordAccessUnknownType { .. }
| Error::RecordUpdateInvalidConstructor { .. }
| Error::RecursiveType { .. }
| Error::RedundantMatchClause { .. }
| Error::TupleIndexOutOfBound { .. }
| Error::UnexpectedLabeledArg { .. }
| Error::UnexpectedLabeledArgInPattern { .. }
| Error::UnknownLabels { .. }
| Error::UnknownModuleField { .. }
| Error::UnknownModuleType { .. }
| Error::UnknownModuleValue { .. }
| Error::UnknownRecordField { .. }
| Error::UnnecessarySpreadOperator { .. }
| Error::UpdateMultiConstructorType { .. }
| Error::ValidatorImported { .. }
| Error::ValidatorMustReturnBool { .. } => None,
Error::UnknownType { name, .. }
| Error::UnknownTypeConstructor { name, .. }
| Error::UnknownVariable { name, .. }
| Error::UnknownModule { name, .. } => Some(name.clone()),
}
}
}
impl Error { impl Error {
pub fn call_situation(mut self) -> Self { pub fn call_situation(mut self) -> Self {
if let Error::UnknownRecordField { if let Error::UnknownRecordField {
@ -1522,6 +1581,30 @@ pub enum Warning {
}, },
} }
impl ExtraData for Warning {
fn extra_data(&self) -> Option<String> {
match self {
Warning::AllFieldsRecordUpdate { .. }
| Warning::ImplicitlyDiscardedResult { .. }
| Warning::NoFieldsRecordUpdate { .. }
| Warning::PubInValidatorModule { .. }
| Warning::SingleConstructorExpect { .. }
| Warning::SingleWhenClause { .. }
| Warning::Todo { .. }
| Warning::UnexpectedTypeHole { .. }
| Warning::UnusedConstructor { .. }
| Warning::UnusedImportedModule { .. }
| Warning::UnusedImportedValue { .. }
| Warning::UnusedPrivateFunction { .. }
| Warning::UnusedPrivateModuleConstant { .. }
| Warning::UnusedType { .. }
| Warning::UnusedVariable { .. }
| Warning::Utf8ByteArrayIsValidHexString { .. }
| Warning::ValidatorInLibraryModule { .. } => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnifyErrorSituation { pub enum UnifyErrorSituation {
/// Clauses in a case expression were found to return different types. /// Clauses in a case expression were found to return different types.

View File

@ -0,0 +1,206 @@
use crate::{line_numbers::LineNumbers, utils::span_to_lsp_range};
use aiken_lang::ast::{Definition, ModuleKind, Span, UntypedDefinition, Use};
use aiken_project::module::CheckedModule;
use itertools::Itertools;
use std::fs;
/// A freshly parsed module alongside its line numbers.
pub struct ParsedDocument {
definitions: Vec<UntypedDefinition>,
line_numbers: LineNumbers,
}
pub type AnnotatedEdit = (String, lsp_types::TextEdit);
/// Parse the target document as an 'UntypedModule' alongside its line numbers. This is useful in
/// case we need to manipulate the AST for a quickfix.
pub fn parse_document(document: &lsp_types::TextDocumentIdentifier) -> Option<ParsedDocument> {
let file_path = document
.uri
.to_file_path()
.expect("invalid text document uri?");
let source_code = fs::read_to_string(file_path).ok()?;
let line_numbers = LineNumbers::new(&source_code);
// NOTE: The 'ModuleKind' second argument doesn't matter. This is just added to the final
// object but has no influence on the parsing.
let (untyped_module, _) = aiken_lang::parser::module(&source_code, ModuleKind::Lib).ok()?;
Some(ParsedDocument {
definitions: untyped_module.definitions,
line_numbers,
})
}
/// Insert some text at the given location.
fn insert_text(at: usize, line_numbers: &LineNumbers, new_text: String) -> lsp_types::TextEdit {
let range = span_to_lsp_range(Span { start: at, end: at }, line_numbers);
lsp_types::TextEdit { range, new_text }
}
/// Find a suitable location (Span) in the import list. The boolean in the answer indicates
/// whether the import is a newline or not. It is set to 'false' when adding a qualified import
/// to an existing list.
impl ParsedDocument {
pub fn import(
&self,
import: &CheckedModule,
unqualified: Option<&str>,
) -> Option<AnnotatedEdit> {
let import_path = import.name.split('/').collect_vec();
let mut last_import = None;
for def in self.definitions.iter() {
match def {
Definition::Use(Use {
location,
module: existing_module,
unqualified: unqualified_list,
..
}) => {
last_import = Some(*location);
if import_path != existing_module.as_slice() {
continue;
}
match unqualified {
// There's already a matching qualified import, so we have nothing to do.
None => return None,
Some(unqualified) => {
let mut last_unqualified = None;
// Insert lexicographically, assuming unqualified imports are already
// ordered. If they are not, it doesn't really matter where we insert
// anyway.
for existing_unqualified in unqualified_list {
last_unqualified = Some(existing_unqualified.location);
let existing_name = existing_unqualified
.as_name
.as_ref()
.unwrap_or(&existing_unqualified.name);
// The unqualified import already exist, nothing to do.
if unqualified == existing_name {
return None;
// Current import is lexicographically greater, we can insert before
} else if unqualified < existing_name.as_str() {
return Some(self.insert_qualified_before(
import,
unqualified,
existing_unqualified.location,
));
} else {
continue;
}
}
return match last_unqualified {
// Only happens if 'unqualified_list' is empty, in which case, we
// simply create a new unqualified list of import.
None => {
Some(self.add_new_qualified(import, unqualified, *location))
}
// Happens if the new qualified import is lexicographically after
// all existing ones.
Some(location) => {
Some(self.insert_qualified_after(import, unqualified, location))
}
};
}
}
}
_ => continue,
}
}
// If the search above didn't lead to anything, we simply insert the import either:
//
// (a) After the last import statement if any;
// (b) As the first statement in the module.
Some(self.add_new_import_line(import, unqualified, last_import))
}
fn insert_qualified_before(
&self,
import: &CheckedModule,
unqualified: &str,
location: Span,
) -> AnnotatedEdit {
let title = format!("Use '{}' from {}", unqualified, import.name);
(
title,
insert_text(
location.start,
&self.line_numbers,
format!("{}, ", unqualified),
),
)
}
fn insert_qualified_after(
&self,
import: &CheckedModule,
unqualified: &str,
location: Span,
) -> AnnotatedEdit {
let title = format!("Use '{}' from {}", unqualified, import.name);
(
title,
insert_text(
location.end,
&self.line_numbers,
format!(", {}", unqualified),
),
)
}
fn add_new_qualified(
&self,
import: &CheckedModule,
unqualified: &str,
location: Span,
) -> AnnotatedEdit {
let title = format!("Use '{}' from {}", unqualified, import.name);
(
title,
insert_text(
location.end,
&self.line_numbers,
format!(".{{{}}}", unqualified),
),
)
}
fn add_new_import_line(
&self,
import: &CheckedModule,
unqualified: Option<&str>,
location: Option<Span>,
) -> AnnotatedEdit {
let import_line = format!(
"use {}{}",
import.name,
match unqualified {
Some(unqualified) => format!(".{{{}}}", unqualified),
None => String::new(),
}
);
let title = format!("Add new import line: {import_line}");
(
title,
match location {
None => insert_text(0, &self.line_numbers, format!("{import_line}\n")),
Some(Span { end, .. }) => {
insert_text(end, &self.line_numbers, format!("\n{import_line}"))
}
},
)
}
}

View File

@ -5,8 +5,10 @@ use lsp_server::Connection;
use std::env; use std::env;
mod cast; mod cast;
mod edits;
pub mod error; pub mod error;
mod line_numbers; mod line_numbers;
mod quickfix;
pub mod server; pub mod server;
mod utils; mod utils;
@ -60,6 +62,7 @@ fn capabilities() -> lsp_types::ServerCapabilities {
// work_done_progress: None, // work_done_progress: None,
// }, // },
// }), // }),
code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(true)),
document_formatting_provider: Some(lsp_types::OneOf::Left(true)), document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
definition_provider: Some(lsp_types::OneOf::Left(true)), definition_provider: Some(lsp_types::OneOf::Left(true)),
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)), hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),

View File

@ -0,0 +1,146 @@
use crate::{
edits::{self, AnnotatedEdit, ParsedDocument},
server::lsp_project::LspProject,
};
use std::collections::HashMap;
const UNKNOWN_VARIABLE: &str = "aiken::check::unknown::variable";
const UNKNOWN_TYPE: &str = "aiken::check::unknown::type";
const UNKNOWN_CONSTRUCTOR: &str = "aiken::check::unknown::type_constructor";
const UNKNOWN_MODULE: &str = "aiken::check::unknown::module";
/// Errors for which we can provide quickfixes
#[allow(clippy::enum_variant_names)]
pub enum Quickfix {
UnknownIdentifier,
UnknownModule,
UnknownConstructor,
}
fn match_code(diagnostic: &lsp_types::Diagnostic, expected: &str) -> bool {
diagnostic.code == Some(lsp_types::NumberOrString::String(expected.to_string()))
}
/// Assert whether a diagnostic can be automatically fixed. Note that diagnostics often comes in
/// two severities, an error and hint; so we must be careful only addressing errors.
pub fn assert(diagnostic: &lsp_types::Diagnostic) -> Option<Quickfix> {
let is_error = diagnostic.severity == Some(lsp_types::DiagnosticSeverity::ERROR);
if !is_error {
return None;
}
if match_code(diagnostic, UNKNOWN_VARIABLE) {
return Some(Quickfix::UnknownIdentifier);
}
if match_code(diagnostic, UNKNOWN_TYPE) {
return Some(Quickfix::UnknownIdentifier);
}
if match_code(diagnostic, UNKNOWN_CONSTRUCTOR) {
return Some(Quickfix::UnknownConstructor);
}
if match_code(diagnostic, UNKNOWN_MODULE) {
return Some(Quickfix::UnknownModule);
}
None
}
pub fn quickfix(
compiler: &LspProject,
text_document: &lsp_types::TextDocumentIdentifier,
diagnostic: &lsp_types::Diagnostic,
quickfix: &Quickfix,
) -> Vec<lsp_types::CodeAction> {
let mut actions = Vec::new();
if let Some(ref parsed_document) = edits::parse_document(text_document) {
if let Some(serde_json::Value::String(ref data)) = diagnostic.data {
let edits = match quickfix {
Quickfix::UnknownIdentifier => unknown_identifier(compiler, parsed_document, data),
Quickfix::UnknownModule => unknown_module(compiler, parsed_document, data),
Quickfix::UnknownConstructor => {
unknown_constructor(compiler, parsed_document, data)
}
};
for (title, edit) in edits.into_iter() {
let mut changes = HashMap::new();
changes.insert(text_document.uri.clone(), vec![edit]);
actions.push(lsp_types::CodeAction {
title,
kind: Some(lsp_types::CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic.clone()]),
is_preferred: Some(true),
disabled: None,
data: None,
command: None,
edit: Some(lsp_types::WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}),
});
}
}
}
actions
}
fn unknown_identifier(
compiler: &LspProject,
parsed_document: &ParsedDocument,
var_name: &str,
) -> Vec<AnnotatedEdit> {
let mut edits = Vec::new();
for module in compiler.project.modules() {
if module.ast.has_definition(var_name) {
if let Some(edit) = parsed_document.import(&module, Some(var_name)) {
edits.push(edit)
}
}
}
edits
}
fn unknown_constructor(
compiler: &LspProject,
parsed_document: &ParsedDocument,
constructor_name: &str,
) -> Vec<AnnotatedEdit> {
let mut edits = Vec::new();
for module in compiler.project.modules() {
if module.ast.has_constructor(constructor_name) {
if let Some(edit) = parsed_document.import(&module, Some(constructor_name)) {
edits.push(edit)
}
}
}
edits
}
fn unknown_module(
compiler: &LspProject,
parsed_document: &ParsedDocument,
module_name: &str,
) -> Vec<AnnotatedEdit> {
let mut edits = Vec::new();
for module in compiler.project.modules() {
if module.name.ends_with(module_name) {
if let Some(edit) = parsed_document.import(&module, None) {
edits.push(edit);
}
}
}
edits
}

View File

@ -6,6 +6,7 @@ use std::{
use aiken_lang::{ use aiken_lang::{
ast::{Definition, Located, ModuleKind, Span, Use}, ast::{Definition, Located, ModuleKind, Span, Use},
error::ExtraData,
parser, parser,
tipo::pretty::Printer, tipo::pretty::Printer,
}; };
@ -23,7 +24,8 @@ use lsp_types::{
Notification, Progress, PublishDiagnostics, ShowMessage, Notification, Progress, PublishDiagnostics, ShowMessage,
}, },
request::{ request::{
Completion, Formatting, GotoDefinition, HoverRequest, Request, WorkDoneProgressCreate, CodeActionRequest, Completion, Formatting, GotoDefinition, HoverRequest, Request,
WorkDoneProgressCreate,
}, },
DocumentFormattingParams, InitializeParams, TextEdit, DocumentFormattingParams, InitializeParams, TextEdit,
}; };
@ -33,6 +35,7 @@ use crate::{
cast::{cast_notification, cast_request}, cast::{cast_notification, cast_request},
error::Error as ServerError, error::Error as ServerError,
line_numbers::LineNumbers, line_numbers::LineNumbers,
quickfix,
utils::{ utils::{
path_to_uri, span_to_lsp_range, text_edit_replace, uri_to_module_name, path_to_uri, span_to_lsp_range, text_edit_replace, uri_to_module_name,
COMPILING_PROGRESS_TOKEN, CREATE_COMPILING_PROGRESS_TOKEN, COMPILING_PROGRESS_TOKEN, CREATE_COMPILING_PROGRESS_TOKEN,
@ -41,7 +44,7 @@ use crate::{
use self::lsp_project::LspProject; use self::lsp_project::LspProject;
mod lsp_project; pub mod lsp_project;
pub mod telemetry; pub mod telemetry;
#[allow(dead_code)] #[allow(dead_code)]
@ -288,6 +291,7 @@ impl Server {
} }
} }
} }
HoverRequest::METHOD => { HoverRequest::METHOD => {
let params = cast_request::<HoverRequest>(request)?; let params = cast_request::<HoverRequest>(request)?;
@ -324,6 +328,33 @@ impl Server {
}) })
} }
CodeActionRequest::METHOD => {
let mut actions = Vec::new();
if let Some(ref compiler) = self.compiler {
let params = cast_request::<CodeActionRequest>(request)
.expect("cast code action request");
for diagnostic in params.context.diagnostics.iter() {
if let Some(strategy) = quickfix::assert(diagnostic) {
let quickfixes = quickfix::quickfix(
compiler,
&params.text_document,
diagnostic,
&strategy,
);
actions.extend(quickfixes);
}
}
}
Ok(lsp_server::Response {
id,
error: None,
result: Some(serde_json::to_value(actions)?),
})
}
unsupported => Err(ServerError::UnsupportedLspRequest { unsupported => Err(ServerError::UnsupportedLspRequest {
request: unsupported.to_string(), request: unsupported.to_string(),
}), }),
@ -445,7 +476,6 @@ impl Server {
fn module_for_uri(&self, uri: &url::Url) -> Option<&CheckedModule> { fn module_for_uri(&self, uri: &url::Url) -> Option<&CheckedModule> {
self.compiler.as_ref().and_then(|compiler| { self.compiler.as_ref().and_then(|compiler| {
let module_name = uri_to_module_name(uri, &self.root).expect("uri to module name"); let module_name = uri_to_module_name(uri, &self.root).expect("uri to module name");
compiler.modules.get(&module_name) compiler.modules.get(&module_name)
}) })
} }
@ -591,7 +621,7 @@ impl Server {
/// the `showMessage` notification instead. /// the `showMessage` notification instead.
fn process_diagnostic<E>(&mut self, error: E) -> Result<(), ServerError> fn process_diagnostic<E>(&mut self, error: E) -> Result<(), ServerError>
where where
E: Diagnostic + GetSource, E: Diagnostic + GetSource + ExtraData,
{ {
let (severity, typ) = match error.severity() { let (severity, typ) = match error.severity() {
Some(severity) => match severity { Some(severity) => match severity {
@ -642,7 +672,7 @@ impl Server {
message, message,
related_information: None, related_information: None,
tags: None, tags: None,
data: None, data: error.extra_data().map(serde_json::Value::String),
}; };
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]

View File

@ -38,8 +38,6 @@ impl LspProject {
self.project.restore(checkpoint); self.project.restore(checkpoint);
result?;
let modules = self.project.modules(); let modules = self.project.modules();
for mut module in modules.into_iter() { for mut module in modules.into_iter() {
@ -61,6 +59,8 @@ impl LspProject {
self.modules.insert(module.name.to_string(), module); self.modules.insert(module.name.to_string(), module);
} }
result?;
Ok(()) Ok(())
} }
} }

View File

@ -4,6 +4,7 @@ use crate::{
}; };
use aiken_lang::{ use aiken_lang::{
ast::{self, BinOp, Span}, ast::{self, BinOp, Span},
error::ExtraData,
parser::error::ParseError, parser::error::ParseError,
tipo, tipo,
}; };
@ -172,6 +173,34 @@ impl From<Error> for Vec<Error> {
} }
} }
impl ExtraData for Error {
fn extra_data(&self) -> Option<String> {
match self {
Error::DuplicateModule { .. } => None,
Error::FileIo { .. } => None,
Error::Format { .. } => None,
Error::StandardIo { .. } => None,
Error::Blueprint { .. } => None,
Error::MissingManifest { .. } => None,
Error::TomlLoading { .. } => None,
Error::ImportCycle { .. } => None,
Error::Parse { .. } => None,
Error::Type { error, .. } => error.extra_data(),
Error::TestFailure { .. } => None,
Error::Http { .. } => None,
Error::ZipExtract { .. } => None,
Error::JoinError { .. } => None,
Error::UnknownPackageVersion { .. } => None,
Error::UnableToResolvePackage { .. } => None,
Error::Json { .. } => None,
Error::MalformedStakeAddress { .. } => None,
Error::NoValidatorNotFound { .. } => None,
Error::MoreThanOneValidatorFound { .. } => None,
Error::Module { .. } => None,
}
}
}
pub trait GetSource { pub trait GetSource {
fn path(&self) -> Option<PathBuf>; fn path(&self) -> Option<PathBuf>;
fn src(&self) -> Option<String>; fn src(&self) -> Option<String>;
@ -481,6 +510,16 @@ pub enum Warning {
DependencyAlreadyExists { name: PackageName }, DependencyAlreadyExists { name: PackageName },
} }
impl ExtraData for Warning {
fn extra_data(&self) -> Option<String> {
match self {
Warning::NoValidators { .. } => None,
Warning::DependencyAlreadyExists { .. } => None,
Warning::Type { warning, .. } => warning.extra_data(),
}
}
}
impl GetSource for Warning { impl GetSource for Warning {
fn path(&self) -> Option<PathBuf> { fn path(&self) -> Option<PathBuf> {
match self { match self {