Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions libs/sqf/src/analyze/lints/s52_duplicate_case.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
use std::{ops::Range, sync::Arc};

use hemtt_common::config::LintConfig;
use hemtt_workspace::{
lint::{AnyLintRunner, Lint, LintRunner},
reporting::{Code, Codes, Diagnostic, Label, Processed, Severity},
WorkspacePath,
};

use crate::{analyze::LintData, BinaryCommand, Expression, Statement, UnaryCommand};

crate::analyze::lint!(LintS52DuplicateCase);

impl Lint<LintData> for LintS52DuplicateCase {
fn ident(&self) -> &'static str {
"duplicate_case"
}

fn sort(&self) -> u32 {
520
}

fn description(&self) -> &'static str {
"Checks for duplicate cases in switch statements"
}

fn documentation(&self) -> &'static str {
r#"### Example

**Incorrect**
```sqf
switch (_value) do {
case 1: { "one" };
case 2: { "two" };
case 1: { "one again" };
};
```

**Correct**
```sqf
switch (_value) do {
case 1: { "one" };
case 2: { "two" };
case 3: { "three" };
};
```

### Explanation

Having duplicate case labels in a switch statement is likely a mistake. Only the first case will be executed, and the duplicate case will never be reached.
"#
}

fn default_config(&self) -> LintConfig {
LintConfig::help()
}

fn runners(&self) -> Vec<Box<dyn AnyLintRunner<LintData>>> {
vec![Box::new(Runner)]
}
}

struct Runner;
impl LintRunner<LintData> for Runner {
type Target = crate::Expression;

fn run(
&self,
_project: Option<&hemtt_common::config::ProjectConfig>,
config: &LintConfig,
processed: Option<&hemtt_workspace::reporting::Processed>,
_runtime: &hemtt_common::config::RuntimeArguments,
target: &Self::Target,
_data: &LintData,
) -> Codes {
let Some(processed) = processed else {
return Vec::new();
};

// Look for switch (expr) do { ... }
let Expression::BinaryCommand(BinaryCommand::Named(cmd), lhs, rhs, _) = target else {
return Vec::new();
};

if !cmd.eq_ignore_ascii_case("do") {
return Vec::new();
}

// Check if this is a switch statement
let Expression::UnaryCommand(UnaryCommand::Named(unary), _, _) = lhs.as_ref() else {
return Vec::new();
};

if !unary.eq_ignore_ascii_case("switch") {
return Vec::new();
}

// The right side should be a Code block containing case statements
let Expression::Code(body) = rhs.as_ref() else {
return Vec::new();
};

// Check if this is a switch statement by looking for case expressions
let mut case_values: Vec<(String, Range<usize>)> = Vec::new();
let mut codes: Codes = Vec::new();

for body_statement in body.content() {
let Statement::Expression(case_expr, _) = body_statement else {
continue;
};

// Try to find case VALUE: EXPR pattern
// This is structured as: case VALUE : EXPR
// Where "case" is typically a unary command or binary command
// and ":" is an Associate operator
Comment thread
BrettMayson marked this conversation as resolved.
Outdated

// Pattern 1: case VALUE : EXPR (where case is unary)
if let Expression::BinaryCommand(BinaryCommand::Associate, left, _right, _) = case_expr {
// Left side should be (case VALUE) - could be unary or binary
if let Expression::UnaryCommand(UnaryCommand::Named(name), value_expr, _) = left.as_ref() {
if name.eq_ignore_ascii_case("case") {
let case_source = value_expr.source(false);
let case_span = value_expr.span();

// Check if we've seen this value before
for (existing_value, existing_span) in &case_values {
if existing_value == &case_source {
// Found a duplicate!
codes.push(Arc::new(CodeS52DuplicateCase::new(
case_span.clone(),
case_source.clone(),
existing_span.clone(),
processed,
config.severity(),
)));
}
}

case_values.push((case_source, case_span));
continue;
}
}
}

// Pattern 2: case VALUE do EXPR (where case is binary)
Comment thread
BrettMayson marked this conversation as resolved.
Outdated
if let Expression::BinaryCommand(BinaryCommand::Named(cmd_name), inner_left, _inner_right, _) =
case_expr
{
if cmd_name.eq_ignore_ascii_case("do") {
if let Expression::BinaryCommand(BinaryCommand::Named(case_name), value_expr, _, _) =
inner_left.as_ref()
{
if case_name.eq_ignore_ascii_case("case") {
let case_source = value_expr.source(false);
let case_span = value_expr.span();

// Check if we've seen this value before
for (existing_value, existing_span) in &case_values {
if existing_value == &case_source {
// Found a duplicate!
codes.push(Arc::new(CodeS52DuplicateCase::new(
case_span.clone(),
case_source.clone(),
existing_span.clone(),
processed,
config.severity(),
)));
}
}

case_values.push((case_source, case_span));
continue;
}
}
}
}
}

codes
}
}

#[allow(clippy::module_name_repetitions)]
pub struct CodeS52DuplicateCase {
span: Range<usize>,
value: String,
first_span: Range<usize>,
severity: Severity,
diagnostic: Option<Diagnostic>,
}

impl Code for CodeS52DuplicateCase {
fn ident(&self) -> &'static str {
"L-S52"
}

fn include(&self) -> bool {
true
Comment thread
BrettMayson marked this conversation as resolved.
Outdated
}

fn link(&self) -> Option<&str> {
Some("/lints/sqf.html#duplicate_case")
}

fn severity(&self) -> Severity {
self.severity
}

fn message(&self) -> String {
format!("Duplicate case `{}` in switch statement", self.value)
}

fn label_message(&self) -> String {
"duplicate case".to_string()
}

fn diagnostic(&self) -> Option<Diagnostic> {
self.diagnostic.clone()
}
}

impl CodeS52DuplicateCase {
#[must_use]
pub fn new(
span: Range<usize>,
value: String,
first_span: Range<usize>,
processed: &Processed,
severity: Severity,
) -> Self {
Self {
span,
value,
first_span,
severity,
diagnostic: None,
}
.generate_processed(processed)
}

fn generate_processed(mut self, processed: &Processed) -> Self {
let Some(mut diag) = Diagnostic::from_code_processed(&self, self.span.clone(), processed)
else {
return self;
};

// Try to get info about the first span
if let Some((path, span)) = get_span_info(self.first_span.clone(), processed) {
diag = diag.with_label(
Label::secondary(path, span)
.with_message("first case here"),
);
}
self.diagnostic = Some(diag);
self
}
}

fn get_span_info(span: Range<usize>, processed: &Processed) -> Option<(WorkspacePath, Range<usize>)> {
let map_start = processed.mapping(span.start)?;
let map_end = processed.mapping(span.end)?;
let map_file = processed.source(map_start.source())?;
Some((
map_file.0.clone(),
map_start.original_start()..map_end.original_end(),
))
}
1 change: 1 addition & 0 deletions libs/sqf/tests/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ lint!(s48_is_equal_type_all, true);
lint!(s49_count_type, true);
lint!(s50_count_side, true);
lint!(s51_push_back_unique, true);
lint!(s52_duplicate_case, true);

#[test]
fn test_s29_function_undefined() {
Expand Down
36 changes: 36 additions & 0 deletions libs/sqf/tests/lints/s52_duplicate_case.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Test duplicate case detection

// Simple duplicate
switch (_value) do {
case 1: { "one" };
case 2: { "two" };
case 1: { "one again" };
};

// Multiple duplicates
switch (_x) do {
case "a": { 1 };
case "b": { 2 };
case "a": { 3 };
case "b": { 4 };
};

// Variable case (should be detected)
switch (_var) do {
case _distBottom: { [_x, 0] };
case _distRight: { [_worldSize, _y] };
case _distBottom: { [_x, _worldSize] };
};

// Default cases are OK (not considered cases for duplication)
switch (_value) do {
case 1: { "one" };
case 2: { "two" };
case default { "other" };
Comment thread
BrettMayson marked this conversation as resolved.
Outdated
};

// Single case is OK
switch (_value) do {
case 1: { "one" };
};

42 changes: 42 additions & 0 deletions libs/sqf/tests/snapshots/lints__simple_s52_duplicate_case.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
source: libs/sqf/tests/lints.rs
expression: lint(stringify! (s52_duplicate_case), true).0
---
help[L-S52]: Duplicate case `"a"` in switch statement
┌─ s52_duplicate_case.sqf:14:10
│
12 │ case "a": { 1 };
│ ---- first case here
13 │ case "b": { 2 };
14 │ case "a": { 3 };
│ ^^^ duplicate case


help[L-S52]: Duplicate case `"b"` in switch statement
┌─ s52_duplicate_case.sqf:15:10
│
13 │ case "b": { 2 };
│ ---- first case here
14 │ case "a": { 3 };
15 │ case "b": { 4 };
│ ^^^ duplicate case


help[L-S52]: Duplicate case `1` in switch statement
┌─ s52_duplicate_case.sqf:7:10
│
5 │ case 1: { "one" };
│ -- first case here
6 │ case 2: { "two" };
7 │ case 1: { "one again" };
│ ^ duplicate case


help[L-S52]: Duplicate case `_distBottom` in switch statement
┌─ s52_duplicate_case.sqf:22:10
│
20 │ case _distBottom: { [_x, 0] };
│ ------------ first case here
21 │ case _distRight: { [_worldSize, _y] };
22 │ case _distBottom: { [_x, _worldSize] };
│ ^^^^^^^^^^^ duplicate case
Loading