use derive_more::Display;
use serde::Deserialize;
use serde::de::value::{Error as ValueError, StrDeserializer};
use crate::report::WarningKind;
use crate::units::LineNo;
const MARKER: &str = "sarah-allow";
const FILE_SUFFIX: &str = "-file";
const COMMENT_LEADERS: &[char] = &['/', '*', '#', '-', '!', '<', ';', '%'];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Display)]
pub enum Scope {
#[display("function")]
Function,
#[display("file")]
File,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Directive {
pub line: LineNo,
pub scope: Scope,
pub kinds: Vec<WarningKind>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct AllowProblem {
pub line: LineNo,
pub message: String,
}
impl Scope {
pub(super) fn allowed_kinds(self) -> &'static [WarningKind] {
match self {
Self::Function => &[WarningKind::LargeFunction, WarningKind::DeepFunction],
Self::File => &[
WarningKind::LargeFile,
WarningKind::LargeFunction,
WarningKind::DeepFunction,
],
}
}
}
pub(super) struct DirectiveHelpers;
impl DirectiveHelpers {
pub(super) fn parse(line: LineNo, text: &str) -> Option<Result<Directive, AllowProblem>> {
let rest = text
.trim_start()
.trim_start_matches(COMMENT_LEADERS)
.trim_start()
.strip_prefix(MARKER)?;
let (scope, rest) = match rest.strip_prefix(FILE_SUFFIX) {
Some(rest) => (Scope::File, rest),
None => (Scope::Function, rest),
};
if rest.starts_with(|c: char| c.is_alphanumeric() || c == '_' || c == '-') {
return None;
}
let problem = |message: String| AllowProblem { line, message };
Some(
Self::parse_body(scope, rest)
.map(|kinds| Directive { line, scope, kinds })
.map_err(problem),
)
}
fn parse_body(scope: Scope, rest: &str) -> Result<Vec<WarningKind>, String> {
let (list, rest) = match rest.trim_start().strip_prefix('(') {
Some(inner) => {
let (list, after) = inner
.split_once(')')
.ok_or_else(|| "missing `)`".to_string())?;
(Some(list), after)
}
None => (None, rest),
};
if !rest.trim_start().starts_with(':') {
return Err(
"missing `: <reason>` (the reason may be empty, the `:` is required)".to_string(),
);
}
Self::parse_kinds(scope, list)
}
fn parse_kinds(scope: Scope, list: Option<&str>) -> Result<Vec<WarningKind>, String> {
let names: Vec<&str> = list
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.collect();
if names.is_empty() {
return Ok(scope.allowed_kinds().to_vec());
}
names
.into_iter()
.map(|name| Self::parse_kind(scope, name))
.collect()
}
fn parse_kind(scope: Scope, name: &str) -> Result<WarningKind, String> {
let kind = WarningKind::deserialize(StrDeserializer::<ValueError>::new(name))
.map_err(|_| format!("unknown warning kind `{name}`"))?;
if !scope.allowed_kinds().contains(&kind) {
return Err(format!("`{name}` cannot be allowed by a {scope} comment"));
}
Ok(kind)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(text: &str) -> Option<Result<Directive, AllowProblem>> {
DirectiveHelpers::parse(LineNo(3), text)
}
fn kinds(text: &str) -> Vec<WarningKind> {
parse(text).unwrap().unwrap().kinds
}
fn problem(text: &str) -> String {
parse(text).unwrap().unwrap_err().message
}
#[test]
fn parses_kinds_and_scope() {
let d = parse(" // sarah-allow(large_function, deep_function): state machine")
.unwrap()
.unwrap();
assert_eq!(d.line, LineNo(3));
assert_eq!(d.scope, Scope::Function);
assert_eq!(
d.kinds,
vec![WarningKind::LargeFunction, WarningKind::DeepFunction]
);
let file = parse("# sarah-allow-file(large_file): generated")
.unwrap()
.unwrap();
assert_eq!(file.scope, Scope::File);
assert_eq!(file.kinds, vec![WarningKind::LargeFile]);
}
#[test]
fn empty_reason_and_list_allow_every_kind_of_the_scope() {
assert_eq!(kinds("// sarah-allow:"), Scope::Function.allowed_kinds());
assert_eq!(kinds("-- sarah-allow():"), Scope::Function.allowed_kinds());
assert_eq!(
kinds("/* sarah-allow-file: */"),
Scope::File.allowed_kinds()
);
assert_eq!(
kinds(" * sarah-allow(deep_function) : x"),
[WarningKind::DeepFunction]
);
}
#[test]
fn rejects_bad_directives() {
assert!(problem("// sarah-allow(large_function)").contains("missing `: <reason>`"));
assert!(problem("// sarah-allow").contains("missing `: <reason>`"));
assert!(problem("// sarah-allow(large_function: x").contains("missing `)`"));
assert!(problem("// sarah-allow(huge): x").contains("unknown warning kind `huge`"));
assert!(
problem("// sarah-allow(large_file): x").contains("cannot be allowed by a function")
);
assert!(problem("# sarah-allow-file(folder_bad_name): x").contains("by a file"));
}
#[test]
fn ignores_non_directives() {
assert!(parse("let s = \"// sarah-allow: x\";").is_none());
assert!(parse("f(); // sarah-allow: x").is_none());
assert!(parse("// sarah-allowed: x").is_none());
assert!(parse("// sarah-allow-files: x").is_none());
assert!(parse("// about sarah-allow: x").is_none());
}
}