use brink_syntax::ast::{self, AstNode};
use brink_syntax::{SyntaxKind, SyntaxNode};
use rowan::TextRange;
use super::context::LowerSink;
use crate::DiagnosticCode;
#[derive(Debug, Clone)]
pub(super) struct ParsedDirective {
pub name: String,
pub bare: bool,
pub dynamic: bool,
pub range: TextRange,
}
pub(super) enum TagLineClass {
Plain,
Directives(Vec<ParsedDirective>),
Mixed,
}
pub(super) fn parse_directive_tag(tag: &ast::Tag) -> Option<ParsedDirective> {
let mut text = String::new();
let mut dynamic = false;
let mut first = true;
for child in tag.syntax().children_with_tokens() {
match child {
rowan::NodeOrToken::Token(tok) => {
if first && tok.kind() == SyntaxKind::HASH {
first = false;
continue;
}
first = false;
text.push_str(tok.text());
}
rowan::NodeOrToken::Node(_) => {
first = false;
dynamic = true;
}
}
}
let trimmed = text.trim();
let rest = trimmed.strip_prefix('@')?;
let name: String = rest
.chars()
.take_while(|c| *c != '(' && !c.is_whitespace())
.collect();
let bare = !dynamic && rest.len() == name.len();
Some(ParsedDirective {
name,
bare,
dynamic,
range: tag.syntax().text_range(),
})
}
pub(super) fn scan_tag_line(tl: &ast::TagLine) -> TagLineClass {
let mut directives = Vec::new();
let mut plain = 0usize;
if let Some(tags) = tl.tags() {
for tag in tags.tags() {
match parse_directive_tag(&tag) {
Some(d) => directives.push(d),
None => plain += 1,
}
}
}
if directives.is_empty() {
TagLineClass::Plain
} else if plain == 0 && directives.len() == 1 {
TagLineClass::Directives(directives)
} else {
TagLineClass::Mixed
}
}
fn is_trivia(kind: SyntaxKind) -> bool {
kind.is_trivia() || kind == SyntaxKind::NEWLINE
}
fn is_attachable_decl(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::VAR_DECL
| SyntaxKind::CONST_DECL
| SyntaxKind::LIST_DECL
| SyntaxKind::EXTERNAL_DECL
)
}
fn is_directive_line(node: &SyntaxNode) -> bool {
ast::TagLine::cast(node.clone())
.is_some_and(|tl| matches!(scan_tag_line(&tl), TagLineClass::Directives(_)))
}
pub(super) fn attached_declaration(tl: &ast::TagLine) -> Option<SyntaxNode> {
let mut cursor = tl.syntax().next_sibling_or_token();
while let Some(el) = cursor {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
return None;
}
cursor = tok.next_sibling_or_token();
}
rowan::NodeOrToken::Node(node) => {
if node.kind() == SyntaxKind::EMPTY_LINE || is_directive_line(&node) {
cursor = node.next_sibling_or_token();
continue;
}
return is_attachable_decl(node.kind()).then_some(node);
}
}
}
None
}
pub(super) fn in_leading_body_run(tl: &ast::TagLine) -> bool {
let Some(parent) = tl.syntax().parent() else {
return false;
};
if !matches!(
parent.kind(),
SyntaxKind::KNOT_BODY | SyntaxKind::STITCH_BODY
) {
return false;
}
let mut cursor = tl.syntax().prev_sibling_or_token();
while let Some(el) = cursor {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
return false;
}
cursor = tok.prev_sibling_or_token();
}
rowan::NodeOrToken::Node(node) => {
if !matches!(node.kind(), SyntaxKind::TAG_LINE | SyntaxKind::EMPTY_LINE) {
return false;
}
cursor = node.prev_sibling_or_token();
}
}
}
true
}
pub(super) fn is_consumed_position(tl: &ast::TagLine) -> bool {
attached_declaration(tl).is_some() || in_leading_body_run(tl)
}
pub(super) fn directives_before(node: &SyntaxNode) -> Vec<ParsedDirective> {
let mut collected: Vec<ParsedDirective> = Vec::new();
let mut cursor = node.prev_sibling_or_token();
while let Some(el) = cursor {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
break;
}
cursor = tok.prev_sibling_or_token();
}
rowan::NodeOrToken::Node(n) => {
if n.kind() == SyntaxKind::EMPTY_LINE {
cursor = n.prev_sibling_or_token();
continue;
}
let Some(tl) = ast::TagLine::cast(n.clone()) else {
break;
};
let TagLineClass::Directives(dirs) = scan_tag_line(&tl) else {
break;
};
for d in dirs.into_iter().rev() {
collected.insert(0, d);
}
cursor = n.prev_sibling_or_token();
}
}
}
collected
}
pub(super) fn leading_body_directives(body: &SyntaxNode) -> Vec<ParsedDirective> {
let mut collected = Vec::new();
for el in body.children_with_tokens() {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
break;
}
}
rowan::NodeOrToken::Node(node) => {
if node.kind() == SyntaxKind::EMPTY_LINE {
continue;
}
let Some(tl) = ast::TagLine::cast(node) else {
break;
};
if let TagLineClass::Directives(dirs) = scan_tag_line(&tl)
&& attached_declaration(&tl).is_none()
{
collected.extend(dirs);
}
}
}
}
collected
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DirectiveTarget {
Var,
Const,
List,
External,
Knot,
Stitch,
}
impl DirectiveTarget {
fn supports_local(self) -> bool {
matches!(self, Self::Var | Self::Knot | Self::Stitch)
}
}
pub(super) fn apply_scope_directives(
dirs: &[ParsedDirective],
target: DirectiveTarget,
sink: &mut impl LowerSink,
) -> bool {
let mut is_local = false;
for d in dirs {
if d.dynamic {
sink.diagnose(d.range, DiagnosticCode::E046);
} else if d.name != "local" {
sink.diagnose(d.range, DiagnosticCode::E044);
} else if !d.bare {
sink.diagnose(d.range, DiagnosticCode::E050);
} else if !target.supports_local() {
sink.diagnose(d.range, DiagnosticCode::E049);
} else if is_local {
sink.diagnose(d.range, DiagnosticCode::E048);
} else {
is_local = true;
}
}
is_local
}