use std::rc::Rc;
use super::check::{CheckState, Warning};
use super::options::{NULLARY_INLINE_OPTIONS, VARIADIC_INLINE_OPTIONS, split, strip};
use super::pattern;
use super::stages::lua_str_tonumber;
use super::value::{LKey, LTable, LVal};
const LIMIT_OPTS: &[&str] = &[
"max_line_length",
"max_code_line_length",
"max_string_line_length",
"max_comment_line_length",
"max_cyclomatic_complexity",
];
#[derive(Clone, Debug)]
pub enum InlineOpts {
Table(Rc<LTable>),
Push,
Pop,
}
#[derive(Clone, Debug)]
pub struct InlineOption {
pub line: usize,
pub column: Option<usize>,
pub end_column: Option<usize>,
pub options: Option<InlineOpts>,
pub pop_count: Option<usize>,
}
fn is_variadic(name: &str) -> bool {
VARIADIC_INLINE_OPTIONS.iter().any(|(n, _)| *n == name)
}
fn is_valid_option_name(name: &str) -> bool {
if name == "std" || is_variadic(name) {
return true;
}
let name = name.strip_prefix("no_").unwrap_or(name);
NULLARY_INLINE_OPTIONS.contains(&name) || LIMIT_OPTS.contains(&name)
}
fn split_invocation(tokens: &[Vec<u8>]) -> Option<(String, Vec<Vec<u8>>)> {
let mut cur_name: Option<Vec<u8>> = None;
let mut last_valid: Option<(String, usize)> = None;
for (i, token) in tokens.iter().enumerate() {
let name = match cur_name.take() {
Some(mut n) => {
n.push(b'_');
n.extend_from_slice(token);
n
}
None => token.clone(),
};
if let Ok(s) = std::str::from_utf8(&name)
&& is_valid_option_name(s)
{
last_valid = Some((s.to_string(), i + 1));
}
cur_name = Some(name);
}
let (name, end) = last_valid?;
Some((name, tokens[end..].to_vec()))
}
fn unexpected_num_args(name: &str, args: &[Vec<u8>], expected: usize) -> String {
format!(
"inline option '{name}' expects {expected} argument{}, {} given",
if expected == 1 { "" } else { "s" },
args.len()
)
}
fn set(opts: &mut Vec<(LKey, LVal)>, key: &str, value: LVal) {
let key = LKey::Str(key.as_bytes().to_vec());
match opts.iter_mut().find(|(k, _)| *k == key) {
Some(entry) => entry.1 = value,
None => opts.push((key, value)),
}
}
fn strings(args: &[Vec<u8>]) -> LVal {
LVal::table(
args.iter().map(|a| LVal::Str(a.clone())).collect(),
Vec::new(),
)
}
fn parse_options(body: &[u8]) -> Result<Rc<LTable>, String> {
let mut opts: Vec<(LKey, LVal)> = Vec::new();
let parts = split(body, Some(b','));
for name_and_args in &parts {
let tokens = split(name_and_args, None);
let Some((name, args)) = split_invocation(&tokens) else {
if tokens.is_empty() {
return Err(if parts.len() == 1 {
"empty inline option".into()
} else {
"empty inline option invocation".into()
});
}
let joined: Vec<String> = tokens
.iter()
.map(|t| String::from_utf8_lossy(t).into_owned())
.collect();
return Err(format!("unknown inline option '{}'", joined.join(" ")));
};
if name == "std" {
if args.len() != 1 {
return Err(unexpected_num_args(&name, &args, 1));
}
set(&mut opts, "std", LVal::Str(args[0].clone()));
} else if name == "ignore" && args.is_empty() {
set(
&mut opts,
"ignore",
LVal::table(vec![LVal::str(".*")], Vec::new()),
);
} else if is_variadic(&name) {
set(&mut opts, &name, strings(&args));
} else {
let full_name = name.replace('_', " ");
let (name, flag) = match name.strip_prefix("no_") {
Some(rest) => (rest.to_string(), false),
None => (name.clone(), true),
};
if NULLARY_INLINE_OPTIONS.contains(&name.as_str()) {
if !args.is_empty() {
return Err(unexpected_num_args(&full_name, &args, 0));
}
set(&mut opts, &name, LVal::Bool(flag));
} else if flag {
if args.len() != 1 {
return Err(unexpected_num_args(&full_name, &args, 1));
}
let Some(value) = lua_str_tonumber(&args[0]) else {
return Err(format!("inline option '{name}' expects number as argument"));
};
set(&mut opts, &name, LVal::Num(value));
} else {
if !args.is_empty() {
return Err(unexpected_num_args(&full_name, &args, 0));
}
set(&mut opts, &name, LVal::Bool(false));
}
}
}
Ok(Rc::new(LTable::new(Vec::new(), opts)))
}
enum Parsed {
NotInline,
Options(InlineOpts, Option<InlineOpts>),
Error(String),
}
fn parse_inline_comment(contents: &[u8]) -> Parsed {
let stripped = strip(contents);
let Some(m) = pattern::find(stripped, b"^luacheck:", 1).expect("valid pattern") else {
return Parsed::NotInline;
};
let body = &stripped[m.end..];
let (without_parens, _) =
pattern::gsub(body, b"%b()", |_| Some(b" ".to_vec())).expect("valid pattern");
let body = strip(&without_parens).to_vec();
let mut opts2 = None;
let mut body_ref: &[u8] = &body;
let after_push = pattern::lua_match(&body, b"^push%s+(.*)", 1).expect("valid pattern");
let after_push_bytes;
if let Some(caps) = after_push {
after_push_bytes = caps[0].bytes(&body).to_vec();
opts2 = Some(InlineOpts::Push);
body_ref = &after_push_bytes;
} else if body == b"push" {
return Parsed::Options(InlineOpts::Push, None);
} else if body == b"pop" {
return Parsed::Options(InlineOpts::Pop, None);
}
match parse_options(body_ref) {
Ok(opts) => Parsed::Options(InlineOpts::Table(opts), opts2),
Err(err) => Parsed::Error(err),
}
}
const fn order(t: &InlineOption) -> u8 {
match t.options {
Some(InlineOpts::Push) => 1,
Some(InlineOpts::Pop) => 3,
_ => 2,
}
}
fn warn_column_range(st: &mut CheckState, code: &str, item: &InlineOption) {
let mut warning = Warning::new(code);
warning.line = item.line;
warning.column = item.column.unwrap_or(0);
warning.end_column = item.end_column.unwrap_or(0);
st.warnings.push(warning);
}
fn apply_boundaries(st: &mut CheckState, items: Vec<InlineOption>) -> Vec<InlineOption> {
let mut res: Vec<InlineOption> = Vec::new();
let mut res_last: Option<usize> = None;
let mut pushes: Vec<InlineOption> = Vec::new();
let mut push_option_counts: Vec<usize> = Vec::new();
let mut option_count = 0usize;
for item in items {
match item.options {
Some(InlineOpts::Push) => {
pushes.push(item);
push_option_counts.push(option_count);
}
Some(InlineOpts::Pop) => {
let top_is_function = pushes.last().is_some_and(|p| p.end_column.is_none());
if pushes.is_empty() || (item.end_column.is_some() && top_is_function) {
warn_column_range(st, "023", &item);
continue;
}
if item.end_column.is_none() {
while pushes.last().is_some_and(|p| p.end_column.is_some()) {
let top = pushes.pop().expect("push");
warn_column_range(st, "022", &top);
push_option_counts.pop();
}
}
pushes.pop();
let prev_option_count = push_option_counts.pop().expect("push count");
let pop_count = option_count - prev_option_count;
if pop_count > 0 {
let line = item.line + 1;
match res_last {
Some(i) if res[i].line == line => {
res[i].pop_count = Some(res[i].pop_count.unwrap_or(0) + pop_count);
}
_ => {
res.push(InlineOption {
line,
column: None,
end_column: None,
options: None,
pop_count: Some(pop_count),
});
res_last = Some(res.len() - 1);
}
}
}
option_count = prev_option_count;
}
_ => {
let line = item.line;
match res_last {
Some(i) if res[i].line == line => {
res[i].options.clone_from(&item.options);
res[i].column = item.column;
res[i].end_column = item.end_column;
}
_ => {
res.push(item);
res_last = Some(res.len() - 1);
}
}
if st.code_lines.contains_key(&line) {
res.push(InlineOption {
line: line + 1,
column: None,
end_column: None,
options: None,
pop_count: Some(1),
});
res_last = Some(res.len() - 1);
} else {
option_count += 1;
}
}
}
}
while let Some(top) = pushes.pop() {
warn_column_range(st, "022", &top);
}
res
}
pub fn run(st: &mut CheckState) {
let mut items: Vec<InlineOption> = Vec::new();
let comments: Vec<_> = st
.comments
.iter()
.map(|c| (c.contents.clone(), c.range))
.collect();
for (contents, range) in comments {
let column = Some(st.offset_to_column(range.line, range.offset));
let end_column = Some(st.offset_to_column(range.line, range.end_offset));
match parse_inline_comment(&contents) {
Parsed::NotInline => {}
Parsed::Options(opts1, opts2) => {
items.push(InlineOption {
line: range.line,
column,
end_column,
options: Some(opts1),
pop_count: None,
});
if let Some(opts2) = opts2 {
items.push(InlineOption {
line: range.line,
column,
end_column,
options: Some(opts2),
pop_count: None,
});
}
}
Parsed::Error(msg) => {
let mut warning = Warning::new("021");
warning.msg = Some(msg.into_bytes());
st.warn_range(warning, range);
}
}
}
for &line in &st.lines[st.top_line].lines.clone() {
let fn_node = st.lines[line].node;
let range = st.ast.range(fn_node);
let end_range = st.ast.nodes[fn_node].end_range.expect("function end");
items.push(InlineOption {
line: range.line,
column: Some(st.offset_to_column(range.line, range.offset)),
end_column: None,
options: Some(InlineOpts::Push),
pop_count: None,
});
items.push(InlineOption {
line: end_range.line,
column: Some(st.offset_to_column(end_range.line, end_range.offset)),
end_column: None,
options: Some(InlineOpts::Pop),
pop_count: None,
});
}
items.sort_by(|a, b| {
a.line
.cmp(&b.line)
.then_with(|| order(a).cmp(&order(b)))
.then_with(|| a.column.cmp(&b.column))
});
st.inline_options = apply_boundaries(st, items);
}