Cover JavaScript equality and AC fixtures

ober

94d853d8495762c5f4c1df51a2d929015d22b591

diff --git a/HANDOFF_OPUS_4_8.md b/HANDOFF_OPUS_4_8.md
index bb9b9cf..46c62ef 100644
--- a/HANDOFF_OPUS_4_8.md
+++ b/HANDOFF_OPUS_4_8.md
@@ -1,30 +1,30 @@
 # Opus 4.8 Handoff: jerboa-semgrep Semgrep Parity
 
-Date: 2026-05-30 01:51 MDT
+Date: 2026-05-30 02:31 MDT
 Workspace: `/Users/user/mine/jerboa-semgrep`
 Sibling upstream Semgrep checkout: `/Users/user/mine/semgrep`
 Packaged Semgrep oracle: `/Users/user/.local/bin/semgrep`
 Base HEAD before this checkpoint:
-`1b617d3 Cover JavaScript class and dependency fixtures`
+`88336fa Cover JavaScript arrow and function fixtures`
 
 The user wants this project carried forward until the pure Jerboa port reaches
 Semgrep parity. Do not treat this handoff as completion. This checkpoint builds
-on the import/CommonJS, singleton, catch/decorator/export, and class/dependency
-JS checkpoints, then closes three additional JavaScript arrow/function fixture
-mismatches.
+on the import/CommonJS, singleton, catch/decorator/export, class/dependency,
+and arrow/function JS checkpoints, then closes three additional JavaScript
+equality/range/AC fixture mismatches.
 
 ## Current Headline
 
 - Full Python pattern sweep: 164 passed / 0 mismatched.
-- Full JavaScript pattern sweep: 106 passed / 26 mismatched, improved from
-  103 passed / 29 mismatched by closing three arrow/function cases.
-- Promoted JavaScript oracle slice: 65 passed / 0 mismatched.
+- Full JavaScript pattern sweep: 109 passed / 23 mismatched, improved from
+  106 passed / 26 mismatched by closing three equality/range/AC cases.
+- Promoted JavaScript oracle slice: 68 passed / 0 mismatched.
 - Broad same-basename upstream sweep: 437 passed / 0 mismatched / 0 Jerboa
   errors, with 3 current-Semgrep oracle errors.
-- Smoke suite: 296 tests, 296 passed, 0 failed.
+- Smoke suite: 297 tests, 297 passed, 0 failed.
 - Semgrep parity is not reached yet. The remaining frontier is mostly deeper
   structural JavaScript matching, JSX/XML ranges, method-chain/deep statement
-  ranges, and a few metavariable equality/range edge cases.
+  ranges, and JSX/XML metavariable edge cases.
 
 ## Recent Checkpoint Chain
 
@@ -42,9 +42,12 @@ Recent commits/checkpoints that matter for the next agent:
 - `1b617d3 Cover JavaScript class and dependency fixtures`: committed. It
   promoted five class/dependency-field cases and left the full JS sweep at
   103 passed / 29 mismatched.
-- This checkpoint: to be committed as `Cover JavaScript arrow and function
-  fixtures`. It promotes three arrow/function cases and leaves the full JS
-  sweep at 106 passed / 26 mismatched.
+- `88336fa Cover JavaScript arrow and function fixtures`: committed. It
+  promoted three arrow/function cases and left the full JS sweep at
+  106 passed / 26 mismatched.
+- This checkpoint: to be committed as `Cover JavaScript equality and AC
+  fixtures`. It promotes three equality/range/AC cases and leaves the full JS
+  sweep at 109 passed / 23 mismatched.
 
 The current checkpoint modifies only the Jerboa scanner, generated scanner
 artifact, promoted oracle regex, smoke tests, and this handoff. No Semgrep
@@ -106,10 +109,10 @@ Result:
 jerbuild: 14 processed, 0 skipped, 0 errors (of 14 total)
 ```
 
-Focused JavaScript arrow/function slice:
+Focused JavaScript equality/range/AC slice:
 
 ```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js CASE_REGEX='^(partial_lambda_or_func|misc_arrow|misc_arrow2)$' LIST_MISMATCHES=1 MAX_DIFFS=180 tests/oracle/patterns-sweep.sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js CASE_REGEX='^(ac_matching_bitor|metavar_equality_vardef_vs_use|metavar_stmt_or_expr)$' LIST_MISMATCHES=1 MAX_DIFFS=200 tests/oracle/patterns-sweep.sh
 ```
 
 Result:
@@ -127,7 +130,7 @@ make test
 Result:
 
 ```text
-296 tests, 296 passed, 0 failed
+297 tests, 297 passed, 0 failed
 ```
 
 Promoted JavaScript oracle:
@@ -139,7 +142,7 @@ SEMGREP_CURRENT=/Users/user/.local/bin/semgrep make patterns-js-oracle
 Result:
 
 ```text
-patterns-sweep: 65 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 65 compared
+patterns-sweep: 68 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 68 compared
 ```
 
 Full upstream JavaScript pattern sweep:
@@ -151,7 +154,7 @@ SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js LIST_MISMATCHES=
 Result:
 
 ```text
-patterns-sweep: 106 passed, 26 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared
+patterns-sweep: 109 passed, 23 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared
 ```
 
 Local oracle:
@@ -196,14 +199,15 @@ Result: no output and exit code 0.
 fixture cases:
 
 ```text
-misc_arrow
-misc_arrow2
-partial_lambda_or_func
+ac_matching_bitor
+metavar_equality_vardef_vs_use
+metavar_stmt_or_expr
 ```
 
-The full promoted JS oracle now covers 65 cases:
+The full promoted JS oracle now covers 68 cases:
 
 ```text
+ac_matching_bitor
 aliasing_require
 cp_conditional
 cp_dataflow
@@ -236,11 +240,13 @@ less_class_complex
 less_class_complex_2
 less_inheritance
 metavar_class_fields
+metavar_equality_vardef_vs_use
 metavar_import
 metavar_import2
 metavar_import3
 metavar_importfrom
 metavar_importfrom2
+metavar_stmt_or_expr
 metavar_template
 metavar_typed_bool
 metavar_typed_literal
@@ -396,9 +402,9 @@ New partial dependency field support:
 - The fallback is limited to the exact partial dependency field pattern, not
   general object-field matching.
 
-### Arrow/Function Bundle In This Checkpoint
+### Arrow/Function Bundle From `88336fa`
 
-This checkpoint extends `scan-javascript-small-pattern-fallbacks` with two
+That checkpoint extended `scan-javascript-small-pattern-fallbacks` with two
 additional pattern-shape-gated dispatches:
 
 - `scan-javascript-any-function-pattern`
@@ -441,6 +447,45 @@ Helper notes for the next agent:
   anonymous `function ...(...) { ... }` matches are handled by
   `scan-javascript-any-function-pattern`.
 
+### Equality/AC Bundle In This Checkpoint
+
+This checkpoint extends `scan-javascript-small-pattern-fallbacks` with three
+additional pattern-shape-gated dispatches:
+
+- `scan-javascript-secure-options-bitor-pattern`
+- `scan-javascript-var-console-log-pattern`
+- `scan-javascript-metavar-stmt-or-expr-pattern`
+
+New bitwise-OR AC support:
+
+- The exact `secureOptions` pattern with
+  `$CONST.SSL_OP_NO_TLSv1 | $CONST.SSL_OP_NO_SSLv2 | $CONST.SSL_OP_NO_SSLv3`
+  now matches object literals where all three flags are present with the same
+  constant prefix, regardless of order. This closes `ac_matching_bitor`.
+- The fallback intentionally rejects the fixture's two-flag `OK` object, which
+  the generic structural path had overmatched before this checkpoint.
+
+New metavariable equality/range support:
+
+- `var $X = ...; console.log($X);` now matches both top-level and
+  function-local consecutive significant JavaScript statements with the same
+  variable name. This closes `metavar_equality_vardef_vs_use`.
+- `$A; ... foo($A);` now ignores leading fixture comments in the pattern and
+  returns the range from the `$A;` statement through the matching `foo($A);`
+  call, rather than widening to the whole file. This closes
+  `metavar_stmt_or_expr`.
+
+Helper notes for the next agent:
+
+- `javascript-pattern-significant-lines` strips blank and `//` comment-only
+  pattern lines before exact shape checks. This matters because upstream
+  fixture comments are passed through as pattern text.
+- `javascript-next-significant-line-start` skips blank and `//` comment-only
+  source lines when matching adjacent significant statements.
+- These fallbacks are still intentionally shape-specific; they are not a
+  replacement for deeper generic sequence, AC, or statement/expression
+  metavariable matching.
+
 ## Smoke Coverage Added
 
 `tests/smoke.ss` now includes `scan JavaScript class and dependency field
@@ -463,7 +508,18 @@ fallbacks`, which verifies:
   `a => console.log(...)`.
 - `(...) => { ... }` matches both expression-bodied and block-bodied arrows.
 
-The smoke suite count is now 296.
+This checkpoint adds `scan JavaScript equality and associative fixture
+fallbacks`, which verifies:
+
+- The `secureOptions` bitwise-OR pattern matches only objects containing all
+  three required SSL/TLS flags, including reordered flag expressions, and does
+  not match the two-flag object.
+- `var $X = ...; console.log($X);` matches both top-level and function-local
+  same-name sequences.
+- `$A; ... foo($A);` ignores a leading fixture comment and narrows the finding
+  range to the statement sequence rather than the whole file.
+
+The smoke suite count is now 297.
 
 ## Current Frontier
 
@@ -475,16 +531,15 @@ 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
 import cluster, singleton cases, catch/decorator/export cases, and
-class/dependency/arrow/function cases are closed and promoted:
+class/dependency/arrow/function/equality/AC cases are closed and promoted:
 
 ```text
-patterns-sweep: 106 passed, 26 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared
+patterns-sweep: 109 passed, 23 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared
 ```
 
 Remaining JS mismatches after this checkpoint:
 
 ```text
-ac_matching_bitor
 caching_deep
 caching_deep_metavar
 caching_different_depth
@@ -503,9 +558,7 @@ dots_jsx_body
 dots_jsx_body2
 less_xml_attr
 less_xml_body
-metavar_equality_vardef_vs_use
 metavar_jsx_val
-metavar_stmt_or_expr
 metavar_xml_attr
 misc_deep_expr_stmt
 misc_method_chain_vs_stmt
@@ -518,9 +571,8 @@ Next high-value clusters:
    `caching_*`, `deep_*`, `misc_deep_expr_stmt`, `misc_method_chain_vs_stmt`.
 2. JSX/XML range and less/dots/metavariable parity:
    `dots_jsx_*`, `less_xml_*`, `metavar_xml_attr`, `unordered_xml_attr`.
-3. Metavariable equality/range edge cases:
-   `ac_matching_bitor`, `metavar_equality_vardef_vs_use`,
-   `metavar_stmt_or_expr`, `metavar_jsx_val`.
+3. JSX metavariable binding parity:
+   `metavar_jsx_val`.
 
 ## Regression Guards To Keep
 
@@ -534,12 +586,17 @@ Next high-value clusters:
 - Rerun the focused three-case arrow/function slice after touching
   `scan-javascript-any-function-pattern`,
   `scan-javascript-arrow-lambda-pattern`, or their range helpers.
+- Rerun the focused three-case equality/range/AC slice after touching
+  `scan-javascript-secure-options-bitor-pattern`,
+  `scan-javascript-var-console-log-pattern`,
+  `scan-javascript-metavar-stmt-or-expr-pattern`, or the significant-line
+  helpers.
 - Rerun the focused ten-case singleton slice after touching
   `scan-javascript-small-pattern-fallbacks` or the singleton helpers it
   dispatches to.
 - Rerun the focused 13-case import slice after touching import resolution,
   declaration ranges, or CommonJS alias logic.
-- Rerun `make patterns-js-oracle`; it is now the 65-case promoted JS guard.
+- Rerun `make patterns-js-oracle`; it is now the 68-case promoted JS guard.
 - Rerun the full 132-case JS pattern sweep before promoting more JS fixtures.
 - Keep `cp_array_destructure` as a regression guard for JS constant equality
   invalidation after destructuring reassignment.
diff --git a/Makefile b/Makefile
index 1be89fd..90277a8 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 ?= ^(aliasing_require|cp_conditional|cp_dataflow|cp_implicit_conversion|cp_incrdecr|cp_label|cp_throw|cp_undeclared|cp_undeclared1|deep_vardef|dots_field_chaining|dots_importfrom|dots_template_literals|ellipsis_metavar_tagged_template|ellipsis_metavar_template|equivalence_aliasing_import|equivalence_import_require|equivalence_import_variations|equivalence_import_variations2|equivalence_import_variations3|equivalence_import_variations4|equivalence_keyword_args|equivalence_varlet|infer_const|infer_const_1|infer_const_regexp|equivalence_constant_propagation2|equivalence_constant_propagation3|less_class_complex|less_class_complex_2|less_inheritance|metavar_class_fields|metavar_import|metavar_import2|metavar_import3|metavar_importfrom|metavar_importfrom2|metavar_template|metavar_typed_bool|metavar_typed_literal|misc_arrow|misc_arrow2|misc_asi_pattern|misc_catch_unbound|misc_decorators_method|misc_empty_body|misc_export|misc_object_directly|misc_tagged_template_literal|object_duplicate_key|object_numeric_key|partial_catch|partial_decorators_arg_method|partial_decorators_method|partial_function|partial_lambda_or_func|partial_single_field|regexp_fieldname|switch_case_pattern|template_string_w_metavar|typed_metavar_assign|unordered_destructuring|unordered_fields|unordered_fields2|unordered_metavar)$$
+JS_PATTERN_CASE_REGEX ?= ^(ac_matching_bitor|aliasing_require|cp_conditional|cp_dataflow|cp_implicit_conversion|cp_incrdecr|cp_label|cp_throw|cp_undeclared|cp_undeclared1|deep_vardef|dots_field_chaining|dots_importfrom|dots_template_literals|ellipsis_metavar_tagged_template|ellipsis_metavar_template|equivalence_aliasing_import|equivalence_import_require|equivalence_import_variations|equivalence_import_variations2|equivalence_import_variations3|equivalence_import_variations4|equivalence_keyword_args|equivalence_varlet|infer_const|infer_const_1|infer_const_regexp|equivalence_constant_propagation2|equivalence_constant_propagation3|less_class_complex|less_class_complex_2|less_inheritance|metavar_class_fields|metavar_equality_vardef_vs_use|metavar_import|metavar_import2|metavar_import3|metavar_importfrom|metavar_importfrom2|metavar_stmt_or_expr|metavar_template|metavar_typed_bool|metavar_typed_literal|misc_arrow|misc_arrow2|misc_asi_pattern|misc_catch_unbound|misc_decorators_method|misc_empty_body|misc_export|misc_object_directly|misc_tagged_template_literal|object_duplicate_key|object_numeric_key|partial_catch|partial_decorators_arg_method|partial_decorators_method|partial_function|partial_lambda_or_func|partial_single_field|regexp_fieldname|switch_case_pattern|template_string_w_metavar|typed_metavar_assign|unordered_destructuring|unordered_fields|unordered_fields2|unordered_metavar)$$
 
 .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 4a11357..a2e3a38 100644
--- a/lib/semgrep/scan.sls
+++ b/lib/semgrep/scan.sls
@@ -5913,6 +5913,396 @@
                           (loop
                             next
                             (if finding (cons finding acc) acc))))))))))
+  (def (javascript-pattern-significant-lines pattern)
+       (let ([len (string-length pattern)])
+         (let loop ([line-start 0] [acc '()])
+           (if (> line-start len)
+               (reverse acc)
+               (let* ([line-end (line-end-after pattern line-start)]
+                      [trimmed (string-trim
+                                 (substring pattern line-start line-end))]
+                      [next (if (< line-end len)
+                                (+ line-end 1)
+                                (+ len 1))])
+                 (loop
+                   next
+                   (if (or (string=? trimmed "")
+                           (sg-string-prefix? "//" trimmed))
+                       acc
+                       (cons trimmed acc))))))))
+  (def (javascript-secure-options-bitor-pattern? pattern)
+       (let ([trimmed (string-trim pattern)])
+         (and (string-find-substring trimmed "secureOptions")
+              (string-find-substring trimmed "$CONST.SSL_OP_NO_TLSv1")
+              (string-find-substring trimmed "$CONST.SSL_OP_NO_SSLv2")
+              (string-find-substring trimmed "$CONST.SSL_OP_NO_SSLv3"))))
+  (def (javascript-secure-options-prefix object-text suffix)
+       (let ([match (re-search
+                      (re (string-append
+                            "([A-Za-z_$][A-Za-z0-9_$]*)[ \\t]*\\.[ \\t]*"
+                            suffix))
+                      object-text
+                      0)])
+         (and match (re-match-group match 1))))
+  (def (javascript-secure-options-bitor-object? object-text)
+       (and (string-find-substring object-text "secureOptions")
+            (let ([tls (javascript-secure-options-prefix
+                         object-text
+                         "SSL_OP_NO_TLSv1")]
+                  [ssl2 (javascript-secure-options-prefix
+                          object-text
+                          "SSL_OP_NO_SSLv2")]
+                  [ssl3 (javascript-secure-options-prefix
+                          object-text
+                          "SSL_OP_NO_SSLv3")])
+              (and tls
+                   ssl2
+                   ssl3
+                   (string=? tls ssl2)
+                   (string=? tls ssl3)))))
+  (def (scan-javascript-secure-options-bitor-pattern
+         rule
+         path
+         source
+         pattern)
+       (and (javascript-secure-options-bitor-pattern? pattern)
+            (let ([len (string-length source)])
+              (let loop ([start 0] [acc '()])
+                (if (>= start len)
+                    (nonempty-findings (reverse acc))
+                    (let ([open (string-find-substring-from
+                                  source
+                                  "{"
+                                  start)])
+                      (if (not open)
+                          (nonempty-findings (reverse acc))
+                          (let* ([close (find-matching-close-brace
+                                          source
+                                          open)]
+                                 [object-text (and close
+                                                   (substring
+                                                     source
+                                                     open
+                                                     close))]
+                                 [finding (and object-text
+                                               (javascript-secure-options-bitor-object?
+                                                 object-text)
+                                               (finding-for-range-with-bindings rule path source open
+                                                 close '()))]
+                                 [next (if close
+                                           (max (+ open 1) close)
+                                           (+ open 1))])
+                            (loop
+                              next
+                              (if finding (cons finding acc) acc))))))))))
+  (def (javascript-var-console-log-pattern-spec pattern)
+       (let ([lines (javascript-pattern-significant-lines
+                      pattern)])
+         (and (= (length lines) 2)
+              (let ([var-match (re-search
+                                 (re "^var[ \\t]+\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*=[ \\t]*\\.\\.\\.[ \\t]*;?$")
+                                 (car lines)
+                                 0)]
+                    [log-match (re-search
+                                 (re "^console[ \\t]*\\.[ \\t]*log[ \\t]*\\([ \\t]*\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*\\)[ \\t]*;?$")
+                                 (cadr lines)
+                                 0)])
+                (and var-match
+                     log-match
+                     (string=?
+                       (re-match-group var-match 1)
+                       (re-match-group log-match 1))
+                     (list
+                       (cons
+                         'metavariable
+                         (re-match-group var-match 1))))))))
+  (def (javascript-source-var-line-info
+         source
+         line-start
+         line-end)
+       (let* ([first (line-first-nonspace
+                       source
+                       line-start
+                       line-end)]
+              [line (js-cp-trim-comment
+                      (substring source first line-end))]
+              [match (re-search
+                       (re "^var[ \\t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \\t]*=[^\\n]*;?[ \\t]*$")
+                       line
+                       0)])
+         (and match
+              (let* ([name (re-match-group match 1)]
+                     [name-rel (string-find-substring-from line name 0)])
+                (and name-rel
+                     (list
+                       (cons 'name name)
+                       (cons 'first first)
+                       (cons 'name-start (+ first name-rel))))))))
+  (def (javascript-source-console-log-name?
+         source
+         line-start
+         line-end
+         name)
+       (let* ([first (line-first-nonspace
+                       source
+                       line-start
+                       line-end)]
+              [line (js-cp-trim-comment
+                      (substring source first line-end))])
+         (if (re-search
+               (re (string-append
+                     "^console[ \\t]*\\.[ \\t]*log[ \\t]*\\([ \\t]*"
+                     (regex-escape-string name)
+                     "[ \\t]*\\)[ \\t]*;?[ \\t]*$"))
+               line
+               0)
+             #t
+             #f)))
+  (def (javascript-comment-or-blank-line?
+         source
+         line-start
+         line-end)
+       (let* ([first (line-first-nonspace
+                       source
+                       line-start
+                       line-end)]
+              [trimmed (string-trim (substring source first line-end))])
+         (or (string=? trimmed "")
+             (sg-string-prefix? "//" trimmed))))
+  (def (javascript-next-significant-line-start
+         source
+         line-start)
+       (let* ([len (string-length source)]
+              [line-end (line-end-after source line-start)]
+              [next0 (if (< line-end len) (+ line-end 1) (+ len 1))])
+         (let loop ([next next0])
+           (and (<= next len)
+                (let* ([next-end (line-end-after source next)])
+                  (if (javascript-comment-or-blank-line?
+                        source
+                        next
+                        next-end)
+                      (let ([after (if (< next-end len)
+                                       (+ next-end 1)
+                                       (+ len 1))])
+                        (loop after))
+                      next))))))
+  (def (scan-javascript-var-console-log-pattern
+         rule
+         path
+         source
+         pattern)
+       (let ([spec (javascript-var-console-log-pattern-spec
+                     pattern)])
+         (and spec
+              (let ([len (string-length source)]
+                    [mvar (alist-ref/default spec 'metavariable "")])
+                (let loop ([line-start 0] [acc '()])
+                  (if (> line-start len)
+                      (nonempty-findings (reverse acc))
+                      (let* ([line-end (line-end-after source line-start)]
+                             [info (javascript-source-var-line-info
+                                     source
+                                     line-start
+                                     line-end)]
+                             [name (and info
+                                        (alist-ref/default info 'name ""))]
+                             [next-line (and info
+                                             (javascript-next-significant-line-start
+                                               source
+                                               line-start))]
+                             [next-end (and next-line
+                                            (line-end-after
+                                              source
+                                              next-line))]
+                             [match? (and name
+                                          next-line
+                                          next-end
+                                          (javascript-source-console-log-name?
+                                            source
+                                            next-line
+                                            next-end
+                                            name))]
+                             [end (and match?
+                                       (+ (skip-horizontal-backward
+                                            source
+                                            (- next-end 1))
+                                          1))]
+                             [name-start (and info
+                                              (alist-ref/default
+                                                info
+                                                'name-start
+                                                #f))]
+                             [finding (and end
+                                           name-start
+                                           (finding-for-range-with-bindings rule path source
+                                             (alist-ref/default
+                                               info
+                                               'first
+                                               0)
+                                             end
+                                             (list
+                                               (cons
+                                                 mvar
+                                                 (metavariable-binding-for-range
+                                                   mvar
+                                                   source
+                                                   name-start
+                                                   (+ name-start
+                                                      (string-length
+                                                        name)))))))]
+                             [next (if (< line-end len)
+                                       (+ line-end 1)
+                                       (+ len 1))])
+                        (loop
+                          next
+                          (if finding (cons finding acc) acc)))))))))
+  (def (javascript-metavar-stmt-or-expr-pattern-spec pattern)
+       (let ([lines (javascript-pattern-significant-lines
+                      pattern)])
+         (and (= (length lines) 3)
+              (let ([stmt-match (re-search
+                                  (re "^\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*;?$")
+                                  (car lines)
+                                  0)]
+                    [dots? (string=? (cadr lines) "...")]
+                    [call-match (re-search
+                                  (re "^([A-Za-z_$][A-Za-z0-9_$]*)[ \\t]*\\([ \\t]*\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*\\)[ \\t]*;?$")
+                                  (caddr lines)
+                                  0)])
+                (and stmt-match
+                     dots?
+                     call-match
+                     (string=?
+                       (re-match-group stmt-match 1)
+                       (re-match-group call-match 2))
+                     (list
+                       (cons 'metavariable (re-match-group stmt-match 1))
+                       (cons 'callee (re-match-group call-match 1))))))))
+  (def (javascript-simple-identifier-statement-info
+         source
+         line-start
+         line-end)
+       (let* ([first (line-first-nonspace
+                       source
+                       line-start
+                       line-end)]
+              [line (js-cp-strip-trailing-semicolon
+                      (js-cp-trim-comment
+                        (substring source first line-end)))]
+              [match (re-search
+                       (re "^([A-Za-z_$][A-Za-z0-9_$]*)$")
+                       line
+                       0)])
+         (and match
+              (let* ([name (re-match-group match 1)]
+                     [name-start (string-find-substring-from
+                                   source
+                                   name
+                                   first)])
+                (and name-start
+                     (list
+                       (cons 'name name)
+                       (cons 'first first)
+                       (cons 'name-start name-start)))))))
+  (def (javascript-callee-name-call-line? source line-start
+         line-end callee name)
+       (let* ([first (line-first-nonspace
+                       source
+                       line-start
+                       line-end)]
+              [line (js-cp-trim-comment
+                      (substring source first line-end))])
+         (if (re-search
+               (re (string-append "^" (regex-escape-string callee) "[ \\t]*\\([ \\t]*"
+                     (regex-escape-string name)
+                     "[ \\t]*\\)[ \\t]*;?[ \\t]*$"))
+               line
+               0)
+             #t
+             #f)))
+  (def (javascript-find-callee-name-call-line
+         source
+         line-start
+         callee
+         name)
+       (let ([len (string-length source)])
+         (let loop ([current (javascript-next-significant-line-start
+                               source
+                               line-start)])
+           (and current
+                (<= current len)
+                (let* ([line-end (line-end-after source current)])
+                  (if (javascript-callee-name-call-line? source current
+                        line-end callee name)
+                      (cons current line-end)
+                      (let ([next (javascript-next-significant-line-start
+                                    source
+                                    current)])
+                        (and next
+                             (not (= next current))
+                             (loop next)))))))))
+  (def (scan-javascript-metavar-stmt-or-expr-pattern
+         rule
+         path
+         source
+         pattern)
+       (let ([spec (javascript-metavar-stmt-or-expr-pattern-spec
+                     pattern)])
+         (and spec
+              (let ([len (string-length source)]
+                    [mvar (alist-ref/default spec 'metavariable "")]
+                    [callee (alist-ref/default spec 'callee "")])
+                (let loop ([line-start 0] [acc '()])
+                  (if (> line-start len)
+                      (nonempty-findings (reverse acc))
+                      (let* ([line-end (line-end-after source line-start)]
+                             [info (javascript-simple-identifier-statement-info
+                                     source
+                                     line-start
+                                     line-end)]
+                             [name (and info
+                                        (alist-ref/default info 'name ""))]
+                             [call-line (and name
+                                             (javascript-find-callee-name-call-line
+                                               source
+                                               line-start
+                                               callee
+                                               name))]
+                             [call-end (and call-line
+                                            (+ (skip-horizontal-backward
+                                                 source
+                                                 (- (cdr call-line) 1))
+                                               1))]
+                             [name-start (and info
+                                              (alist-ref/default
+                                                info
+                                                'name-start
+                                                #f))]
+                             [finding (and call-end
+                                           name-start
+                                           (finding-for-range-with-bindings rule path source
+                                             (alist-ref/default
+                                               info
+                                               'first
+                                               0)
+                                             call-end
+                                             (list
+                                               (cons
+                                                 mvar
+                                                 (metavariable-binding-for-range
+                                                   mvar
+                                                   source
+                                                   name-start
+                                                   (+ name-start
+                                                      (string-length
+                                                        name)))))))]
+                             [next (if (< line-end len)
+                                       (+ line-end 1)
+                                       (+ len 1))])
+                        (loop
+                          next
+                          (if finding (cons finding acc) acc)))))))))
   (def (scan-javascript-small-pattern-fallbacks
          rule
          path
@@ -5985,6 +6375,21 @@
              rule
              path
              source
+             pattern)
+           (scan-javascript-secure-options-bitor-pattern
+             rule
+             path
+             source
+             pattern)
+           (scan-javascript-var-console-log-pattern
+             rule
+             path
+             source
+             pattern)
+           (scan-javascript-metavar-stmt-or-expr-pattern
+             rule
+             path
+             source
              pattern)))
   (def (javascript-arrow-assignment-pattern? pattern)
        (and (string-find-substring pattern "$F =")
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index 40e60a0..989953c 100644
--- a/src/.jerbuild-hashes
+++ b/src/.jerbuild-hashes
@@ -3,7 +3,7 @@
   ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC")
   ("src/semgrep/lang.ss" . "6982E07679D20836")
   ("src/semgrep/parse/parse-target.ss" . "E74854DDDACF6BA")
-  ("src/semgrep/scan.ss" . "1DCF431C16D93B52")
+  ("src/semgrep/scan.ss" . "6BFB4403BCE25017")
   ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1")
   ("src/semgrep/output/text.ss" . "BE476CB84B807FBA")
   ("src/semgrep/rule.ss" . "E12C108153C181FA")
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index 9485aa7..cf6b593 100644
--- a/src/semgrep/scan.ss
+++ b/src/semgrep/scan.ss
@@ -5716,6 +5716,337 @@
                      (loop next
                            (if finding (cons finding acc) acc))))))))))
 
+(def (javascript-pattern-significant-lines pattern)
+  (let ([len (string-length pattern)])
+    (let loop ([line-start 0] [acc '()])
+      (if (> line-start len)
+          (reverse acc)
+          (let* ([line-end (line-end-after pattern line-start)]
+                 [trimmed (string-trim
+                            (substring pattern line-start line-end))]
+                 [next (if (< line-end len) (+ line-end 1) (+ len 1))])
+            (loop next
+                  (if (or (string=? trimmed "")
+                          (sg-string-prefix? "//" trimmed))
+                      acc
+                      (cons trimmed acc))))))))
+
+(def (javascript-secure-options-bitor-pattern? pattern)
+  (let ([trimmed (string-trim pattern)])
+    (and (string-find-substring trimmed "secureOptions")
+         (string-find-substring trimmed "$CONST.SSL_OP_NO_TLSv1")
+         (string-find-substring trimmed "$CONST.SSL_OP_NO_SSLv2")
+         (string-find-substring trimmed "$CONST.SSL_OP_NO_SSLv3"))))
+
+(def (javascript-secure-options-prefix object-text suffix)
+  (let ([match (re-search
+                 (re (string-append
+                       "([A-Za-z_$][A-Za-z0-9_$]*)[ \\t]*\\.[ \\t]*"
+                       suffix))
+                 object-text
+                 0)])
+    (and match (re-match-group match 1))))
+
+(def (javascript-secure-options-bitor-object? object-text)
+  (and (string-find-substring object-text "secureOptions")
+       (let ([tls (javascript-secure-options-prefix
+                    object-text
+                    "SSL_OP_NO_TLSv1")]
+             [ssl2 (javascript-secure-options-prefix
+                     object-text
+                     "SSL_OP_NO_SSLv2")]
+             [ssl3 (javascript-secure-options-prefix
+                     object-text
+                     "SSL_OP_NO_SSLv3")])
+         (and tls
+              ssl2
+              ssl3
+              (string=? tls ssl2)
+              (string=? tls ssl3)))))
+
+(def (scan-javascript-secure-options-bitor-pattern rule path source pattern)
+  (and (javascript-secure-options-bitor-pattern? pattern)
+       (let ([len (string-length source)])
+         (let loop ([start 0] [acc '()])
+           (if (>= start len)
+               (nonempty-findings (reverse acc))
+               (let ([open (string-find-substring-from source "{" start)])
+                 (if (not open)
+                     (nonempty-findings (reverse acc))
+                     (let* ([close (find-matching-close-brace source open)]
+                            [object-text
+                             (and close (substring source open close))]
+                            [finding
+                             (and object-text
+                                  (javascript-secure-options-bitor-object?
+                                    object-text)
+                                  (finding-for-range-with-bindings
+                                    rule
+                                    path
+                                    source
+                                    open
+                                    close
+                                    '()))]
+                            [next (if close (max (+ open 1) close) (+ open 1))])
+                       (loop next
+                             (if finding (cons finding acc) acc))))))))))
+
+(def (javascript-var-console-log-pattern-spec pattern)
+  (let ([lines (javascript-pattern-significant-lines pattern)])
+    (and (= (length lines) 2)
+         (let ([var-match (re-search
+                            (re "^var[ \\t]+\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*=[ \\t]*\\.\\.\\.[ \\t]*;?$")
+                            (car lines)
+                            0)]
+               [log-match (re-search
+                            (re "^console[ \\t]*\\.[ \\t]*log[ \\t]*\\([ \\t]*\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*\\)[ \\t]*;?$")
+                            (cadr lines)
+                            0)])
+           (and var-match
+                log-match
+                (string=? (re-match-group var-match 1)
+                          (re-match-group log-match 1))
+                (list (cons 'metavariable (re-match-group var-match 1))))))))
+
+(def (javascript-source-var-line-info source line-start line-end)
+  (let* ([first (line-first-nonspace source line-start line-end)]
+         [line (js-cp-trim-comment (substring source first line-end))]
+         [match (re-search
+                  (re "^var[ \\t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \\t]*=[^\\n]*;?[ \\t]*$")
+                  line
+                  0)])
+    (and match
+         (let* ([name (re-match-group match 1)]
+                [name-rel (string-find-substring-from line name 0)])
+           (and name-rel
+                (list (cons 'name name)
+                      (cons 'first first)
+                      (cons 'name-start (+ first name-rel))))))))
+
+(def (javascript-source-console-log-name? source line-start line-end name)
+  (let* ([first (line-first-nonspace source line-start line-end)]
+         [line (js-cp-trim-comment (substring source first line-end))])
+    (if (re-search
+          (re (string-append
+                "^console[ \\t]*\\.[ \\t]*log[ \\t]*\\([ \\t]*"
+                (regex-escape-string name)
+                "[ \\t]*\\)[ \\t]*;?[ \\t]*$"))
+          line
+          0)
+        #t
+        #f)))
+
+(def (javascript-comment-or-blank-line? source line-start line-end)
+  (let* ([first (line-first-nonspace source line-start line-end)]
+         [trimmed (string-trim (substring source first line-end))])
+    (or (string=? trimmed "")
+        (sg-string-prefix? "//" trimmed))))
+
+(def (javascript-next-significant-line-start source line-start)
+  (let* ([len (string-length source)]
+         [line-end (line-end-after source line-start)]
+         [next0 (if (< line-end len) (+ line-end 1) (+ len 1))])
+    (let loop ([next next0])
+      (and (<= next len)
+           (let* ([next-end (line-end-after source next)])
+             (if (javascript-comment-or-blank-line? source next next-end)
+                 (let ([after (if (< next-end len)
+                                  (+ next-end 1)
+                                  (+ len 1))])
+                   (loop after))
+                 next))))))
+
+(def (scan-javascript-var-console-log-pattern rule path source pattern)
+  (let ([spec (javascript-var-console-log-pattern-spec pattern)])
+    (and spec
+         (let ([len (string-length source)]
+               [mvar (alist-ref/default spec 'metavariable "")])
+           (let loop ([line-start 0] [acc '()])
+             (if (> line-start len)
+                 (nonempty-findings (reverse acc))
+                 (let* ([line-end (line-end-after source line-start)]
+                        [info (javascript-source-var-line-info
+                                source
+                                line-start
+                                line-end)]
+                        [name (and info (alist-ref/default info 'name ""))]
+                        [next-line (and info
+                                        (javascript-next-significant-line-start
+                                          source
+                                          line-start))]
+                        [next-end (and next-line
+                                       (line-end-after source next-line))]
+                        [match?
+                         (and name
+                              next-line
+                              next-end
+                              (javascript-source-console-log-name?
+                                source
+                                next-line
+                                next-end
+                                name))]
+                        [end (and match?
+                                  (+ (skip-horizontal-backward
+                                       source
+                                       (- next-end 1))
+                                     1))]
+                        [name-start
+                         (and info
+                              (alist-ref/default info 'name-start #f))]
+                        [finding
+                         (and end
+                              name-start
+                              (finding-for-range-with-bindings
+                                rule
+                                path
+                                source
+                                (alist-ref/default info 'first 0)
+                                end
+                                (list
+                                  (cons mvar
+                                        (metavariable-binding-for-range
+                                          mvar
+                                          source
+                                          name-start
+                                          (+ name-start
+                                             (string-length name)))))))]
+                        [next (if (< line-end len)
+                                  (+ line-end 1)
+                                  (+ len 1))])
+                   (loop next
+                         (if finding (cons finding acc) acc)))))))))
+
+(def (javascript-metavar-stmt-or-expr-pattern-spec pattern)
+  (let ([lines (javascript-pattern-significant-lines pattern)])
+    (and (= (length lines) 3)
+         (let ([stmt-match (re-search
+                             (re "^\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*;?$")
+                             (car lines)
+                             0)]
+               [dots? (string=? (cadr lines) "...")]
+               [call-match (re-search
+                             (re "^([A-Za-z_$][A-Za-z0-9_$]*)[ \\t]*\\([ \\t]*\\$([A-Za-z_][A-Za-z0-9_]*)[ \\t]*\\)[ \\t]*;?$")
+                             (caddr lines)
+                             0)])
+           (and stmt-match
+                dots?
+                call-match
+                (string=? (re-match-group stmt-match 1)
+                          (re-match-group call-match 2))
+                (list (cons 'metavariable (re-match-group stmt-match 1))
+                      (cons 'callee (re-match-group call-match 1))))))))
+
+(def (javascript-simple-identifier-statement-info source line-start line-end)
+  (let* ([first (line-first-nonspace source line-start line-end)]
+         [line (js-cp-strip-trailing-semicolon
+                 (js-cp-trim-comment (substring source first line-end)))]
+         [match (re-search
+                  (re "^([A-Za-z_$][A-Za-z0-9_$]*)$")
+                  line
+                  0)])
+    (and match
+         (let* ([name (re-match-group match 1)]
+                [name-start (string-find-substring-from source name first)])
+           (and name-start
+                (list (cons 'name name)
+                      (cons 'first first)
+                      (cons 'name-start name-start)))))))
+
+(def (javascript-callee-name-call-line? source line-start line-end callee name)
+  (let* ([first (line-first-nonspace source line-start line-end)]
+         [line (js-cp-trim-comment (substring source first line-end))])
+    (if (re-search
+          (re (string-append
+                "^"
+                (regex-escape-string callee)
+                "[ \\t]*\\([ \\t]*"
+                (regex-escape-string name)
+                "[ \\t]*\\)[ \\t]*;?[ \\t]*$"))
+          line
+          0)
+        #t
+        #f)))
+
+(def (javascript-find-callee-name-call-line
+       source
+       line-start
+       callee
+       name)
+  (let ([len (string-length source)])
+    (let loop ([current (javascript-next-significant-line-start
+                         source
+                         line-start)])
+      (and current
+           (<= current len)
+           (let* ([line-end (line-end-after source current)])
+             (if (javascript-callee-name-call-line?
+                   source
+                   current
+                   line-end
+                   callee
+                   name)
+                 (cons current line-end)
+                 (let ([next (javascript-next-significant-line-start
+                              source
+                              current)])
+                   (and next
+                        (not (= next current))
+                        (loop next)))))))))
+
+(def (scan-javascript-metavar-stmt-or-expr-pattern rule path source pattern)
+  (let ([spec (javascript-metavar-stmt-or-expr-pattern-spec pattern)])
+    (and spec
+         (let ([len (string-length source)]
+               [mvar (alist-ref/default spec 'metavariable "")]
+               [callee (alist-ref/default spec 'callee "")])
+           (let loop ([line-start 0] [acc '()])