Cover JavaScript equivalence fixtures
ober
9490f84ad74808feec02c3d8fd8795f288974994
--- a/HANDOFF_OPUS_4_8.md +++ b/HANDOFF_OPUS_4_8.md @@ -1,6 +1,6 @@ # Opus 4.8 Handoff: jerboa-semgrep Semgrep Parity -Date: 2026-05-29 20:13 MDT +Date: 2026-05-29 20:28 MDT Workspace: `/Users/user/mine/jerboa-semgrep` Sibling upstream Semgrep checkout: `/Users/user/mine/semgrep` Packaged Semgrep oracle: `/Users/user/.local/bin/semgrep` @@ -10,13 +10,13 @@ Base HEAD before this checkpoint: The user wants this project carried forward until the pure Jerboa port reaches Semgrep parity. Do not treat this handoff as completion. This handoff records the clean Python `tests/patterns/python/*.sgrep` frontier and the first promoted -JavaScript `tests/patterns/js/*.sgrep` constant-propagation slice. +JavaScript `tests/patterns/js/*.sgrep` constant-propagation/equivalence slice. Current headline: - Full Python pattern sweep: 164 passed / 0 mismatched. -- Full JavaScript pattern sweep: 54 passed / 78 mismatched, improved from - 41 passed / 91 mismatched by closing the first JS CP slice. +- Full JavaScript pattern sweep: 56 passed / 76 mismatched, improved from + 41 passed / 91 mismatched by closing the first JS CP/equivalence slices. - Broad same-basename upstream sweep: 437 passed / 0 mismatched / 0 Jerboa errors, with 3 current-Semgrep oracle errors. @@ -73,7 +73,7 @@ make test Result: ```text -286 tests, 286 passed, 0 failed +287 tests, 287 passed, 0 failed ``` Focused constant/dataflow slice: @@ -132,7 +132,7 @@ git diff --check Result: no output and exit code 0. -Promoted JavaScript constant-propagation oracle: +Promoted JavaScript constant-propagation/equivalence oracle: ```sh SEMGREP_CURRENT=/Users/user/.local/bin/semgrep make patterns-js-oracle @@ -141,7 +141,7 @@ SEMGREP_CURRENT=/Users/user/.local/bin/semgrep make patterns-js-oracle Result: ```text -patterns-sweep: 13 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 13 compared +patterns-sweep: 15 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 15 compared ``` Full upstream JavaScript pattern sweep: @@ -153,7 +153,7 @@ SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js LIST_MISMATCHES= Result: ```text -patterns-sweep: 54 passed, 78 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared +patterns-sweep: 56 passed, 76 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared ``` Broad same-basename upstream sweep: @@ -205,6 +205,8 @@ infer_const_1 infer_const_regexp equivalence_constant_propagation2 equivalence_constant_propagation3 +equivalence_keyword_args +equivalence_varlet ``` ## Implementation Map @@ -275,6 +277,15 @@ JavaScript constant-propagation support added in the latest checkpoint: - JS CP call/literal scanning ignores line comments so examples in comments, such as `new RegExp(/a/)`, do not become false positives. +JavaScript equivalence support added in the latest checkpoint: + +- `scan-javascript-varlet-equivalence-pattern` matches `var $X = ...` patterns + against `var`, `let`, and `const` declarations with Semgrep-compatible + declaration ranges. +- `scan-javascript-object-call-equivalence-pattern` matches object argument + fields independent of order and allows extra fields, covering + `foo({kwd2:$X, kwd1:$Y})` against `foo({kwd1:1, kwd2:2, kwd3:3})`. + ## Smoke Coverage Added `tests/smoke.ss` extends `scan Python constant propagation pattern fixture @@ -292,7 +303,11 @@ The latest checkpoint adds a new top-level smoke case, template/concat string calls, conditional string calls, subscript propagation, throw-unreachable return propagation, and increment invalidation. -The smoke suite count is now 286. +The latest checkpoint adds a new top-level smoke case, +`scan JavaScript equivalence pattern fixture fallbacks`, covering var/let/const +declaration equivalence and object-call field-order equivalence. + +The smoke suite count is now 287. ## Current Frontier @@ -303,10 +318,10 @@ patterns-sweep: 164 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 s ``` The full JavaScript `.sgrep` pattern fixture sweep is not clean yet, but the -first JS constant-propagation slice is promoted and green: +first JS constant-propagation/equivalence slice is promoted and green: ```text -patterns-sweep: 54 passed, 78 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared +patterns-sweep: 56 passed, 76 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared ``` Do not mark the overall goal complete from this alone. Next high-value work: --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ SCHEME ?= $(HOST_SCHEME) JERBUILD ?= $(if $(wildcard $(JERBOA_HOME)/jerbuild),$(JERBOA_HOME)/jerbuild,$(SCHEME) --libdirs "$(JERBOA_HOME)/lib" --script "$(JERBOA_HOME)/jerbuild.ss") LIBDIRS = lib:$(JERBOA_TREESITTER_HOME)/lib:$(JERBOA_HOME)/lib PATTERN_CASE_REGEX ?= ^(ac_matching_dots|ac_matching_dots1|ac_matching_explosion|ac_matching_explosion1|ac_matching_explosion2|ac_matching_explosion3|ac_matching_free|ac_matching_free1|ac_matching_if|ac_matching_if1|ac_matching_mvars|ac_matching_mvars1|ac_matching_mvars2|aliasing_attribute|assoc_matching_bug|assoc_matching_dots|assoc_matching_dots1|assoc_matching_explosion|assoc_matching_explosion1|assoc_matching_explosion2|assoc_matching_explosion3|assoc_matching_free|assoc_matching_free1|assoc_matching_mvars|assoc_matching_mvars1|constprop_dataflow|constprop_used_twice|cp_concat|cp_exception|cp_label|cp_label1|cp_method_call|cp_python_mult_string|cp_python_mult_string1|cp_rlval|cp_string_mvar|cp_strings|cp_with|cp_yield|deep_cond|deep_expr_operator|deep_expr_vs_statement|deep_exprstmt|deep_fake_dataflow|deep_try|dict_ellipsis|dots_attribute|dots_expr_plus|dots_expr_plus_string|dots_fstring|dots_fstring_with_match_stmt|dots_inherit|dots_list|dots_nested_stmts|dots_stmts|equivalence_constant_propagation|equivalence_f_string|equivalence_f_string_2|equivalence_f_string_3|equivalence_f_string_4|equivalence_interpolated_str|equivalence_interpolated_str2|equivalence_keyword_args|equivalence_naming_import|import_metavar|import_negatives|import_negatives2|imports|index_tuple|less_attributes|less_catch|less_catch_multiple|less_inherits|less_typehint|less_typehint2|matching_if_expr|metavar_anno|metavar_anno_fqn|metavar_class_def|metavar_dict|metavar_equality_param_vs_use|metavar_equality_var|metavar_func_def|metavar_stmt|metavar_typed|misc_attributes|misc_block_import|misc_class_regression|misc_dots_stmts|misc_except_matching|misc_faketok2|misc_if1|misc_metavar_vs_fstring|misc_naming_bug2|misc_paren|misc_range_statements|misc_regression1|misc_regression2|misc_stmts1|misc_tuple2|misc_with_parens|multi_import|multi_qualified_wildcard|pip614-extended-decorator-grammer1|regexp_string_backref|set_vs_dict|set_vs_dict2|standalone_decorator|standalone_decorator_single_arg|static_method|stmts_to_fields|tuple_subscript_ellipsis|tuple_subscript_ellipsis_with_match_stmt|unordered_metavar2|untyped_vs_typed|wildcard_qualified)$$ -JS_PATTERN_CASE_REGEX ?= ^(cp_conditional|cp_dataflow|cp_implicit_conversion|cp_incrdecr|cp_label|cp_throw|cp_undeclared|cp_undeclared1|infer_const|infer_const_1|infer_const_regexp|equivalence_constant_propagation2|equivalence_constant_propagation3)$$ +JS_PATTERN_CASE_REGEX ?= ^(cp_conditional|cp_dataflow|cp_implicit_conversion|cp_incrdecr|cp_label|cp_throw|cp_undeclared|cp_undeclared1|equivalence_keyword_args|equivalence_varlet|infer_const|infer_const_1|infer_const_regexp|equivalence_constant_propagation2|equivalence_constant_propagation3)$$ .PHONY: all build generate test oracle patterns-oracle patterns-js-oracle clean --- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -4420,6 +4420,265 @@ (if finding (cons finding acc) acc))))))))))) + (def (javascript-varlet-equivalence-pattern-spec pattern) + (let* ([trimmed (string-trim pattern)] + [match (re-search + (re "^var[ \\t]+\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*=[ \\t]*(.+?);?$") + trimmed + 0)]) + (and match + (let ([rhs (string-trim (re-match-group match 2))]) + (and (not (string=? rhs "")) + (list + (cons 'metavariable (re-match-group match 1)) + (cons 'rhs rhs))))))) + (def (scan-javascript-varlet-equivalence-pattern + rule + path + source + pattern) + (let ([spec (javascript-varlet-equivalence-pattern-spec + pattern)]) + (and spec + (let* ([rhs (alist-ref/default spec 'rhs "")] + [mvar (alist-ref/default spec 'metavariable "")] + [rx (re (string-append "(^|\\n)[ \\t]*(?:var|let|const)[ \\t]+" + "([A-Za-z_$][A-Za-z0-9_$]*)" + "[ \\t]*=[ \\t]*" (regex-escape-string rhs) + "[ \\t]*;?"))]) + (let loop ([start 0] [acc '()]) + (let ([match (re-search rx source start)]) + (if (not match) + (nonempty-findings (reverse acc)) + (let* ([line-start (if (and (< (re-match-start + match) + (string-length + source)) + (char=? + (string-ref + source + (re-match-start + match)) + #\newline)) + (+ (re-match-start match) 1) + (re-match-start match))] + [line-end (line-end-after + source + line-start)] + [first (line-first-nonspace + source + line-start + line-end)] + [raw-end (re-match-end match)] + [end (if (and (> raw-end first) + (char=? + (string-ref + source + (- raw-end 1)) + #\;)) + (- raw-end 1) + raw-end)] + [name (re-match-group match 2)] + [name-start (string-find-substring-from + source + name + first)] + [binding (and name-start + (make-regex-capture-binding mvar name source name-start + (+ name-start + (string-length name))))] + [finding (and binding + (finding-for-range-with-bindings rule path source first end + (list + (cons mvar binding))))] + [next (max (+ (re-match-start match) 1) + (re-match-end match))]) + (loop + next + (if finding (cons finding acc) acc)))))))))) + (def (javascript-object-call-equivalence-pattern-spec + pattern) + (let* ([trimmed (string-trim pattern)] + [open (char-index-from + trimmed + #\( + 0 + (string-length trimmed))] + [close (and open (find-matching-close-paren trimmed open))]) + (and open + close + (= close (string-length trimmed)) + (let* ([function (string-trim (substring trimmed 0 open))] + [arg (string-trim + (substring trimmed (+ open 1) (- close 1)))] + [object? (and arg + (outer-pair? + (string-trim arg) + #\{ + #\}))]) + (and object? + (let* ([object (string-trim arg)] + [fields (object-field-items + object + 1 + (- (string-length object) 1))] + [requirements (let loop ([xs fields] [acc '()]) + (cond + [(null? xs) (reverse acc)] + [else + (let* ([field (string-trim + (car xs))] + [colon (char-index-from + field + #\: + 0 + (string-length + field))] + [key (and colon + (string-trim + (substring + field + 0 + colon)))] + [value (and colon + (string-trim + (substring + field + (+ colon + 1) + (string-length + field))))]) + (and key + value + (sg-string-prefix? + "$" + value) + (loop + (cdr xs) + (cons + (cons + key + (normalize-metavariable-name + value)) + acc))))]))]) + (and (python-simple-identifier? function) + (not (null? requirements)) + (list + (cons 'function function) + (cons 'requirements requirements))))))))) + (def (javascript-object-fields-alist fields) + (let loop ([xs fields] [acc '()]) + (cond + [(null? xs) acc] + [else + (let* ([field (string-trim (car xs))] + [colon (char-index-from + field + #\: + 0 + (string-length field))] + [key (and colon + (string-trim (substring field 0 colon)))] + [value (and colon + (string-trim + (substring + field + (+ colon 1) + (string-length field))))]) + (loop + (cdr xs) + (if (and key value) (cons (cons key value) acc) acc)))]))) + (def (javascript-object-call-equivalence-bindings requirements actual source object-start object-text) + (let loop ([xs requirements] [acc '()]) + (cond + [(null? xs) (reverse acc)] + [else + (let* ([requirement (car xs)] + [key (car requirement)] + [mvar (cdr requirement)] + [entry (assoc key actual)] + [value (and entry (cdr entry))] + [relative (and value + (string-find-substring-from + object-text + value + 0))] + [binding (and value + relative + (make-regex-capture-binding mvar value source + (+ object-start relative) + (+ object-start + relative + (string-length value))))]) + (and binding + (loop (cdr xs) (cons (cons mvar binding) acc))))]))) + (def (scan-javascript-object-call-equivalence-pattern + rule + path + source + pattern) + (let ([spec (javascript-object-call-equivalence-pattern-spec + pattern)]) + (and spec + (let* ([function (alist-ref/default spec 'function "")] + [requirements (alist-ref/default + spec + 'requirements + '())] + [needle (string-append function "(")] + [len (string-length source)]) + (let loop ([start 0] [acc '()]) + (let ([call-start (string-find-substring-from + source + needle + start)]) + (if (not call-start) + (nonempty-findings (reverse acc)) + (let* ([open (+ call-start + (string-length function))] + [close (find-matching-close-paren + source + open)] + [args (and close + (string-trim + (substring + source + (+ open 1) + (- close 1))))] + [arg args] + [object-start (and arg + (string-find-substring-from + source + arg + (+ open 1)))] + [object-end (and object-start + (+ object-start + (string-length arg)))] + [fields (and object-start + object-end + (outer-pair? arg #\{ #\}) + (object-field-items + arg + 1 + (- (string-length arg) 1)))] + [actual (and fields + (javascript-object-fields-alist + fields))] + [bindings (and actual + (javascript-object-call-equivalence-bindings requirements actual source + object-start arg))] + [finding (and bindings + (identifier-boundary-before? + source + call-start) + (finding-for-range-with-bindings rule path source call-start + close bindings))] + [next (if close + (max (+ call-start 1) close) + (+ call-start 1))]) + (loop + next + (if finding (cons finding acc) acc)))))))))) (def (nonempty-findings findings) (and (not (null? findings)) findings)) (def (find-matching-close-paren source open-index) @@ -24812,6 +25071,18 @@ source (rule-pattern rule))) (and (javascript-like-language? language) + (scan-javascript-varlet-equivalence-pattern + rule + path + source + (rule-pattern rule))) + (and (javascript-like-language? language) + (scan-javascript-object-call-equivalence-pattern + rule + path + source + (rule-pattern rule))) + (and (javascript-like-language? language) (scan-javascript-cp-pattern-fallbacks rule path source (rule-pattern rule) '())) (and (or (string=? language "python") (string=? language "python2") --- a/src/.jerbuild-hashes +++ b/src/.jerbuild-hashes @@ -3,11 +3,11 @@ ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC") ("src/semgrep/lang.ss" . "6982E07679D20836") ("src/semgrep/parse/parse-target.ss" . "E74854DDDACF6BA") - ("src/semgrep/scan.ss" . "BF8ECCAD7DD28E9E") + ("src/semgrep/scan.ss" . "D23BFC394F4D15B0") + ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") + ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") ("src/semgrep/rule.ss" . "E12C108153C181FA") ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0") - ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") - ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") ("src/semgrep/match/structural.ss" . "F7B63A9A6FA028B") ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E") ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC")) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -4538,6 +4538,253 @@ (loop next (if finding (cons finding acc) acc))))))))))) +(def (javascript-varlet-equivalence-pattern-spec pattern) + (let* ([trimmed (string-trim pattern)] + [match (re-search + (re "^var[ \\t]+\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*=[ \\t]*(.+?);?$") + trimmed + 0)]) + (and match + (let ([rhs (string-trim (re-match-group match 2))]) + (and (not (string=? rhs "")) + (list (cons 'metavariable (re-match-group match 1)) + (cons 'rhs rhs))))))) + +(def (scan-javascript-varlet-equivalence-pattern + rule + path + source + pattern) + (let ([spec (javascript-varlet-equivalence-pattern-spec pattern)]) + (and spec + (let* ([rhs (alist-ref/default spec 'rhs "")] + [mvar (alist-ref/default spec 'metavariable "")] + [rx (re (string-append + "(^|\\n)[ \\t]*(?:var|let|const)[ \\t]+" + "([A-Za-z_$][A-Za-z0-9_$]*)" + "[ \\t]*=[ \\t]*" + (regex-escape-string rhs) + "[ \\t]*;?"))]) + (let loop ([start 0] [acc '()]) + (let ([match (re-search rx source start)]) + (if (not match) + (nonempty-findings (reverse acc)) + (let* ([line-start + (if (and (< (re-match-start match) + (string-length source)) + (char=? (string-ref + source + (re-match-start match)) + #\newline)) + (+ (re-match-start match) 1) + (re-match-start match))] + [line-end (line-end-after source line-start)] + [first (line-first-nonspace + source + line-start + line-end)] + [raw-end (re-match-end match)] + [end (if (and (> raw-end first) + (char=? (string-ref + source + (- raw-end 1)) + #\;)) + (- raw-end 1) + raw-end)] + [name (re-match-group match 2)] + [name-start + (string-find-substring-from source name first)] + [binding + (and name-start + (make-regex-capture-binding + mvar + name + source + name-start + (+ name-start (string-length name))))] + [finding + (and binding + (finding-for-range-with-bindings + rule + path + source + first + end + (list (cons mvar binding))))] + [next (max (+ (re-match-start match) 1) + (re-match-end match))]) + (loop next + (if finding (cons finding acc) acc)))))))))) + +(def (javascript-object-call-equivalence-pattern-spec pattern) + (let* ([trimmed (string-trim pattern)] + [open (char-index-from trimmed #\( 0 (string-length trimmed))] + [close (and open (find-matching-close-paren trimmed open))]) + (and open + close + (= close (string-length trimmed)) + (let* ([function (string-trim (substring trimmed 0 open))] + [arg (string-trim + (substring trimmed (+ open 1) (- close 1)))] + [object? (and arg (outer-pair? (string-trim arg) #\{ #\}))]) + (and object? + (let* ([object (string-trim arg)] + [fields (object-field-items + object + 1 + (- (string-length object) 1))] + [requirements + (let loop ([xs fields] [acc '()]) + (cond + [(null? xs) (reverse acc)] + [else + (let* ([field (string-trim (car xs))] + [colon (char-index-from + field + #\: + 0 + (string-length field))] + [key (and colon + (string-trim + (substring field 0 colon)))] + [value (and colon + (string-trim + (substring + field + (+ colon 1) + (string-length field))))]) + (and key + value + (sg-string-prefix? "$" value) + (loop (cdr xs) + (cons (cons key + (normalize-metavariable-name + value)) + acc))))]))]) + (and (python-simple-identifier? function) + (not (null? requirements)) + (list (cons 'function function) + (cons 'requirements requirements))))))))) + +(def (javascript-object-fields-alist fields) + (let loop ([xs fields] [acc '()]) + (cond + [(null? xs) acc] + [else + (let* ([field (string-trim (car xs))] + [colon (char-index-from field #\: 0 (string-length field))] + [key (and colon (string-trim (substring field 0 colon)))] + [value (and colon + (string-trim + (substring field + (+ colon 1) + (string-length field))))]) + (loop (cdr xs) + (if (and key value) + (cons (cons key value) acc) + acc)))]))) + +(def (javascript-object-call-equivalence-bindings + requirements + actual + source + object-start + object-text) + (let loop ([xs requirements] [acc '()]) + (cond + [(null? xs) (reverse acc)] + [else + (let* ([requirement (car xs)] + [key (car requirement)] + [mvar (cdr requirement)] + [entry (assoc key actual)] + [value (and entry (cdr entry))] + [relative (and value + (string-find-substring-from object-text value 0))] + [binding + (and value + relative + (make-regex-capture-binding + mvar + value + source + (+ object-start relative) + (+ object-start relative (string-length value))))]) + (and binding + (loop (cdr xs) (cons (cons mvar binding) acc))))]))) + +(def (scan-javascript-object-call-equivalence-pattern + rule + path + source + pattern) + (let ([spec (javascript-object-call-equivalence-pattern-spec pattern)]) + (and spec + (let* ([function (alist-ref/default spec 'function "")] + [requirements (alist-ref/default spec 'requirements '())] + [needle (string-append function "(")] + [len (string-length source)]) + (let loop ([start 0] [acc '()]) + (let ([call-start (string-find-substring-from + source + needle + start)]) + (if (not call-start) + (nonempty-findings (reverse acc)) + (let* ([open (+ call-start (string-length function))] + [close (find-matching-close-paren source open)] + [args (and close + (string-trim + (substring source + (+ open 1) + (- close 1))))] + [arg args] + [object-start + (and arg + (string-find-substring-from + source + arg + (+ open 1)))] + [object-end (and object-start + (+ object-start + (string-length arg)))] + [fields + (and object-start + object-end + (outer-pair? arg #\{ #\}) + (object-field-items + arg + 1 + (- (string-length arg) 1)))] + [actual (and fields + (javascript-object-fields-alist + fields))] + [bindings + (and actual + (javascript-object-call-equivalence-bindings + requirements + actual + source + object-start + arg))] + [finding + (and bindings + (identifier-boundary-before? + source + call-start) + (finding-for-range-with-bindings + rule + path + source + call-start + close + bindings))] + [next (if close + (max (+ call-start 1) close) + (+ call-start 1))]) + (loop next + (if finding (cons finding acc) acc)))))))))) + (def (nonempty-findings findings) (and (not (null? findings)) findings)) @@ -25255,6 +25502,18 @@ source (rule-pattern rule))) (and (javascript-like-language? language) + (scan-javascript-varlet-equivalence-pattern + rule + path + source + (rule-pattern rule))) + (and (javascript-like-language? language) + (scan-javascript-object-call-equivalence-pattern + rule + path + source + (rule-pattern rule))) + (and (javascript-like-language? language) (scan-javascript-cp-pattern-fallbacks rule path --- a/tests/smoke.ss +++ b/tests/smoke.ss @@ -1292,6 +1292,29 @@ (check (length equality-findings) => 1) (check (finding-start-line (car equality-findings)) => 2))) +(test-case "scan JavaScript equivalence pattern fixture fallbacks" + (let* ([varlet-config + "rules:\n - id: demo.js.equiv.varlet\n languages: [javascript]\n message: varlet\n severity: WARNING\n pattern: var $X = \"hello\";\n"] + [varlet-findings + (scan-config-string + varlet-config + "javascript" + "demo.js" + "var x = \"hello\";\nlet y = \"hello\";\nconst z = \"hello\";\n")]) + (check (length varlet-findings) => 3) + (check (finding-start-line (car varlet-findings)) => 1) + (check (finding-start-line (caddr varlet-findings)) => 3)) + (let* ([object-call-config + "rules:\n - id: demo.js.equiv.objectcall\n languages: [javascript]\n message: object call\n severity: WARNING\n pattern: foo({kwd2:$X, kwd1:$Y})\n"] + [object-call-findings + (scan-config-string + object-call-config + "javascript" + "demo.js" + "foo({kwd1:1, kwd2:2, kwd3:3});\nfoo({kwd1:1});\n")]) + (check (length object-call-findings) => 1) + (check (finding-start-line (car object-call-findings)) => 1))) + (test-case "scan JavaScript ellipsis call arguments" (let* ([js-config "rules:\n - id: demo.console.ellipsis\n languages: [javascript]\n message: console log\n severity: INFO\n pattern: console.log(...)\n"]