use super::program_ref;
use super::{Context, LintRule};
use crate::swc_util::span_and_ctx_drop;
use crate::tags::{self, Tags};
use crate::Program;
use crate::ProgramRef;
use deno_ast::swc::ast::{Expr, SwitchStmt};
use deno_ast::swc::ecma_visit::noop_visit_type;
use deno_ast::swc::ecma_visit::{Visit, VisitWith};
use deno_ast::SourceRangedForSpanned;
use derive_more::Display;
use std::collections::HashSet;
#[derive(Debug)]
pub struct NoDuplicateCase;
const CODE: &str = "no-duplicate-case";
#[derive(Display)]
enum NoDuplicateCaseMessage {
#[display(fmt = "Duplicate values in `case` are not allowed")]
Unexpected,
}
#[derive(Display)]
enum NoDuplicateCaseHint {
#[display(fmt = "Remove or rename the duplicate case clause")]
RemoveOrRename,
}
impl LintRule for NoDuplicateCase {
fn tags(&self) -> Tags {
&[tags::RECOMMENDED]
}
fn code(&self) -> &'static str {
CODE
}
fn lint_program_with_ast_view<'view>(
&self,
context: &mut Context<'view>,
program: Program<'view>,
) {
let program = program_ref(program);
let mut visitor = NoDuplicateCaseVisitor::new(context);
match program {
ProgramRef::Module(m) => m.visit_with(&mut visitor),
ProgramRef::Script(s) => s.visit_with(&mut visitor),
}
}
}
struct NoDuplicateCaseVisitor<'c, 'view> {
context: &'c mut Context<'view>,
}
impl<'c, 'view> NoDuplicateCaseVisitor<'c, 'view> {
fn new(context: &'c mut Context<'view>) -> Self {
Self { context }
}
}
impl Visit for NoDuplicateCaseVisitor<'_, '_> {
noop_visit_type!();
fn visit_switch_stmt(&mut self, switch_stmt: &SwitchStmt) {
let mut seen: HashSet<Box<Expr>> = HashSet::new();
for case in &switch_stmt.cases {
if let Some(test) = &case.test {
let span_dropped_test = span_and_ctx_drop(test.clone());
if !seen.insert(span_dropped_test) {
self.context.add_diagnostic_with_hint(
case.range(),
CODE,
NoDuplicateCaseMessage::Unexpected,
NoDuplicateCaseHint::RemoveOrRename,
);
}
}
}
switch_stmt.visit_children_with(self);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_duplicate_case_valid() {
assert_lint_ok! {
NoDuplicateCase,
"var a = 1; switch (a) {case 1: break; case 2: break; default: break;}",
"var a = 1; switch (a) {case 1: break; case '1': break; default: break;}",
"var a = 1; switch (a) {case 1: break; case true: break; default: break;}",
"var a = 1; switch (a) {default: break;}",
"var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p.p.p2: break; default: break;}",
"var a = 1, f = function(b) { return b ? { p1: 1 } : { p1: 2 }; }; switch (a) {case f(true).p1: break; case f(true, false).p1: break; default: break;}",
"var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a + 2).p1: break; default: break;}",
"var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a == 1 ? 2 : 3).p1: break; case f(a === 1 ? 2 : 3).p1: break; default: break;}",
"var a = 1, f1 = function() { return { p1: 1 } }, f2 = function() { return { p1: 2 } }; switch (a) {case f1().p1: break; case f2().p1: break; default: break;}",
"var a = [1,2]; switch(a.toString()){case ([1,2]).toString():break; case ([1]).toString():break; default:break;}",
"switch(a) { case a: break; } switch(a) { case a: break; }",
"switch(a) { case toString: break; }",
r#"
switch (a) {
case 1:
switch (b) {
case 2:
foo();
break;
default:
bar();
break;
}
default:
break;
}
"#,
};
}
#[test]
fn no_duplicate_case_invalid() {
assert_lint_err! {
NoDuplicateCase,
r#"
const someText = "some text";
switch (someText) {
case "a":
break;
case "b":
break;
case "a":
break;
default:
break;
}
"#: [
{
col: 4,
line: 8,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1; switch (a) {case 1: break; case 1: break; case 2: break; default: break;}": [
{
col: 38,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = '1'; switch (a) {case '1': break; case '1': break; case '2': break; default: break;}": [
{
col: 42,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, one = 1; switch (a) {case one: break; case one: break; case 2: break; default: break;}": [
{
col: 49,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p.p.p1: break; default: break;}": [
{
col: 68,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, f = function(b) { return b ? { p1: 1 } : { p1: 2 }; }; switch (a) {case f(true).p1: break; case f(true).p1: break; default: break;}": [
{
col: 102,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a + 1).p1: break; default: break;}": [
{
col: 86,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a === 1 ? 2 : 3).p1: break; case f(a === 1 ? 2 : 3).p1: break; default: break;}": [
{
col: 96,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, f1 = function() { return { p1: 1 } }; switch (a) {case f1().p1: break; case f1().p1: break; default: break;}": [
{
col: 82,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = [1, 2]; switch(a.toString()){case ([1, 2]).toString():break; case ([1, 2]).toString():break; default:break;}": [
{
col: 69,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"switch (a) { case a: case a: }": [
{
col: 21,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"switch (a) { case a: break; case b: break; case a: break; case c: break; case a: break; }": [
{
col: 43,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
},
{
col: 73,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a+1).p1: break; default: break;}": [
{
col: 86,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
r#"
switch (a) {
case 1:
switch (b) {
case 1:
break;
case 1:
break;
}
default:
break;
}
"#: [
{
line: 7,
col: 6,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment\n .p1: break; default: break;}": [
{
col: 68,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p\n/* comment */\n.p1: break; case p.p.p1: break; default: break;}": [
{
line: 3,
col: 12,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p\n/* comment */\n.p1: break; case p. p // comment\n .p1: break; default: break;}": [
{
line: 3,
col: 12,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment\n .p1: break; case p .p\n/* comment */\n.p1: break; default: break;}": [
{
line: 1,
col: 68,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
},
{
line: 2,
col: 13,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
"var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(\na + 1 // comment\n).p1: break; case f(a+1)\n.p1: break; default: break;}": [
{
line: 3,
col: 13,
message: NoDuplicateCaseMessage::Unexpected,
hint: NoDuplicateCaseHint::RemoveOrRename,
}
],
};
}
}