use voltaic_ast::Binding;
use voltaic_ast::ClassMember;
use voltaic_ast::ClauseBody;
use voltaic_ast::Expr;
use voltaic_ast::Ident;
use voltaic_ast::Item;
use voltaic_ast::Pattern;
use voltaic_ast::Program;
use voltaic_ast::Stmt;
use voltaic_diag::Code;
use voltaic_diag::Diagnostic;
use voltaic_diag::DiagnosticCode;
use voltaic_diag::Lint;
use voltaic_diag::Severity;
use voltaic_diag::Span;
use voltaic_lexer::TokenKind;
use voltaic_lexer::Trivia;
use voltaic_lexer::TriviaKind;
use std::path::Path;
pub fn lint(path: &Path, source: &str, program: Option<&Program>) -> Vec<Diagnostic> {
let shown = path.display().to_string();
let warning = |code, message, span, help| Diagnostic {
code,
severity: Severity::Warning,
message,
span,
help: Some(help),
path: shown.clone(),
source: source.to_string(),
};
let unwraps = unwrap_call_sites(source, program).into_iter().map(|span| {
warning(
DiagnosticCode::from(Code::UnwrapUsed),
UNWRAP_MESSAGE.to_string(),
span,
"if a panic is intended here, say so on the line above: `#[voltaic::allow(unwrap)]`"
.to_string(),
)
});
let pragmas = comment_pragma_sites(source).into_iter().map(|pair| {
let span = pair.0;
let named = pair.1;
let code_text = named.code().text();
let help = format!(
"the comment still suppresses `{}` for this milestone; the attribute is what plan 64 keeps",
code_text
);
warning(
DiagnosticCode::from(Code::CommentAllowPragma),
comment_pragma_message(named),
span,
help,
)
});
let mut found: Vec<Diagnostic> = unwraps.chain(pragmas).collect();
if let Some(tree) = program {
let shadows = shadow_sites(tree).into_iter().map(|pair| {
let span = pair.0;
let name = pair.1;
warning(
DiagnosticCode::from(Code::ShadowedBinding),
shadow_message(&name),
span,
SHADOW_HELP.to_string(),
)
});
found.extend(shadows);
}
found.sort_by_key(|d| d.span);
found
}
pub(crate) fn allow_pragma(trivium: &Trivia, lint: Lint) -> bool {
attribute_pragma(trivium) == Some(lint) || comment_pragma(trivium) == Some(lint)
}
pub(crate) fn attribute_pragma(trivium_attribute_pragma: &Trivia) -> Option<Lint> {
match trivium_attribute_pragma.kind {
TriviaKind::Attribute => Lint::from_allow_attribute(trivium_attribute_pragma.text.trim()),
_ => None,
}
}
#[cfg(test)]
fn trivium_attribute_pragma(kind: TriviaKind, text: &str) -> Trivia {
Trivia {
kind,
text: text.to_string(),
span: (0, 0),
}
}
pub(crate) fn binding_name(binding: &Binding) -> Option<&Ident> {
match &binding.pattern {
Pattern::Binding(ident) => Some(ident),
Pattern::Literal { .. }
| Pattern::Wildcard(_)
| Pattern::Range { .. }
| Pattern::Apply { .. }
| Pattern::Tuple { .. }
| Pattern::Slice { .. }
| Pattern::Record { .. }
| Pattern::As { .. }
| Pattern::Rest(_)
| Pattern::NamedRest { .. }
| Pattern::Or(_) => None,
}
}
pub(crate) fn comment_pragma(trivium_comment_pragma: &Trivia) -> Option<Lint> {
match trivium_comment_pragma.kind {
TriviaKind::LineComment => Lint::from_allow_comment(trivium_comment_pragma.text.trim()),
_ => None,
}
}
#[cfg(test)]
fn trivium_comment_pragma(kind: TriviaKind, text: &str) -> Trivia {
Trivia {
kind,
text: text.to_string(),
span: (0, 0),
}
}
pub(crate) fn comment_pragma_message(lint: Lint) -> String {
let comment = lint.allow_comment();
let attribute = lint.allow_attribute();
format!(
"`-- {}` is deprecated — write `#[{}]` on the line above instead (plan 53 §1.3)",
comment, attribute
)
}
pub(crate) fn comment_pragma_sites(source: &str) -> Vec<(Span, Lint)> {
let Ok(tokens) = voltaic_lexer::lex(source) else {
return Vec::new();
};
tokens
.iter()
.flat_map(|t| t.leading.iter().chain(t.trailing.as_ref()))
.filter_map(|trivium| comment_pragma(trivium).map(|lint| (trivium.span, lint)))
.collect()
}
pub(crate) fn line_of(source: &str, offset: usize) -> usize {
let end = offset.min(source.len());
source[0..end].matches('\n').count() + 1
}
pub(crate) struct PragmaWalk {
pub(crate) lint: Lint,
pub(crate) found: Vec<Span>,
}
impl ProgramWalk for PragmaWalk {
fn item(&mut self, item: &Item) {
if voltaic_ast::leading(item)
.iter()
.any(|t| allow_pragma(t, self.lint))
{
self.found.push(item.span());
}
();
}
fn stmts(&mut self, stmts: &[Stmt]) {
for stmt in stmts {
if stmt.leading.iter().any(|t| allow_pragma(t, self.lint)) {
self.found.push(stmt.span);
}
}
();
}
}
pub(crate) trait ProgramWalk {
fn item(&mut self, __rdx_a1: &Item);
fn stmts(&mut self, __rdx_a1: &[Stmt]);
}
pub(crate) const SHADOW_HELP: &str = "to reassign instead, write `:=` and `mut` on the first binding; if the shadow is meant, say so on the line above: `#[voltaic::allow(shadow)]`";
pub(crate) fn shadow_message(name: &str) -> String {
format!(
"`{}` is already bound in this block — a bare `=` binds a second name rather than reassigning the first (plan 54 §1.1)",
name
)
}
pub(crate) fn shadow_sites(program: &Program) -> Vec<(Span, String)> {
let mut walk = ShadowWalk { found: Vec::new() };
walk_program(program, &mut walk);
let ranges = suppressed_spans(program, Lint::Shadow);
walk
.found
.into_iter()
.filter(|pair| {
let span = pair.0;
!ranges.iter().any(|r| r.0 <= span.0 && span.1 <= r.1)
})
.collect()
}
pub(crate) struct ShadowWalk {
pub(crate) found: Vec<(Span, String)>,
}
impl ProgramWalk for ShadowWalk {
fn item(&mut self, _item: &Item) {
();
}
fn stmts(&mut self, stmts: &[Stmt]) {
let mut bound: Vec<&str> = Vec::new();
for stmt in stmts {
let Expr::Binding(binding) = &stmt.expr else {
continue;
};
let Some(ident) = binding_name(binding) else {
continue;
};
let suppressed = stmt
.trailing
.as_ref()
.is_some_and(|t| allow_pragma(t, Lint::Shadow));
if bound.contains(&ident.text.as_str()) && !suppressed {
self.found.push((ident.span, ident.text.clone()));
}
bound.push(&ident.text);
}
();
}
}
pub(crate) fn suppressed_spans(program: &Program, lint: Lint) -> Vec<Span> {
let mut walk = PragmaWalk {
lint,
found: Vec::new(),
};
walk_program(program, &mut walk);
walk.found
}
pub(crate) fn unwrap_call_sites(source: &str, program: Option<&Program>) -> Vec<Span> {
let Ok(tokens) = voltaic_lexer::lex(source) else {
return Vec::new();
};
let suppressed: Vec<usize> = tokens
.iter()
.flat_map(|t| t.leading.iter().chain(t.trailing.as_ref()))
.filter(|trivium| allow_pragma(trivium, Lint::Unwrap))
.map(|trivium| line_of(source, trivium.span.0))
.collect();
let ranges = program
.map(|p| suppressed_spans(p, Lint::Unwrap))
.unwrap_or_default();
tokens
.windows(2)
.filter(|pair| pair[0].kind == TokenKind::Dot)
.filter_map(|pair| match &pair[1].kind {
TokenKind::Ident(name) if name == "unwrap" || name == "expect" => Some(pair[1].span),
_ => None,
})
.filter(|span| !suppressed.contains(&line_of(source, span.0)))
.filter(|span| !ranges.iter().any(|r| r.0 <= span.0 && span.1 <= r.1))
.collect()
}
pub(crate) const UNWRAP_MESSAGE: &str =
"`unwrap` and `expect` panic — the ergonomic path is `?`, `??` or `catch` (doc 03 §12.7)";
pub(crate) fn walk_item(item: &Item, walk: &mut dyn ProgramWalk) {
walk.item(item);
match item {
Item::Fn(decl) => {
for clause in &decl.clauses {
match &clause.body {
ClauseBody::Plain(block) => walk_stmts(&block.stmts, walk),
ClauseBody::Guarded(rows) => {
for row in rows {
walk_stmts(&row.body.stmts, walk);
}
}
};
walk_stmts(&clause.wheres, walk);
}
}
Item::Class(decl) => {
for member in &decl.members {
walk_member(member, walk);
}
}
Item::Instance(decl) => {
for member in &decl.members {
walk_member(member, walk);
}
}
Item::Test(decl) => walk_stmts(&decl.body.stmts, walk),
Item::Module(decl) => {
for inner in decl.items.iter().flatten() {
walk_item(inner, walk);
}
}
Item::Data(_)
| Item::Type(_)
| Item::Errors(_)
| Item::Import(_)
| Item::Const(_)
| Item::InlineRust(_)
| Item::Extern(_)
| Item::PatternSynonym(_)
| Item::DeriveTemplate(_)
| Item::OperatorDecl(_) => (),
};
();
}
pub(crate) fn walk_member(member: &ClassMember, walk: &mut dyn ProgramWalk) {
if let ClassMember::Method(decl) = member {
walk_item(&Item::Fn(decl.clone()), walk);
}
();
}
pub(crate) fn walk_program(program: &Program, walk: &mut dyn ProgramWalk) {
for item in &program.items {
walk_item(item, walk);
}
();
}
pub(crate) fn walk_stmts(stmts: &[Stmt], walk: &mut dyn ProgramWalk) {
walk.stmts(stmts);
for stmt in stmts {
for nested in stmt.expr.statement_blocks() {
walk_stmts(&nested.stmts, walk);
}
}
();
}
#[cfg(test)]
mod __rdx_tests {
use super::*;
#[test]
fn the_attribute_suppresses() {
assert_eq!(
attribute_pragma(&trivium_attribute_pragma(
TriviaKind::Attribute,
"voltaic::allow(unwrap)"
)),
Some(Lint::Unwrap)
);
}
#[test]
fn each_lint_is_suppressed_by_its_own_name() {
assert_eq!(
attribute_pragma(&trivium_attribute_pragma(
TriviaKind::Attribute,
"voltaic::allow(shadow)"
)),
Some(Lint::Shadow)
);
}
#[test]
fn an_unknown_lint_name_is_not_a_pragma() {
assert_eq!(
attribute_pragma(&trivium_attribute_pragma(
TriviaKind::Attribute,
"voltaic::allow(unwarp)"
)),
None
);
}
#[test]
fn an_unnamespaced_allow_is_rustc_s() {
assert_eq!(
attribute_pragma(&trivium_attribute_pragma(
TriviaKind::Attribute,
"allow(unwrap)"
)),
None
);
}
#[test]
fn whitespace_inside_the_brackets_is_not_normalised() {
assert_eq!(
attribute_pragma(&trivium_attribute_pragma(
TriviaKind::Attribute,
"voltaic :: allow( unwrap )"
)),
None
);
}
#[test]
fn an_inner_attribute_does_not_suppress() {
assert_eq!(
attribute_pragma(&trivium_attribute_pragma(
TriviaKind::InnerAttribute,
"voltaic::allow(unwrap)"
)),
None
);
}
#[test]
fn the_pragma_suppresses() {
assert_eq!(
comment_pragma(&trivium_comment_pragma(
TriviaKind::LineComment,
"voltaic: allow unwrap"
)),
Some(Lint::Unwrap)
);
}
#[test]
fn each_lint_is_suppressed_by_its_own_name_2() {
assert_eq!(
comment_pragma(&trivium_comment_pragma(
TriviaKind::LineComment,
"voltaic: allow shadow"
)),
Some(Lint::Shadow)
);
}
#[test]
fn an_unrelated_comment_does_not_suppress() {
assert_eq!(
comment_pragma(&trivium_comment_pragma(
TriviaKind::LineComment,
"allow unwrap here, it cannot fail"
)),
None
);
}
#[test]
fn a_doc_comment_does_not_suppress() {
assert_eq!(
comment_pragma(&trivium_comment_pragma(
TriviaKind::DocComment,
"voltaic: allow unwrap"
)),
None
);
}
#[test]
fn the_unwrap_wording_is_plan_53_s() {
assert_eq!(
comment_pragma_message(Lint::Unwrap),
"`-- voltaic: allow unwrap` is deprecated — write `#[voltaic::allow(unwrap)]` on the line above instead (plan 53 §1.3)"
);
}
#[test]
fn each_lint_gets_its_own_spellings() {
let text = comment_pragma_message(Lint::Shadow);
assert!(text.contains("-- voltaic: allow shadow"));
assert!(text.contains("#[voltaic::allow(shadow)]"));
}
#[test]
fn it_names_the_shadowed_binding() {
assert!(shadow_message("cursor").starts_with("`cursor` is already bound"));
}
}