diff --git a/Cargo.lock b/Cargo.lock index d2b4856..f571d9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -284,6 +284,15 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "logic_lint" +version = "0.1.0" +dependencies = [ + "lazy-regex", + "tag_code", + "var_utils", +] + [[package]] name = "memchr" version = "2.5.0" @@ -292,9 +301,10 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mindustry_logic_bang_lang" -version = "0.14.17" +version = "0.14.18" dependencies = [ "display_source", + "logic_lint", "parser", "syntax", "tag_code", @@ -534,7 +544,7 @@ dependencies = [ [[package]] name = "tag_code" -version = "0.1.3" +version = "0.1.4" [[package]] name = "term" diff --git a/Cargo.toml b/Cargo.toml index 9e5bcd7..e254405 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mindustry_logic_bang_lang" -version = "0.14.17" +version = "0.14.18" edition = "2021" authors = ["A4-Tacks "] @@ -19,6 +19,7 @@ members = [ "./tools/tag_code", "./tools/utils", "./tools/var_utils", + "./tools/logic_lint", ] [dependencies] @@ -26,6 +27,7 @@ tag_code = { path = "./tools/tag_code", version = "*" } display_source = { path = "./tools/display_source", version = "*" } parser = { path = "./tools/parser", version = "*" } syntax = { path = "./tools/syntax", version = "*" } +logic_lint = { path = "./tools/logic_lint", version = "*" } [profile.release] strip = true diff --git a/src/main.rs b/src/main.rs index 145da06..b5a05dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ use parser::{ }, }; use tag_code::TagCodes; +use logic_lint::Source; /// 带有错误前缀, 并且文本为红色的eprintln macro_rules! err { @@ -62,6 +63,7 @@ pub const HELP_MSG: &str = concat_lines! { "\t", "r: compile MdtLogicCode to MdtBangLang"; "\t", "R: compile MdtLogicCode to MdtBangLang (Builded TagDown)"; "\t", "C: compile MdtTagCode to MdtLogicCode"; + "\t", "l: lint MdtLogicCode"; ; "input from stdin"; "output to stdout"; @@ -98,7 +100,7 @@ fn main() { ); let mut src = read_stdin(); for mode in modes { - src = mode.compile(&src) + src = mode.compile(src) } println!("{src}") } @@ -111,33 +113,34 @@ enum CompileMode { MdtLogicToMdtTagCode { tag_down: bool }, MdtLogicToBang { tag_down: bool }, MdtTagCodeToMdtLogic, + LintLogic, } impl CompileMode { - fn compile(&self, src: &str) -> String { + fn compile(&self, src: String) -> String { match *self { Self::BangToMdtLogic => { - let ast = build_ast(src); + let ast = build_ast(&src); let mut meta = compile_ast(ast); build_tag_down(&mut meta); let logic_lines = meta.tag_codes_mut().compile().unwrap(); logic_lines.join("\n") }, Self::BangToASTDebug => { - let ast = build_ast(src); + let ast = build_ast(&src); format!("{ast:#?}") }, Self::BangToASTDisplay => { - let ast = build_ast(src); + let ast = build_ast(&src); display_ast(&ast) }, Self::BangToMdtTagCode { tag_down } => { - let ast = build_ast(src); + let ast = build_ast(&src); let mut meta = compile_ast(ast); if tag_down { build_tag_down(&mut meta); } meta.tag_codes().to_string() }, Self::MdtLogicToMdtTagCode { tag_down } => { - match TagCodes::from_str(src) { + match TagCodes::from_str(&src) { Ok(mut lines) => { if tag_down { lines.build_tagdown().unwrap(); @@ -152,7 +155,7 @@ impl CompileMode { } }, Self::MdtLogicToBang { tag_down } => { - match TagCodes::from_str(src) { + match TagCodes::from_str(&src) { Ok(mut lines) => { if tag_down { lines.build_tagdown().unwrap(); @@ -183,12 +186,17 @@ impl CompileMode { } }, Self::MdtTagCodeToMdtLogic => { - let tag_codes = TagCodes::from_tag_lines(src); + let tag_codes = TagCodes::from_tag_lines(&src); let mut meta = CompileMeta::with_tag_codes(tag_codes); build_tag_down(&mut meta); let logic_lines = meta.tag_codes_mut().compile().unwrap(); logic_lines.join("\n") }, + Self::LintLogic => { + let linter = Source::from_str(&src); + linter.show_lints(); + src + }, } } } @@ -207,6 +215,7 @@ impl TryFrom for CompileMode { 'r' => Self::MdtLogicToBang { tag_down: false }, 'R' => Self::MdtLogicToBang { tag_down: true }, 'C' => Self::MdtTagCodeToMdtLogic, + 'l' => Self::LintLogic, mode => return Err(mode), }) } @@ -337,7 +346,14 @@ fn unwrap_parse_err(result: ParseResult<'_>, src: &str) -> Expand { let [loc] = get_locations(src, [location]); let view = &src[ location - ..src.len().min(location+MAX_INVALID_TOKEN_VIEW) + .. + src.len().min( + src[location..] + .char_indices() + .map(|(i, _ch)| location+i) + .take(MAX_INVALID_TOKEN_VIEW+1) + .last() + .unwrap_or(location)) ]; err!( "在位置 {:?} 处找到无效的令牌: {:?}", diff --git a/tools/logic_lint/Cargo.toml b/tools/logic_lint/Cargo.toml new file mode 100644 index 0000000..f21fb82 --- /dev/null +++ b/tools/logic_lint/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "logic_lint" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy-regex = "3.0.2" +tag_code = { path = "../tag_code", version = "*" } +var_utils = { path = "../var_utils", version = "*" } diff --git a/tools/logic_lint/src/lib.rs b/tools/logic_lint/src/lib.rs new file mode 100644 index 0000000..2d5f0fa --- /dev/null +++ b/tools/logic_lint/src/lib.rs @@ -0,0 +1,147 @@ +pub mod lints; + +use core::fmt; +use std::{borrow::Cow, ops::Deref}; + +use crate::lints::{ShowLint, Lint}; +use tag_code::mdt_logic_split_unwraped; + +const LIGHT_ARGS_BEGIN: &str = "\x1b[7m"; +const LIGHT_ARGS_END: &str = "\x1b[27m"; + +#[derive(Debug)] +pub struct Line<'a> { + args: Vec>, +} +impl<'a> Line<'a> { + pub fn from_line(lineno: usize, s: &'a str) -> Self { + let logic_args = mdt_logic_split_unwraped(s); + assert_ne!( + logic_args.len(), 0, + "line args count by zero ({})", lineno); + let args = logic_args + .into_iter() + .enumerate() + .map(|(i, arg)| Var::new(lineno, i, arg)) + .collect(); + + Self { args } + } + + pub fn hint_args(&self, hints: &[usize]) -> Vec> { + self.args().into_iter() + .enumerate() + .map(|(i, arg)| if hints.contains(&i) { + format!( + "{}{}{}", + LIGHT_ARGS_BEGIN, + arg.value(), + LIGHT_ARGS_END, + ).into() + } else { + arg.value().into() + }) + .collect() + } + + pub fn lint(&'a self, src: &'a Source<'_>) -> Vec { + lints::lint(src, self) + } + + pub fn lineno(&self) -> usize { + self.args.first().unwrap().lineno + } + + pub fn args(&self) -> &[Var<'_>] { + self.args.as_ref() + } +} + +#[derive(Debug)] +pub struct Source<'a> { + lines: Vec>, +} +impl<'a> Source<'a> { + pub fn from_str(s: &'a str) -> Self { + let lines = s.lines().enumerate() + .map(|(lineno, line)| Line::from_line(lineno, line)) + .collect(); + + Self { + lines, + } + } + + /// 返回指定行周围的行, 而不包括指定行 + pub fn view_lines( + &self, + lineno: usize, + rng: (usize, usize), + ) -> (&[Line<'_>], &[Line<'_>]) { + let (head, tail) = ( + &self.lines[..lineno], + &self.lines[lineno+1..], + ); + let head = &head[ + (head.len().checked_sub(rng.0).unwrap_or_default())..]; + let tail = &tail[..rng.1.min(tail.len())]; + (head, tail) + } + + pub fn lint(&self) -> Vec { + self.lines.iter() + .map(|line| line.lint(self)) + .flatten() + .collect() + } + + pub fn lines(&self) -> &[Line<'_>] { + self.lines.as_ref() + } + + pub fn show_lints(&self) { + struct LintFmtter<'a>(&'a Source<'a>, &'a Lint<'a>); + impl fmt::Display for LintFmtter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.1.show_lint(self.0, f) + } + } + for lint in self.lint() { + let fmtter = LintFmtter(self, &lint); + eprintln!("{}", fmtter) + } + } +} + +#[derive(Debug)] +pub struct Var<'a> { + lineno: usize, + arg_idx: usize, + value: &'a str, +} + +impl<'a> Deref for Var<'a> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.value() + } +} + +impl<'a> Var<'a> { + pub fn new(lineno: usize, arg_idx: usize, value: &'a str) -> Self { + Self { lineno, arg_idx, value } + } + + pub fn value(&self) -> &str { + self.value + } + + pub fn arg_idx(&self) -> usize { + self.arg_idx + } + + pub fn lineno(&self) -> usize { + self.lineno + } +} diff --git a/tools/logic_lint/src/lints.rs b/tools/logic_lint/src/lints.rs new file mode 100644 index 0000000..6003bac --- /dev/null +++ b/tools/logic_lint/src/lints.rs @@ -0,0 +1,387 @@ +use core::fmt; + +use lazy_regex::regex_is_match; +use var_utils::{AsVarType, VarType}; + +use crate::{Var, Source}; + +macro_rules! color_str { + ($fnum:literal $($num:literal)* : $str:literal) => { + concat!( + "\x1b[", + stringify!($fnum), + $(";", stringify!($num), )* + "m", + $str, + "\x1b[0m", + ) + }; +} +macro_rules! make_lints { + { + $lint_vis:vis fn $lint_name:ident<$lifetime:lifetime>($src:ident, $line:ident) -> $res_ty:ty; + let $lints:ident; + $( + $(|)? $($prefix:literal)|+ ($argc:literal) $body:block + )* + } => { + $lint_vis fn $lint_name<$lifetime>( + $src: &$lifetime $crate::Source<$lifetime>, + $line: &$lifetime $crate::Line<$lifetime>, + ) -> Vec<$res_ty> { + let mut $lints = Vec::<$res_ty>::new(); + match $line.args() { + $( + [$crate::Var { value: $($prefix)|+, .. }, ..] => { + $lints.extend(check_argc($src, $line, $argc)); + $body + }, + )* + [] => unreachable!(), + [_, ..] => (), + } + $lints + } + }; +} + +fn check_assigh_var<'a>( + src: &'a crate::Source<'a>, + line: &'a crate::Line<'a>, + var: &'a Var<'a>, +) -> Option> { + match var.as_var_type() { + VarType::String(_) | VarType::Number(_) => { + Lint::new(var, WarningLint::AssignLiteral).into() + }, + VarType::Var(_) => check_var(src, line, var), + } +} +fn check_var<'a>( + _src: &'a crate::Source<'a>, + _line: &'a crate::Line<'a>, + var: &'a Var<'a>, +) -> Option> { + match var.value() { + "__" => Lint::new( + var, + WarningLint::UsedDoubleUnderline).into(), + s if regex_is_match!(r"^_\d+$", s) => Lint::new( + var, + WarningLint::UsedRawArgs).into(), + s if !s.is_empty() && s.chars().next().unwrap().is_uppercase() => { + Lint::new( + var, + WarningLint::SuspectedConstant + ).into() + }, + _ => None, + } +} +fn check_vars<'a>( + src: &'a crate::Source<'a>, + line: &'a crate::Line<'a>, + vars: impl IntoIterator> + 'a, +) -> impl Iterator> + 'a { + vars.into_iter() + .filter_map(|var| check_var(src, line, var)) +} +fn check_argc<'a>( + _src: &'a crate::Source<'a>, + line: &'a crate::Line<'a>, + expected: usize, +) -> Option> { + let len = line.args().len() - 1; + if len == expected { + return None; + } + Lint::new( + line.args().first().unwrap(), + WarningLint::ArgsCountNotMatch { + expected, + found: len, + } + ).into() +} + +const OP_METHODS: &[&str] = &[ + "add", "sub", "mul", "div", "idiv", "mod", + "pow", "equal", "notEqual", "land", "lessThan", "lessThanEq", + "greaterThan", "greaterThanEq", "strictEqual", "shl", "shr", "or", + "and", "xor", "not", "max", "min", "angle", + "angleDiff", "len", "noise", "abs", "log", "log10", + "floor", "ceil", "sqrt", "rand", "sin", "cos", + "tan", "asin", "acos", "atan", +]; +const UNIT_CONTROL_METHODS: &[&str] = &[ + "idle", "stop", "move", "approach", "pathfind", + "autoPathfind", "boost", "target", "targetp", "itemDrop", + "itemTake", "payDrop", "payTake", "payEnter", "mine", + "flag", "build", "getBlock", "within", "unbind", +]; + +make_lints! { + pub fn lint<'a>(src, line) -> Lint<'a>; + let lints; + "set" | "getlink" (2) { + if let [_, result, ..] = line.args() { + lints.extend(check_assigh_var(src, line, result)) + } + if let [_, _, var, ..] = line.args() { + lints.extend(check_vars(src, line, [var])) + } + } + "op" (4) { + if let [_, oper, ..] = line.args() { + if !OP_METHODS.contains(&oper.value()) { + lints.push(Lint::new(oper, ErrorLint::InvalidOper)) + } + } + if let [_, _, result, ..] = line.args() { + lints.extend(check_assigh_var(src, line, result)) + } + if let [_, _, _, var, var1, ..] = line.args() { + lints.extend(check_vars(src, line, [var, var1])) + } + } + "lookup" (3) { + if let [_, mode, result, index, ..] = line.args() { + match mode.value() { + "block" | "unit" | "item" | "liquid" => (), + _ => lints.push(Lint::new(mode, ErrorLint::InvalidOper)), + } + lints.extend(check_assigh_var(src, line, result)); + lints.extend(check_vars(src, line, [index])); + } + } + "ucontrol" (6) { + if let [_, oper, args @ ..] = line.args() { + if !UNIT_CONTROL_METHODS.contains(&oper.value()) { + lints.push(Lint::new(oper, ErrorLint::InvalidOper)) + } + lints.extend(check_vars(src, line, args)); + } + } + "ulocate" (8) { + if let [_, mode, btype, args @ ..] = line.args() { + match mode.value() { + "building" | "ore" | "spawn" | "damaged" => (), + _ => lints.push(Lint::new(mode, ErrorLint::InvalidOper)), + } + match btype.value() { + | "core" | "storage" | "generator" | "turret" | "factory" + | "repair" | "rally" | "battery" | "reactor" => (), + _ => lints.push(Lint::new(btype, ErrorLint::InvalidOper)), + } + lints.extend(check_vars(src, line, args)) + } + } + "end" | "stop" (0) {} + "print" | "printflush" | "drawflush" | "wait" | "ubind" (1) { + if let [_, var, ..] = line.args() { + lints.extend(check_vars(src, line, [var])) + } + } + "packcolor" (5) { + if let [_, result, args @ ..] = line.args() { + lints.extend(check_assigh_var(src, line, result)); + lints.extend(check_vars(src, line, args)); + } + } + "control" (6) { + if let [_, mode, args @ ..] = line.args() { + match mode.value() { + "enabled" | "shoot" | "shootp" | "config" | "color" => (), + _ => lints.push(Lint::new(mode, ErrorLint::InvalidOper)), + } + lints.extend(check_vars(src, line, args)); + } + } + "read" (3) { + if let [_, result, args @ ..] = line.args() { + lints.extend(check_assigh_var(src, line, result)); + lints.extend(check_vars(src, line, args)); + } + } + "draw" (7) { + if let [_, mode, args @ ..] = line.args() { + match mode.value() { + | "clear" | "color" | "col" | "stroke" | "line" | "rect" + | "lineRect" | "poly" | "linePoly" | "triangle" | "image" + => (), + _ => lints.push(Lint::new(mode, ErrorLint::InvalidOper)), + } + lints.extend(check_vars(src, line, args)); + } + } + "write" (3) { + if let [_, args @ ..] = line.args() { + lints.extend(check_vars(src, line, args)); + } + } + "radar" | "uradar" (7) { + fn check_filter<'a>(arg: &'a Var<'a>) -> Option> { + match arg.value() { + | "any" | "enemy" | "ally" | "player" | "attacker" + | "flying" | "boss" | "ground" + => None, + | _ + => Lint::new(arg, ErrorLint::InvalidOper).into(), + } + } + if let [_, filt1, filt2, filt3, order, from, rev, result] + = line.args() { + lints.extend(check_filter(filt1)); + lints.extend(check_filter(filt2)); + lints.extend(check_filter(filt3)); + lints.extend(check_vars(src, line, [order, from, rev])); + lints.extend(check_assigh_var(src, line, result)); + } + } +} + +pub trait ShowLint { + fn show_lint( + &self, + src: &Source<'_>, + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result; +} + +#[derive(Debug)] +pub struct Lint<'a> { + arg: &'a Var<'a>, + msg: LintType, +} +impl<'a> Lint<'a> { + pub fn new(arg: &'a Var<'a>, msg: impl Into) -> Self { + Self { arg, msg: msg.into() } + } +} +impl ShowLint for Lint<'_> { + fn show_lint( + &self, + src: &Source<'_>, + f: &mut fmt::Formatter<'_>, + ) -> Result<(), std::fmt::Error> { + let lineno = self.arg.lineno(); + let arg_idx = self.arg.arg_idx(); + write!( + f, concat!("{} ", color_str!(33: "[{}@{}]"), ": "), + self.msg.lint_type(), + lineno, + arg_idx, + )?; + self.msg.show_lint(src, f)?; + write!(f, "\n")?; + + let (prelines, suflines) + = src.view_lines(lineno, (2, 2)); + + macro_rules! show_lines { + ($lines:expr) => {{ + for line in $lines { + writeln!(f, " {}", line.hint_args(&[]).join(" "))?; + } + }}; + } + + let args = src.lines()[lineno].hint_args(&[arg_idx]); + show_lines!(prelines); + writeln!(f, concat!(color_str!(1 92: "==>"), " {}"), args.join(" "))?; + show_lines!(suflines); + Ok(()) + } +} + +#[derive(Debug)] +pub enum LintType { + Warning(WarningLint), + Error(ErrorLint), +} +impl LintType { + pub fn lint_type(&self) -> &'static str { + match self { + LintType::Warning(_) => color_str!(1 93: "Warning"), + LintType::Error(_) => color_str!(1 91: "Error"), + } + } +} +impl From for LintType { + fn from(value: WarningLint) -> Self { + Self::Warning(value) + } +} +impl From for LintType { + fn from(value: ErrorLint) -> Self { + Self::Error(value) + } +} +impl ShowLint for LintType { + fn show_lint( + &self, + src: &Source<'_>, + f: &mut fmt::Formatter<'_>, + ) -> Result<(), std::fmt::Error> { + match self { + LintType::Warning(warn) => warn.show_lint(src, f), + LintType::Error(err) => err.show_lint(src, f), + } + } +} + +#[derive(Debug)] +pub enum WarningLint { + /// 显式使用双下划线名称 + UsedDoubleUnderline, + /// 直接使用未被替换的参数调用协定, 如`_0` + UsedRawArgs, + /// 参数数量不匹配 + ArgsCountNotMatch { + expected: usize, + found: usize, + }, + /// 向字面量赋值 + AssignLiteral, + /// 从命名来看疑似是未被替换的常量 + SuspectedConstant, +} +impl ShowLint for WarningLint { + fn show_lint( + &self, + _src: &Source<'_>, + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + match self { + WarningLint::UsedDoubleUnderline => write!(f, "使用了双下划线")?, + WarningLint::UsedRawArgs => write!(f, "使用了参数协定的原始格式")?, + WarningLint::ArgsCountNotMatch { expected, found } => { + write!( + f, + "不合预期的参数个数, 期待{expected}个, 得到{found}个")? + }, + WarningLint::AssignLiteral => write!(f, "对字面量进行操作")?, + WarningLint::SuspectedConstant => { + write!(f, "命名疑似未被替换的常量")? + }, + } + Ok(()) + } +} + +#[derive(Debug)] +pub enum ErrorLint { + InvalidOper, +} +impl ShowLint for ErrorLint { + fn show_lint( + &self, + _src: &Source<'_>, + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + match self { + ErrorLint::InvalidOper => write!(f, "无效的操作符")?, + } + Ok(()) + } +} diff --git a/tools/tag_code/Cargo.toml b/tools/tag_code/Cargo.toml index 5e2410c..e30e3d7 100644 --- a/tools/tag_code/Cargo.toml +++ b/tools/tag_code/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tag_code" -version = "0.1.3" +version = "0.1.4" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/tools/tag_code/src/lib.rs b/tools/tag_code/src/lib.rs index 30593b5..5858746 100644 --- a/tools/tag_code/src/lib.rs +++ b/tools/tag_code/src/lib.rs @@ -929,7 +929,7 @@ pub fn mdt_logic_split(s: &str) -> Result, usize> { debug_assert!(! s1.chars().next().unwrap().is_whitespace()); if s1.starts_with('"') { // string - if let Some(mut idx) = s1.trim_start_matches('"').find('"') { + if let Some(mut idx) = s1.strip_prefix('"').unwrap().find('"') { idx += '"'.len_utf8(); res.push(&s1[..=idx]); s1 = &s1[idx..]; @@ -1518,6 +1518,7 @@ mod tests { (r#" "你好" "#, &["\"你好\""]), (r#"甲乙"丙丁" "#, &["甲乙", "\"丙丁\""]), (r#"张三 李四"#, &["张三", "李四"]), + (r#" "" "#, &["\"\""]), ]; for &(src, args) in datas { assert_eq!(&mdt_logic_split(src).unwrap(), args);