use proc_macro2::TokenStream;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned as _;
use syn::{Attribute, Meta, Token};
use super::directive::build;
use super::scopes::Scopes;
use super::{Directive, Intent};
use crate::Result;
use crate::cfg::CfgSet;
use crate::error::Error;
use crate::model::Channel;
use crate::parse::{CommentKind, SourceFile};
const STATED_VALUE: &str = "value";
pub fn directives(file: &SourceFile) -> Result<Vec<Directive>> {
directives_for(file, &CfgSet::unconditional())
}
pub(crate) fn directives_for(file: &SourceFile, cfg: &CfgSet) -> Result<Vec<Directive>> {
let scopes = Scopes::of(file);
let mut found = attribute_directives(file, &scopes, cfg)?;
found.extend(comment_directives(file, &scopes)?);
found.sort_by_key(|directive| directive.scope.start);
Ok(found)
}
fn attribute_directives(file: &SourceFile, scopes: &Scopes, cfg: &CfgSet) -> Result<Vec<Directive>> {
let mut found = Vec::new();
for (attribute, item_span) in &scopes.attributes {
let line = file.line_of(attribute.span().byte_range().start);
for (path, arguments) in unwrap_cfg_attr(attribute, cfg) {
let segments: Vec<String> = path.segments.iter().map(|segment| segment.ident.to_string()).collect();
let (channel, intent) = match segments.as_slice() {
[namespace] if namespace == "gamma" => (Channel::Attribute, None),
[namespace, name] if namespace == "gamma" => {
if name == STATED_VALUE {
continue;
}
if name == "test_timeout_multiplier" || name == "timeout_multiplier" {
(Channel::Attribute, None)
} else if let Some(intent) = Intent::parse(name) {
(Channel::Attribute, Some(intent))
} else {
return Err(Error::new(format!(
"{}:{line}: unknown directive `{namespace}::{name}`, expected `skip`, `expect_survived`, `expect_killed`, `test_timeout_multiplier`, or `timeout_multiplier`",
file.path()
))
.usage());
}
}
_ => continue,
};
let directive = build(intent, &arguments, channel, line, item_span.clone(), file)?;
if directive.intent.is_some() || directive.test_timeout_multiplier.is_some() {
found.push(directive);
}
}
}
Ok(found)
}
fn unwrap_cfg_attr(attribute: &Attribute, cfg: &CfgSet) -> Vec<(syn::Path, TokenStream)> {
unwrap_meta(&attribute.meta, cfg)
}
fn unwrap_meta(meta: &Meta, cfg: &CfgSet) -> Vec<(syn::Path, TokenStream)> {
if !meta.path().is_ident("cfg_attr") {
let arguments = match meta {
Meta::List(list) => list.tokens.clone(),
_ => TokenStream::new(),
};
return vec![(meta.path().clone(), arguments)];
}
let Meta::List(list) = meta else {
return Vec::new();
};
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
let Ok(parts) = parser.parse2(list.tokens.clone()) else {
return Vec::new();
};
let Some(predicate) = parts.first() else {
return Vec::new();
};
if cfg.decide_meta(predicate) == cargo_gamma_engine::cfg::Verdict::No {
return Vec::new();
}
parts.into_iter().skip(1).flat_map(|meta| unwrap_meta(&meta, cfg)).collect()
}
fn comment_directives(file: &SourceFile, scopes: &Scopes) -> Result<Vec<Directive>> {
let mut found = Vec::new();
for comment in file.comments() {
if comment.kind != CommentKind::Line {
continue;
}
let body = file.slice(&comment.body);
let Some(source) = directive_source(body) else {
continue;
};
if crate::parse::exceeds_nesting_limit(&source) {
return Err(Error::new(format!(
"{}:{}: `{body}` nests too deeply to be safely parsed as a directive",
file.path(),
comment.line
))
.usage());
}
let parser = Attribute::parse_outer;
let attributes = Parser::parse_str(parser, &source).map_err(|error| {
Error::new(format!(
"{}:{}: `{body}` is not a well-formed directive: {error}",
file.path(),
comment.line
))
.usage()
})?;
let mut recognized = false;
for attribute in &attributes {
let segments: Vec<String> = attribute.path().segments.iter().map(|segment| segment.ident.to_string()).collect();
let (channel, intent) = match segments.as_slice() {
[namespace] if namespace == "gamma" => (Channel::Comment, None),
[namespace, name] if namespace == "gamma" => {
if name == STATED_VALUE {
return Err(Error::new(format!(
"{}:{}: `{namespace}::{name}` states the value a function returns and must be written as a real attribute on that function, not as a comment",
file.path(), comment.line
))
.usage());
}
if name == "test_timeout_multiplier" || name == "timeout_multiplier" {
(Channel::Comment, None)
} else if let Some(intent) = Intent::parse(name) {
(Channel::Comment, Some(intent))
} else {
return Err(Error::new(format!(
"{}:{}: unknown directive `{namespace}::{name}`, expected `skip`, `expect_survived`, `expect_killed`, `test_timeout_multiplier`, or `timeout_multiplier`",
file.path(), comment.line
))
.usage());
}
}
_ => continue,
};
let arguments = match &attribute.meta {
Meta::List(list) => list.tokens.clone(),
_ => TokenStream::new(),
};
let scope = if comment.trailing {
scopes.enclosing_on_line(comment.line)
} else {
scopes.following(comment.span.end)
};
let Some(scope) = scope else {
return Err(Error::new(format!("{}:{}: `{body}` does not apply to anything", file.path(), comment.line)).usage());
};
let directive = build(intent, &arguments, channel, comment.line, scope, file)?;
if directive.intent.is_some() || directive.test_timeout_multiplier.is_some() {
found.push(directive);
recognized = true;
}
}
if !recognized {
return Err(Error::new(format!("{}:{}: `{body}` is not a recognized directive", file.path(), comment.line)).usage());
}
}
Ok(found)
}
fn directive_source(body: &str) -> Option<String> {
let inner = body.strip_prefix("#[").map_or(body, str::trim_start);
if body.starts_with("#[") && (inner.starts_with("gamma::") || inner.starts_with("gamma(") || inner.starts_with("gamma]")) {
return Some(body.to_owned());
}
None
}