use std::collections::HashMap;
use crate::lint::{Hit, Rule, Severity};
use crate::visit;
pub struct ServiceLocatorAntiPattern;
pub struct EmptyFunctionBody;
pub struct DeprecatedGlobalCall;
pub struct TypeCheckInLoop;
pub struct DeepNesting;
pub struct DotMethodCall;
pub struct PrintInHotPath;
pub struct DebugInHotPath;
pub struct IndexFunctionMetatable;
pub struct ConditionalFieldInConstructor;
pub struct GlobalFunctionNotLocal;
pub struct AssertInHotPath;
pub struct RedundantCondition;
pub struct LongFunctionBody;
pub struct DuplicateStringLiteral;
pub struct TypeOverTypeof;
pub struct NestedTernary;
pub struct UnusedVariable;
pub struct MultipleReturns;
impl Rule for ServiceLocatorAntiPattern {
fn id(&self) -> &'static str { "style::duplicate_get_service" }
fn severity(&self) -> Severity { Severity::Warn }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut service_calls: HashMap<String, Vec<usize>> = HashMap::new();
visit::each_call(ast, |call, _ctx| {
if !visit::is_method_call(call, "GetService") {
return;
}
if let Some(name) = visit::first_string_arg(call) {
service_calls
.entry(name)
.or_default()
.push(visit::call_pos(call));
}
});
let mut hits = Vec::new();
for (name, positions) in &service_calls {
if positions.len() > 1 {
for &pos in &positions[1..] {
hits.push(Hit {
pos,
msg: format!("duplicate GetService(\"{name}\") - cache in a module-level local"),
});
}
}
}
hits
}
}
impl Rule for EmptyFunctionBody {
fn id(&self) -> &'static str { "style::empty_function_body" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
for pos in visit::find_pattern_positions(source, "function()") {
let after = &source[pos + "function()".len()..];
let trimmed = after.trim_start();
if trimmed.starts_with("end") {
hits.push(Hit {
pos,
msg: "empty function body - use a NOOP constant or remove if unnecessary".into(),
});
}
}
hits
}
}
impl Rule for DeprecatedGlobalCall {
fn id(&self) -> &'static str { "style::deprecated_global" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
visit::each_call(ast, |call, _ctx| {
if visit::is_bare_call(call, "rawget") || visit::is_bare_call(call, "rawset") || visit::is_bare_call(call, "rawequal") {
hits.push(Hit {
pos: visit::call_pos(call),
msg: "rawget/rawset/rawequal - may indicate metatable workaround, verify necessity".into(),
});
}
});
hits
}
}
impl Rule for TypeCheckInLoop {
fn id(&self) -> &'static str { "style::type_check_in_loop" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
visit::each_call(ast, |call, ctx| {
if ctx.in_loop && visit::is_bare_call(call, "typeof") {
hits.push(Hit {
pos: visit::call_pos(call),
msg: "typeof() in loop - if checking same value, cache the type string outside loop".into(),
});
}
});
hits
}
}
impl Rule for DeepNesting {
fn id(&self) -> &'static str { "style::deep_nesting" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
let mut max_depth: u32 = 0;
let mut depth: u32 = 0;
let mut deepest_pos: usize = 0;
let mut byte_pos: usize = 0;
for line in source.lines() {
let trimmed = line.trim();
let openers = ["function", "if ", "for ", "while ", "repeat", "do"];
for opener in &openers {
if trimmed.starts_with(opener) {
depth += 1;
break;
}
}
if depth > max_depth {
max_depth = depth;
deepest_pos = byte_pos;
}
if trimmed == "end" || trimmed.starts_with("end)") || trimmed.starts_with("until") {
depth = depth.saturating_sub(1);
}
byte_pos += line.len() + 1;
}
if max_depth > 8 {
hits.push(Hit {
pos: deepest_pos,
msg: format!("nesting depth of {max_depth} - consider extracting helper functions (max recommended: 5-6)"),
});
}
hits
}
}
impl Rule for DotMethodCall {
fn id(&self) -> &'static str { "style::dot_method_call" }
fn severity(&self) -> Severity { Severity::Warn }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
visit::each_call(ast, |call, _ctx| {
let prefix = match visit::prefix_token(call) {
Some(t) => t,
None => return,
};
let prefix_name = visit::tok_text(prefix);
let suffixes: Vec<_> = call.suffixes().collect();
if suffixes.len() < 2 {
return;
}
let field_name = match &suffixes[0] {
full_moon::ast::Suffix::Index(full_moon::ast::Index::Dot { name, .. }) => {
visit::tok_text(name)
}
_ => return,
};
if !field_name.starts_with(|c: char| c.is_uppercase()) {
return;
}
if let full_moon::ast::Suffix::Call(full_moon::ast::Call::AnonymousCall(
full_moon::ast::FunctionArgs::Parentheses { arguments, .. },
)) = &suffixes[1]
{
if let Some(first_arg) = arguments.iter().next() {
let arg_text = format!("{first_arg}").trim().to_string();
if arg_text == prefix_name {
hits.push(Hit {
pos: visit::call_pos(call),
msg: format!("{prefix_name}.{field_name}({prefix_name}, ...) - use {prefix_name}:{field_name}(...) for NAMECALL optimization"),
});
}
}
}
});
hits
}
}
impl Rule for PrintInHotPath {
fn id(&self) -> &'static str { "style::print_in_hot_path" }
fn severity(&self) -> Severity { Severity::Warn }
fn check(&self, source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
let has_runservice = source.contains("Heartbeat") || source.contains("RenderStepped") || source.contains("Stepped");
visit::each_call(ast, |call, ctx| {
let is_print = visit::is_bare_call(call, "print") || visit::is_bare_call(call, "warn");
if !is_print {
return;
}
if ctx.in_loop {
hits.push(Hit {
pos: visit::call_pos(call),
msg: "print/warn in loop - I/O is expensive, remove or guard with a flag for production".into(),
});
} else if has_runservice {
let pos = visit::call_pos(call);
let before_start = visit::floor_char(source, pos.saturating_sub(500));
let before = &source[before_start..pos];
if before.contains("Heartbeat") || before.contains("RenderStepped") || before.contains("Stepped") {
hits.push(Hit {
pos,
msg: "print/warn in RunService callback - fires every frame, remove for production".into(),
});
}
}
});
hits
}
}
impl Rule for DebugInHotPath {
fn id(&self) -> &'static str { "style::debug_in_hot_path" }
fn severity(&self) -> Severity { Severity::Warn }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
visit::each_call(ast, |call, ctx| {
if !ctx.in_loop {
return;
}
let is_debug = visit::is_dot_call(call, "debug", "traceback")
|| visit::is_dot_call(call, "debug", "info");
if is_debug {
hits.push(Hit {
pos: visit::call_pos(call),
msg: "debug.traceback/info in loop - expensive stack introspection, move outside loop or guard".into(),
});
}
});
hits
}
}
impl Rule for IndexFunctionMetatable {
fn id(&self) -> &'static str { "style::index_function_metatable" }
fn severity(&self) -> Severity { Severity::Warn }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
let patterns = ["__index = function", "__index=function"];
for pattern in &patterns {
for pos in visit::find_pattern_positions(source, pattern) {
hits.push(Hit {
pos,
msg: "__index = function(...) prevents inline caching - use __index = methodTable for faster lookups".into(),
});
}
}
hits
}
}
impl Rule for ConditionalFieldInConstructor {
fn id(&self) -> &'static str { "style::conditional_field_in_constructor" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
for pos in visit::find_pattern_positions(source, "= {}") {
let before_start = visit::floor_char(source, pos.saturating_sub(80));
let before = &source[before_start..pos];
let line_start = before.rfind('\n').map(|i| i + 1).unwrap_or(0);
let line_prefix = before[line_start..].trim();
let var_name = if let Some(rest) = line_prefix.strip_prefix("local ") {
rest.trim()
} else {
line_prefix
};
if var_name.is_empty() || var_name.contains(' ') || var_name.contains('.') {
continue;
}
let after_start = pos + "= {}".len();
let after_end = visit::ceil_char(source, (after_start + 800).min(source.len()));
let after = &source[after_start..after_end];
let mut in_if = false;
let mut in_else = false;
let mut if_fields: Vec<String> = Vec::new();
let mut else_fields: Vec<String> = Vec::new();
let field_prefix = format!("{var_name}.");
for line in after.lines().take(30) {
let trimmed = line.trim();
if trimmed.starts_with("if ") || trimmed.starts_with("elseif ") {
in_if = true;
in_else = false;
} else if trimmed == "else" {
in_if = false;
in_else = true;
} else if trimmed == "end" {
if in_if || in_else {
break;
}
}
if trimmed.starts_with(&field_prefix) && trimmed.contains(" = ") {
let field = trimmed[field_prefix.len()..].split(' ').next().unwrap_or("").to_string();
if in_if {
if_fields.push(field);
} else if in_else {
else_fields.push(field);
}
}
}
if !if_fields.is_empty() && !else_fields.is_empty() {
let has_unique_if = if_fields.iter().any(|f| !else_fields.contains(f));
let has_unique_else = else_fields.iter().any(|f| !if_fields.contains(f));
if has_unique_if || has_unique_else {
hits.push(Hit {
pos,
msg: "conditional field assignment creates polymorphic table shapes - defeats inline caching, use uniform fields".into(),
});
}
}
}
hits
}
}
impl Rule for GlobalFunctionNotLocal {
fn id(&self) -> &'static str { "style::global_function_not_local" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
for stmt in ast.nodes().stmts() {
if let full_moon::ast::Stmt::FunctionDeclaration(func) = stmt {
let name = format!("{}", func.name());
if !name.contains('.') && !name.contains(':') {
let pos = func.function_token().start_position().bytes();
hits.push(Hit {
pos,
msg: format!("global function '{name}' - use 'local function' for GETIMPORT optimization and --!optimize 2 inlining"),
});
}
}
}
hits
}
}
impl Rule for AssertInHotPath {
fn id(&self) -> &'static str { "style::assert_in_hot_path" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
visit::each_call(ast, |call, ctx| {
if ctx.in_loop && visit::is_bare_call(call, "assert") {
hits.push(Hit {
pos: visit::call_pos(call),
msg: "assert() in loop - has overhead even when condition is true, guard with a debug flag or move outside".into(),
});
}
});
hits
}
}
impl Rule for RedundantCondition {
fn id(&self) -> &'static str { "style::redundant_condition" }
fn severity(&self) -> Severity { Severity::Warn }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
for pos in visit::find_pattern_positions(source, "if true then") {
hits.push(Hit {
pos,
msg: "if true then - condition is always true, remove the if statement".into(),
});
}
for pos in visit::find_pattern_positions(source, "if false then") {
hits.push(Hit {
pos,
msg: "if false then - condition is always false, dead code".into(),
});
}
for pos in visit::find_pattern_positions(source, "while false do") {
hits.push(Hit {
pos,
msg: "while false do - loop body is dead code".into(),
});
}
hits
}
}
impl Rule for LongFunctionBody {
fn id(&self) -> &'static str { "style::long_function_body" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
check_functions(ast.nodes(), &mut hits);
hits
}
}
fn check_functions(block: &full_moon::ast::Block, hits: &mut Vec<Hit>) {
for stmt in block.stmts() {
match stmt {
full_moon::ast::Stmt::LocalFunction(f) => {
let count = count_stmts(f.body().block());
if count > 80 {
let pos = f.local_token().start_position().bytes();
let name = format!("{}", f.name());
hits.push(Hit {
pos,
msg: format!("function '{name}' has {count} statements - consider splitting into smaller functions"),
});
}
check_functions(f.body().block(), hits);
}
full_moon::ast::Stmt::FunctionDeclaration(f) => {
let count = count_stmts(f.body().block());
if count > 80 {
let pos = f.function_token().start_position().bytes();
let name = format!("{}", f.name());
hits.push(Hit {
pos,
msg: format!("function '{name}' has {count} statements - consider splitting into smaller functions"),
});
}
check_functions(f.body().block(), hits);
}
full_moon::ast::Stmt::Do(s) => check_functions(s.block(), hits),
full_moon::ast::Stmt::If(s) => {
check_functions(s.block(), hits);
if let Some(eis) = s.else_if() {
for ei in eis { check_functions(ei.block(), hits); }
}
if let Some(eb) = s.else_block() { check_functions(eb, hits); }
}
full_moon::ast::Stmt::While(s) => check_functions(s.block(), hits),
full_moon::ast::Stmt::Repeat(s) => check_functions(s.block(), hits),
full_moon::ast::Stmt::NumericFor(s) => check_functions(s.block(), hits),
full_moon::ast::Stmt::GenericFor(s) => check_functions(s.block(), hits),
_ => {}
}
}
}
fn count_stmts(block: &full_moon::ast::Block) -> usize {
let mut count = 0;
for stmt in block.stmts() {
count += 1;
match stmt {
full_moon::ast::Stmt::If(s) => {
count += count_stmts(s.block());
if let Some(eis) = s.else_if() {
for ei in eis { count += count_stmts(ei.block()); }
}
if let Some(eb) = s.else_block() { count += count_stmts(eb); }
}
full_moon::ast::Stmt::While(s) => count += count_stmts(s.block()),
full_moon::ast::Stmt::Repeat(s) => count += count_stmts(s.block()),
full_moon::ast::Stmt::NumericFor(s) => count += count_stmts(s.block()),
full_moon::ast::Stmt::GenericFor(s) => count += count_stmts(s.block()),
full_moon::ast::Stmt::Do(s) => count += count_stmts(s.block()),
_ => {}
}
}
count
}
impl Rule for DuplicateStringLiteral {
fn id(&self) -> &'static str { "style::duplicate_string_literal" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut counts: HashMap<String, (usize, usize)> = HashMap::new();
let mut i = 0;
let bytes = source.as_bytes();
while i < bytes.len() {
if bytes[i] == b'"' || bytes[i] == b'\'' {
let quote = bytes[i];
let start = i;
i += 1;
while i < bytes.len() && bytes[i] != quote {
if bytes[i] == b'\\' { i += 1; }
i += 1;
}
if i < bytes.len() {
let s = &source[start + 1..i];
if s.len() >= 4 {
let entry = counts.entry(s.to_string()).or_insert((0, start));
entry.0 += 1;
}
}
}
i += 1;
}
counts.into_iter()
.filter(|(_, (count, _))| *count >= 5)
.map(|(s, (count, pos))| Hit {
pos,
msg: format!("string \"{s}\" appears {count} times - extract to a local constant"),
})
.collect()
}
}
impl Rule for TypeOverTypeof {
fn id(&self) -> &'static str { "style::type_over_typeof" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
visit::each_call(ast, |call, _ctx| {
if visit::is_bare_call(call, "type") {
hits.push(Hit {
pos: visit::call_pos(call),
msg: "type() doesn't handle Roblox types - typeof() returns correct types for Vector3, CFrame, Instance, etc.".into(),
});
}
});
hits
}
}
impl Rule for NestedTernary {
fn id(&self) -> &'static str { "style::nested_ternary" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
for (line_num, line) in source.lines().enumerate() {
let if_count = line.matches(" if ").count() + line.matches("(if ").count();
if if_count >= 3 {
let pos = source.lines().take(line_num).map(|l| l.len() + 1).sum::<usize>();
hits.push(Hit {
pos,
msg: "deeply nested ternary (if/then/else) expression - extract to a helper function for readability".into(),
});
}
}
hits
}
}
impl Rule for UnusedVariable {
fn id(&self) -> &'static str { "style::unused_variable_in_loop" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
let lines: Vec<&str> = source.lines().collect();
let loop_depth = build_loop_depth_map(source);
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if i >= loop_depth.len() || loop_depth[i] == 0 { continue; }
if !trimmed.starts_with("local ") { continue; }
let after_local = trimmed.strip_prefix("local ").unwrap();
if after_local.starts_with("function") { continue; }
if let Some(eq_pos) = after_local.find(" = ") {
let var_name = after_local[..eq_pos].trim();
if var_name.contains(',') || var_name.starts_with('_') { continue; }
let rhs = &after_local[eq_pos + 3..];
if rhs.contains("Instance.new") || rhs.contains(".new(") || rhs.contains(":Clone()") {
let mut used = false;
for j in (i + 1)..lines.len().min(i + 20) {
let next = lines[j].trim();
if next == "end" || next.starts_with("end ") || next.starts_with("until") { break; }
if next.contains(var_name) && !next.starts_with("local ") {
used = true;
break;
}
}
if !used {
let byte_pos: usize = lines[..i].iter().map(|l| l.len() + 1).sum();
hits.push(Hit {
pos: byte_pos,
msg: format!("'{var_name}' allocated in loop but never used afterward - remove or use the variable"),
});
}
}
}
}
hits
}
}
impl Rule for MultipleReturns {
fn id(&self) -> &'static str { "style::multiple_returns_hot_path" }
fn severity(&self) -> Severity { Severity::Allow }
fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
let mut hits = Vec::new();
let mut in_heartbeat = false;
for (i, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.contains("Heartbeat") || trimmed.contains("RenderStepped") || trimmed.contains("Stepped") {
if trimmed.contains(":Connect") || trimmed.contains(":Once") {
in_heartbeat = true;
}
}
if in_heartbeat && trimmed == "end)" {
in_heartbeat = false;
}
if in_heartbeat && trimmed.starts_with("return ") && trimmed.contains(", ") {
let return_count = trimmed.split(", ").count();
if return_count >= 4 {
let byte_pos: usize = source.lines().take(i).map(|l| l.len() + 1).sum();
hits.push(Hit {
pos: byte_pos,
msg: format!("returning {return_count} values from hot path - multiple returns require stack management overhead each frame"),
});
}
}
}
hits
}
}
fn build_loop_depth_map(source: &str) -> Vec<u32> {
let mut depth: u32 = 0;
let mut depths = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("for ") || trimmed.starts_with("while ") || trimmed.starts_with("repeat") {
depth += 1;
}
depths.push(depth);
if trimmed == "end" || trimmed.starts_with("end ") || trimmed.starts_with("until ") || trimmed == "until" {
depth = depth.saturating_sub(1);
}
}
depths
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lint::Rule;
fn parse(src: &str) -> full_moon::ast::Ast {
full_moon::parse(src).unwrap()
}
#[test]
fn dot_method_call_detected() {
let src = "obj.DoSomething(obj, 1, 2)";
let ast = parse(src);
let hits = DotMethodCall.check(src, &ast);
assert_eq!(hits.len(), 1);
assert!(hits[0].msg.contains("NAMECALL"));
}
#[test]
fn colon_method_not_flagged() {
let src = "obj:DoSomething(1, 2)";
let ast = parse(src);
let hits = DotMethodCall.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn dot_lowercase_not_flagged() {
let src = "obj.dosomething(obj, 1)";
let ast = parse(src);
let hits = DotMethodCall.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn print_in_loop_detected() {
let src = "for i = 1, 10 do\n print(i)\nend";
let ast = parse(src);
let hits = PrintInHotPath.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn warn_in_loop_detected() {
let src = "while true do\n warn(\"test\")\nend";
let ast = parse(src);
let hits = PrintInHotPath.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn debug_traceback_in_loop_detected() {
let src = "for i = 1, 10 do\n debug.traceback()\nend";
let ast = parse(src);
let hits = DebugInHotPath.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn debug_info_in_loop_detected() {
let src = "for i = 1, 10 do\n debug.info(1, \"s\")\nend";
let ast = parse(src);
let hits = DebugInHotPath.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn debug_outside_loop_not_flagged() {
let src = "local tb = debug.traceback()";
let ast = parse(src);
let hits = DebugInHotPath.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn index_function_detected() {
let src = "setmetatable(t, {__index = function(self, key) return nil end})";
let ast = parse(src);
let hits = IndexFunctionMetatable.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn index_table_not_flagged() {
let src = "setmetatable(t, {__index = methods})";
let ast = parse(src);
let hits = IndexFunctionMetatable.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn conditional_field_detected() {
let src = "local t = {}\nif cond then\n t.health = 100\nelse\n t.damage = 50\nend";
let ast = parse(src);
let hits = ConditionalFieldInConstructor.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn uniform_fields_not_flagged() {
let src = "local t = {}\nif cond then\n t.health = 100\nelse\n t.health = 50\nend";
let ast = parse(src);
let hits = ConditionalFieldInConstructor.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn global_function_detected() {
let src = "function foo()\n return 1\nend";
let ast = parse(src);
let hits = GlobalFunctionNotLocal.check(src, &ast);
assert_eq!(hits.len(), 1);
assert!(hits[0].msg.contains("local function"));
}
#[test]
fn local_function_not_flagged() {
let src = "local function foo()\n return 1\nend";
let ast = parse(src);
let hits = GlobalFunctionNotLocal.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn method_definition_not_flagged() {
let src = "function obj:Method()\n return 1\nend";
let ast = parse(src);
let hits = GlobalFunctionNotLocal.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn assert_in_loop_detected() {
let src = "for i = 1, 10 do\n assert(i > 0)\nend";
let ast = parse(src);
let hits = AssertInHotPath.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn assert_outside_loop_ok() {
let src = "assert(x > 0)";
let ast = parse(src);
let hits = AssertInHotPath.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn redundant_if_true_detected() {
let src = "if true then\n print(1)\nend";
let ast = parse(src);
let hits = RedundantCondition.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn redundant_if_false_detected() {
let src = "if false then\n print(1)\nend";
let ast = parse(src);
let hits = RedundantCondition.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn normal_condition_ok() {
let src = "if x > 0 then\n print(1)\nend";
let ast = parse(src);
let hits = RedundantCondition.check(src, &ast);
assert_eq!(hits.len(), 0);
}
#[test]
fn duplicate_string_literal_detected() {
let src = "local a = \"hello\"\nlocal b = \"hello\"\nlocal c = \"hello\"\nlocal d = \"hello\"\nlocal e = \"hello\"";
let ast = parse(src);
let hits = DuplicateStringLiteral.check(src, &ast);
assert_eq!(hits.len(), 1);
}
#[test]
fn few_strings_ok() {
let src = "local a = \"hello\"\nlocal b = \"hello\"";
let ast = parse(src);
let hits = DuplicateStringLiteral.check(src, &ast);
assert_eq!(hits.len(), 0);
}
}