#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Diagnostic {
code: DiagnosticCode,
severity: Severity,
message: String,
help: Option<String>,
range: TextRange,
}
impl Diagnostic {
pub(crate) fn new(code: DiagnosticCode, range: TextRange) -> Self {
Self {
code,
severity: code.severity(),
message: code.message().to_owned(),
help: code.help().map(str::to_owned),
range,
}
}
pub(crate) fn with_message(
code: DiagnosticCode,
range: TextRange,
message: impl Into<String>,
) -> Self {
Self {
code,
severity: code.severity(),
message: message.into(),
help: code.help().map(str::to_owned),
range,
}
}
pub fn code(&self) -> DiagnosticCode {
self.code
}
pub fn severity(&self) -> Severity {
self.severity
}
pub fn message(&self) -> &str {
&self.message
}
pub fn help(&self) -> Option<&str> {
self.help.as_deref()
}
pub fn range(&self) -> TextRange {
self.range
}
pub fn kind(&self) -> DiagnosticKind {
self.code.kind()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DiagnosticKind {
Syntax,
Semantic,
Dependency,
Validation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DiagnosticCode {
MissingBlueprintBlock,
MultipleBlueprintBlocks,
PromptBeforeBlueprint,
UnknownTopLevelItem,
UnknownBlueprintAttribute,
UnknownPromptAttribute,
UnknownValidateAttribute,
UnknownPromptType,
InvalidBooleanLiteral,
UnterminatedString,
InvalidEscapeSequence,
InvalidDependencyExpression,
UnknownDependencyMethod,
InvalidIdentifier,
InvalidInteger,
MalformedArray,
MissingPromptName,
MissingAttributeValue,
MissingBlueprintVersion,
MissingBlueprintName,
EmptyBlueprintName,
EmptyBlueprintVersion,
DuplicateBlueprintAttribute,
MissingPromptType,
EmptyPromptName,
DuplicatePromptName,
DuplicatePromptAttribute,
DuplicateValidateAttribute,
ChoicesOnNonChoicePrompt,
MissingChoicesForSelect,
MissingChoicesForMultiselect,
EmptyChoicesList,
DuplicateChoice,
NonStringChoice,
DefaultTypeMismatch,
SelectDefaultNotInChoices,
MultiselectDefaultMustBeArray,
MultiselectDefaultContainsUnknownChoice,
RequiredFalseWithNoDefault,
DuplicateValidateBlock,
InvalidBlueprintVersion,
InvalidMinimumAchitekVersion,
UnknownDependencyReference,
SelfDependency,
DependencyCycle,
DependencyTypeMismatch,
ContainsOnNonMultiselectPrompt,
ContainsUnknownChoice,
StringValidationOnNonStringPrompt,
SelectionValidationOnNonMultiselectPrompt,
InvalidLengthBounds,
InvalidSelectionBounds,
InvalidRegex,
}
impl DiagnosticCode {
pub fn kind(&self) -> DiagnosticKind {
match self {
Self::MissingBlueprintBlock
| Self::MultipleBlueprintBlocks
| Self::PromptBeforeBlueprint
| Self::UnknownTopLevelItem
| Self::UnknownBlueprintAttribute
| Self::UnknownPromptAttribute
| Self::UnknownValidateAttribute
| Self::UnknownPromptType
| Self::InvalidBooleanLiteral
| Self::UnterminatedString
| Self::InvalidEscapeSequence
| Self::InvalidDependencyExpression
| Self::UnknownDependencyMethod
| Self::InvalidIdentifier
| Self::InvalidInteger
| Self::MalformedArray
| Self::MissingPromptName
| Self::MissingAttributeValue => DiagnosticKind::Syntax,
Self::MissingBlueprintVersion
| Self::MissingBlueprintName
| Self::EmptyBlueprintName
| Self::EmptyBlueprintVersion
| Self::DuplicateBlueprintAttribute
| Self::MissingPromptType
| Self::EmptyPromptName
| Self::DuplicatePromptName
| Self::DuplicatePromptAttribute
| Self::DuplicateValidateAttribute
| Self::ChoicesOnNonChoicePrompt
| Self::MissingChoicesForSelect
| Self::MissingChoicesForMultiselect
| Self::EmptyChoicesList
| Self::DuplicateChoice
| Self::NonStringChoice
| Self::DefaultTypeMismatch
| Self::SelectDefaultNotInChoices
| Self::MultiselectDefaultMustBeArray
| Self::MultiselectDefaultContainsUnknownChoice
| Self::RequiredFalseWithNoDefault
| Self::DuplicateValidateBlock
| Self::InvalidBlueprintVersion
| Self::InvalidMinimumAchitekVersion => DiagnosticKind::Semantic,
Self::UnknownDependencyReference
| Self::SelfDependency
| Self::DependencyCycle
| Self::DependencyTypeMismatch
| Self::ContainsOnNonMultiselectPrompt
| Self::ContainsUnknownChoice => DiagnosticKind::Dependency,
Self::StringValidationOnNonStringPrompt
| Self::SelectionValidationOnNonMultiselectPrompt
| Self::InvalidLengthBounds
| Self::InvalidSelectionBounds
| Self::InvalidRegex => DiagnosticKind::Validation,
}
}
pub fn severity(&self) -> Severity {
match self {
Self::DuplicateChoice => Severity::Warning,
Self::RequiredFalseWithNoDefault => Severity::Hint,
_ => Severity::Error,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::MissingBlueprintBlock => "ACH0000",
Self::MultipleBlueprintBlocks => "ACH0001",
Self::PromptBeforeBlueprint => "ACH0002",
Self::UnknownTopLevelItem => "ACH0003",
Self::UnknownBlueprintAttribute => "ACH0004",
Self::UnknownPromptAttribute => "ACH0005",
Self::UnknownValidateAttribute => "ACH0006",
Self::UnknownPromptType => "ACH0007",
Self::InvalidBooleanLiteral => "ACH0008",
Self::UnterminatedString => "ACH0009",
Self::InvalidEscapeSequence => "ACH0010",
Self::InvalidDependencyExpression => "ACH0011",
Self::UnknownDependencyMethod => "ACH0012",
Self::InvalidIdentifier => "ACH0013",
Self::InvalidInteger => "ACH0014",
Self::MalformedArray => "ACH0015",
Self::MissingPromptName => "ACH0016",
Self::MissingAttributeValue => "ACH0017",
Self::MissingBlueprintVersion => "ACH1000",
Self::MissingBlueprintName => "ACH1001",
Self::EmptyBlueprintName => "ACH1002",
Self::EmptyBlueprintVersion => "ACH1003",
Self::DuplicateBlueprintAttribute => "ACH1004",
Self::MissingPromptType => "ACH1005",
Self::EmptyPromptName => "ACH1006",
Self::DuplicatePromptName => "ACH1007",
Self::DuplicatePromptAttribute => "ACH1008",
Self::DuplicateValidateAttribute => "ACH1009",
Self::ChoicesOnNonChoicePrompt => "ACH1010",
Self::MissingChoicesForSelect => "ACH1011",
Self::MissingChoicesForMultiselect => "ACH1012",
Self::EmptyChoicesList => "ACH1013",
Self::DuplicateChoice => "ACH1014",
Self::NonStringChoice => "ACH1015",
Self::DefaultTypeMismatch => "ACH1016",
Self::SelectDefaultNotInChoices => "ACH1017",
Self::MultiselectDefaultMustBeArray => "ACH1018",
Self::MultiselectDefaultContainsUnknownChoice => "ACH1019",
Self::RequiredFalseWithNoDefault => "ACH1020",
Self::DuplicateValidateBlock => "ACH1021",
Self::InvalidBlueprintVersion => "ACH1022",
Self::InvalidMinimumAchitekVersion => "ACH1023",
Self::UnknownDependencyReference => "ACH2000",
Self::SelfDependency => "ACH2001",
Self::DependencyCycle => "ACH2002",
Self::DependencyTypeMismatch => "ACH2003",
Self::ContainsOnNonMultiselectPrompt => "ACH2004",
Self::ContainsUnknownChoice => "ACH2005",
Self::StringValidationOnNonStringPrompt => "ACH3000",
Self::SelectionValidationOnNonMultiselectPrompt => "ACH3001",
Self::InvalidLengthBounds => "ACH3002",
Self::InvalidSelectionBounds => "ACH3003",
Self::InvalidRegex => "ACH3004",
}
}
pub fn message(&self) -> &'static str {
match self {
Self::MissingBlueprintBlock => "missing blueprint block",
Self::MultipleBlueprintBlocks => "multiple blueprint blocks",
Self::PromptBeforeBlueprint => "prompt block appears before blueprint block",
Self::UnknownTopLevelItem => "unknown top-level item",
Self::UnknownBlueprintAttribute => "unknown blueprint attribute",
Self::UnknownPromptAttribute => "unknown prompt attribute",
Self::UnknownValidateAttribute => "unknown validate attribute",
Self::UnknownPromptType => "unknown prompt type",
Self::InvalidBooleanLiteral => "invalid boolean literal",
Self::UnterminatedString => "unterminated string literal",
Self::InvalidEscapeSequence => "invalid escape sequence",
Self::InvalidDependencyExpression => "invalid dependency expression",
Self::UnknownDependencyMethod => "unknown dependency method",
Self::InvalidIdentifier => "invalid identifier",
Self::InvalidInteger => "invalid integer literal",
Self::MalformedArray => "malformed array literal",
Self::MissingPromptName => "missing prompt name",
Self::MissingAttributeValue => "missing attribute value",
Self::MissingBlueprintVersion => "missing blueprint version",
Self::MissingBlueprintName => "missing blueprint name",
Self::EmptyBlueprintName => "empty blueprint name",
Self::EmptyBlueprintVersion => "empty blueprint version",
Self::DuplicateBlueprintAttribute => "duplicate blueprint attribute",
Self::MissingPromptType => "missing prompt type",
Self::EmptyPromptName => "empty prompt name",
Self::DuplicatePromptName => "duplicate prompt name",
Self::DuplicatePromptAttribute => "duplicate prompt attribute",
Self::DuplicateValidateAttribute => "duplicate validate attribute",
Self::ChoicesOnNonChoicePrompt => "choices on non-choice prompt",
Self::MissingChoicesForSelect => "missing choices for select prompt",
Self::MissingChoicesForMultiselect => "missing choices for multiselect prompt",
Self::EmptyChoicesList => "empty choices list",
Self::DuplicateChoice => "duplicate choice",
Self::NonStringChoice => "non-string choice",
Self::DefaultTypeMismatch => "default type mismatch",
Self::SelectDefaultNotInChoices => "select default is not in choices",
Self::MultiselectDefaultMustBeArray => "multiselect default must be an array",
Self::MultiselectDefaultContainsUnknownChoice => {
"multiselect default contains unknown choice"
}
Self::RequiredFalseWithNoDefault => "required false with no default",
Self::DuplicateValidateBlock => "duplicate validate block",
Self::InvalidBlueprintVersion => "invalid blueprint version",
Self::InvalidMinimumAchitekVersion => "invalid minimum Achitek version",
Self::UnknownDependencyReference => "dependency references unknown prompt",
Self::SelfDependency => "dependency references itself",
Self::DependencyCycle => "dependency cycle",
Self::DependencyTypeMismatch => "dependency type mismatch",
Self::ContainsOnNonMultiselectPrompt => "contains on non-multiselect prompt",
Self::ContainsUnknownChoice => "contains unknown choice",
Self::StringValidationOnNonStringPrompt => "string validation on non-string prompt",
Self::SelectionValidationOnNonMultiselectPrompt => {
"selection validation on non-multiselect prompt"
}
Self::InvalidLengthBounds => "invalid length bounds",
Self::InvalidSelectionBounds => "invalid selection bounds",
Self::InvalidRegex => "invalid regex",
}
}
pub fn help(&self) -> Option<&'static str> {
match self {
Self::MissingBlueprintBlock => Some("Start the file with a `blueprint { ... }` block."),
Self::MultipleBlueprintBlocks => {
Some("Keep exactly one `blueprint` block in each Achitekfile.")
}
Self::PromptBeforeBlueprint => {
Some("Move the `blueprint` block before all `prompt` blocks.")
}
Self::UnknownTopLevelItem => {
Some("Only `blueprint` and `prompt` blocks are valid at the top level.")
}
Self::UnknownBlueprintAttribute => Some(
"Use one of `version`, `name`, `description`, `author`, or `min_achitek_version`.",
),
Self::UnknownPromptAttribute => Some(
"Use one of `type`, `help`, `choices`, `default`, `required`, `depends_on`, or `validate`.",
),
Self::UnknownValidateAttribute => Some(
"Use one of `regex`, `min_length`, `max_length`, `min_selections`, or `max_selections`.",
),
Self::UnknownPromptType => {
Some("Use one of `string`, `paragraph`, `bool`, `select`, or `multiselect`.")
}
Self::InvalidBooleanLiteral => Some("Use `true` or `false`."),
Self::UnterminatedString => Some("Close the string with `\"`."),
Self::InvalidEscapeSequence => {
Some("Supported escapes are `\\n`, `\\t`, `\\r`, `\\\"`, and `\\\\`.")
}
Self::InvalidDependencyExpression => Some(
"Use a prompt reference, comparison, `contains(...)`, `all(...)`, or `any(...)`.",
),
Self::UnknownDependencyMethod => Some("The only supported method is `contains`."),
Self::InvalidIdentifier => Some(
"Identifiers must start with a letter and contain only letters, digits, or `_`.",
),
Self::InvalidInteger => Some("Use a non-negative integer such as `1` or `42`."),
Self::MalformedArray => Some("Use `[value, value]` with comma-separated values."),
Self::MissingPromptName => Some("Write the prompt name as `prompt \"name\" { ... }`."),
Self::MissingAttributeValue => Some("Add a value after `=`, or remove the attribute."),
Self::MissingBlueprintVersion => {
Some("Add a `version = \"...\"` attribute to the `blueprint` block.")
}
Self::MissingBlueprintName => {
Some("Add a `name = \"...\"` attribute to the `blueprint` block.")
}
Self::EmptyBlueprintName => Some("Use a non-empty blueprint `name` value."),
Self::EmptyBlueprintVersion => Some("Use a non-empty blueprint `version` value."),
Self::DuplicateBlueprintAttribute => {
Some("Keep one value for each `blueprint` attribute.")
}
Self::MissingPromptType => Some("Add a `type = ...` attribute to the prompt block."),
Self::EmptyPromptName => Some("Use a non-empty prompt name."),
Self::DuplicatePromptName => Some("Give each prompt a unique name."),
Self::DuplicatePromptAttribute => {
Some("Keep one value for each prompt attribute in a prompt block.")
}
Self::DuplicateValidateAttribute => {
Some("Keep one value for each validation attribute in a `validate` block.")
}
Self::ChoicesOnNonChoicePrompt => {
Some("Use `choices` only with `select` or `multiselect` prompts.")
}
Self::MissingChoicesForSelect => {
Some("Add a non-empty `choices = [...]` array to the `select` prompt.")
}
Self::MissingChoicesForMultiselect => {
Some("Add a non-empty `choices = [...]` array to the `multiselect` prompt.")
}
Self::EmptyChoicesList => Some("Add at least one string choice."),
Self::DuplicateChoice => Some("Remove the duplicate choice value."),
Self::NonStringChoice => Some("Use string literals for prompt choices."),
Self::DefaultTypeMismatch => Some("Use a default value that matches the prompt type."),
Self::SelectDefaultNotInChoices => {
Some("Set the default to one of the values in `choices`.")
}
Self::MultiselectDefaultMustBeArray => {
Some("Use an array default such as `default = [\"one\"]`.")
}
Self::MultiselectDefaultContainsUnknownChoice => {
Some("Every default value must also appear in `choices`.")
}
Self::RequiredFalseWithNoDefault => {
Some("Remove `required = false` or provide a useful `default` value.")
}
Self::DuplicateValidateBlock => {
Some("Merge validation rules into a single `validate { ... }` block.")
}
Self::InvalidBlueprintVersion => {
Some("Use three numeric version components such as `1.0.0`.")
}
Self::InvalidMinimumAchitekVersion => {
Some("Use three numeric minimum Achitek version components such as `1.0.0`.")
}
Self::UnknownDependencyReference => {
Some("Reference the name of another prompt declared in this file.")
}
Self::SelfDependency => Some("A prompt cannot depend on itself."),
Self::DependencyCycle => Some("Remove or rewrite one dependency to break the cycle."),
Self::DependencyTypeMismatch => {
Some("Compare dependency values with values that match the referenced prompt type.")
}
Self::ContainsOnNonMultiselectPrompt => {
Some("Use `.contains(...)` only with `multiselect` prompt dependencies.")
}
Self::ContainsUnknownChoice => {
Some("Use a `.contains(...)` value that appears in the referenced prompt choices.")
}
Self::StringValidationOnNonStringPrompt => Some(
"Use string length or regex validation only on `string` or `paragraph` prompts.",
),
Self::SelectionValidationOnNonMultiselectPrompt => {
Some("Use selection-count validation only on `multiselect` prompts.")
}
Self::InvalidLengthBounds => {
Some("Ensure `min_length` is less than or equal to `max_length`.")
}
Self::InvalidSelectionBounds => {
Some("Ensure `min_selections` is less than or equal to `max_selections`.")
}
Self::InvalidRegex => Some("Use a regex pattern that can be compiled by Achitek."),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity {
Error,
Warning,
Hint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TextPosition {
pub line: usize,
pub byte: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TextRange {
pub start: TextPosition,
pub end: TextPosition,
}