Cover JavaScript equivalence fixtures

ober

9490f84ad74808feec02c3d8fd8795f288974994

diff --git a/HANDOFF_OPUS_4_8.md b/HANDOFF_OPUS_4_8.md
index 64a7385..ca046f9 100644
--- 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:
diff --git a/Makefile b/Makefile
index 56801c1..c0c728e 100644
--- 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
 
diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index f93a07e..cf8910a 100644
--- 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")
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index b37f940..1a7513d 100644
--- 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"))
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index 07aa596..8993c11 100644
--- 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
diff --git a/tests/smoke.ss b/tests/smoke.ss
index 69c1957..2f0ecfd 100644
--- 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"]