diff --git a/src/lint.c b/src/lint.c index b8d04901..14b1c4ad 100644 --- a/src/lint.c +++ b/src/lint.c @@ -684,6 +684,18 @@ static void check_dup_keys(ASTNode *node, LintContext *ctx) { for (int i = 0; i < node->data.list.count; i++) check_dup_keys(node->data.list.elems[i], ctx); break; + case AST_MATCH: + check_dup_keys(node->data.match.expr, ctx); + for (int i = 0; i < node->data.match.case_count; i++) { + check_dup_keys(node->data.match.patterns[i], ctx); + for (int j = 0; j < node->data.match.body_counts[i]; j++) + check_dup_keys(node->data.match.bodies[i][j], ctx); + } + break; + case AST_UNOBSERVED: + for (int i = 0; i < node->data.block.count; i++) + check_dup_keys(node->data.block.stmts[i], ctx); + break; /* Nothing to do for these. Enumerated rather than covered by a `default:` * so that -Werror=switch (Makefile CFLAGS) makes a new ASTType a build * error here instead of a silent no-op. */ @@ -704,9 +716,7 @@ static void check_dup_keys(ASTNode *node, LintContext *ctx) { case AST_CONTINUE: case AST_DOT_ASSIGN: case AST_IMPORT: - case AST_MATCH: case AST_LAMBDA: - case AST_UNOBSERVED: case AST_INDEX_ASSIGN: case AST_LIST_PATTERN_ASSIGN: case AST_SLICE: diff --git a/tests/test_lint.sh b/tests/test_lint.sh index 9b84ead2..dd238657 100644 --- a/tests/test_lint.sh +++ b/tests/test_lint.sh @@ -90,6 +90,63 @@ OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) check_contains "duplicate dict key" "$OUTPUT" "duplicate dict key 'a'" rm -f "$TMPFILE" +# --- #783: W010 (duplicate dict key) recurses into unobserved blocks --- +# check_dup_keys used to break on AST_UNOBSERVED, so a dict literal inside +# an unobserved: block was never reached and never warned on. +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +unobserved: + w010_d is {"a": 1, "a": 2} +print of w010_d +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_contains "#783 W010 fires inside an unobserved block" "$OUTPUT" "W010.*'a'" +rm -f "$TMPFILE" + +# --- #783: W010 (duplicate dict key) recurses into match arms --- +# check_dup_keys used to break on AST_MATCH, so a dict literal inside a +# match arm was never reached and never warned on. +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +match 1: + case 1: + w010_d is {"a": 1, "a": 2} + case _: + x is 0 +print of 1 +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_contains "#783 W010 fires inside a match arm" "$OUTPUT" "W010.*'a'" +rm -f "$TMPFILE" + +# --- #783: W010 (duplicate dict key) recurses into the match scrutinee --- +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +match {"a": 1, "a": 2}: + case _: + print of "fallback" +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_contains "#783 W010 fires in the match scrutinee" "$OUTPUT" "W010.*'a'" +rm -f "$TMPFILE" + +# --- #783: W010 (duplicate dict key) recurses into match patterns --- +# Patterns are full expressions (parser.c parses them with parse_expression), +# so a dict literal used as a case pattern must be walked too. The wildcard +# case _ stores a NULL pattern; check_dup_keys' !node guard covers it. +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +x is 1 +match x: + case {"a": 1, "a": 2}: + print of "dict" + case _: + print of "other" +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_contains "#783 W010 fires in a match pattern" "$OUTPUT" "W010.*'a'" +rm -f "$TMPFILE" + # --- Multiple warnings on one file --- TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) cat > "$TMPFILE" << 'EIGS'