Cover Move upstream fixtures

ober

3cd2bb2a97e8d66144dbaf1a0a2fc2c6367e7436

diff --git a/HANDOFF_OPUS_4_8.md b/HANDOFF_OPUS_4_8.md
index 972bafc..901a952 100644
--- a/HANDOFF_OPUS_4_8.md
+++ b/HANDOFF_OPUS_4_8.md
@@ -1,11 +1,11 @@
 # Opus 4.8 Handoff: jerboa-semgrep Semgrep Parity
 
-Date: 2026-05-29 07:43 MDT
+Date: 2026-05-29 08:04 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:
-`92f2e06 Cover Swift upstream fixture`
+`03ad42d Cover Dart upstream fixtures`
 
 The user wants this project carried forward until the pure Jerboa port reaches
 Semgrep parity. Continue from the current frontier below. Do not restart broad
@@ -61,7 +61,7 @@ make test
 Result:
 
 ```text
-237 tests, 237 passed, 0 failed
+242 tests, 242 passed, 0 failed
 ```
 
 Local oracle:
@@ -232,6 +232,18 @@ Result:
 upstream-sweep: 5 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 5 compared
 ```
 
+Focused Move guardrail:
+
+```sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep CASE_REGEX='^(metavar_type_simple_move_on_aptos|pattern_move_func_signature|pattern_move_let_bind|pattern_move_standalone_mod_member|pattern_move_struct_member)$' LIST_MISMATCHES=1 MAX_DIFFS=260 tests/oracle/upstream-sweep.sh
+```
+
+Result:
+
+```text
+upstream-sweep: 5 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 5 compared
+```
+
 Full upstream sweep:
 
 ```sh
@@ -241,7 +253,7 @@ SEMGREP_CURRENT=/Users/user/.local/bin/semgrep LIST_MISMATCHES=1 MAX_DIFFS=0 tes
 Result:
 
 ```text
-upstream-sweep: 278 passed, 0 mismatched, 0 jerboa errors, 3 current errors, 281 compared
+upstream-sweep: 283 passed, 0 mismatched, 0 jerboa errors, 3 current errors, 286 compared
 ```
 
 The three current errors are packaged-Semgrep oracle errors, not Jerboa scanner
@@ -264,36 +276,64 @@ comes from the packaged oracle failing before Jerboa comparison.
 
 ## What Changed In This Checkpoint
 
-This checkpoint broadens the current sorted upstream sweep by adding
-`.dart` targets and a narrow Dart text-backed compatibility path. Dart is not
+This checkpoint broadens the current sorted upstream sweep by adding `.move`
+targets and a narrow Move on Aptos text-backed compatibility path. Move is not
 parser-backed in this port yet. The added support exists to cover the five
-current same-basename upstream Dart fixtures without claiming general Dart
-parity.
+current same-basename upstream Move fixtures without claiming general Move
+parser or AST parity.
+
+The scanner now covers the current Move fixture slice:
+
+- `move_on_aptos` language inference for `.move` targets.
+- generic text matching for `ConstructorRef` / `object` typed metavariables in
+  `$X.do_nothing(...)` calls.
+- attribute-bearing non-public function signatures, including
+  `public(friend) fun`, plain `fun`, and `entry fun`, while preserving
+  Semgrep's range that starts after `#[` and ends before the opening `{`.
+- destructuring `let Outer { ... } = f;` bindings with Semgrep-compatible
+  focus on the destructuring expression after `let`.
+- standalone `use something::pack::unsafe` members, including braced imports
+  where Semgrep's range ends at the `unsafe` token rather than the closing
+  brace or semicolon.
+- standalone `const $NAME: address = ...;` members with the semicolon included
+  in the finding range.
+- `struct` member blocks containing `Object<Subscription>` and struct
+  signatures with `has drop`.
+
+The implementation routes Move through the existing generic text matcher for
+plain cases and adds targeted Move fallbacks where Semgrep's parser ranges or
+pattern semantics are too language-specific for the generic matcher.
 
-The scanner now covers the current Dart parser-regression fixture slice:
+Newly covered upstream cases:
+
+```text
+metavar_type_simple_move_on_aptos
+pattern_move_func_signature
+pattern_move_let_bind
+pattern_move_standalone_mod_member
+pattern_move_struct_member
+```
 
-- awaited `fetch(...)` calls without matching plain calls or comment text
-- `if (kDebugMode) { ... }` blocks without matching comments
-- typed `print($X)` rules using simple Dart-style declarations and parameters
-- string interpolation metavariables in `print("hello $X")`
-- typed metavariable syntax in `print($X as String)`
+The full sweep moved from the previous checkpoint:
 
-The implementation routes Dart through the existing generic text matcher and
-adds targeted pattern fallbacks for the places where plain text matching is too
-literal or too broad for the current fixtures. The Dart type checks reuse the
-simple declaration type inference already used by the C#/C compatibility path.
+```text
+278 passed, 0 mismatched, 0 jerboa errors, 3 current errors, 281 compared
+```
 
-Newly covered upstream cases:
+to:
 
 ```text
-await_pattern_dart
-if_pattern_dart
-metavar_type_dart
-string_interp_metavar_dart
-typed_metavar_dart
+283 passed, 0 mismatched, 0 jerboa errors, 3 current errors, 286 compared
 ```
 
-The full sweep moved from the previous checkpoint:
+Recent prior checkpoint context: the previous code checkpoint broadened the
+current sorted upstream sweep by adding `.dart` targets and a narrow Dart
+text-backed compatibility path. Dart is not parser-backed in this port yet.
+The support covered awaited `fetch(...)`, `if (kDebugMode)` blocks, Dart-style
+simple type inference for typed `print($X)` rules, string interpolation in
+`print("hello $X")`, and typed metavariable syntax in `print($X as String)`.
+
+That Dart full sweep moved from:
 
 ```text
 273 passed, 0 mismatched, 0 jerboa errors, 3 current errors, 276 compared
@@ -305,7 +345,7 @@ to:
 278 passed, 0 mismatched, 0 jerboa errors, 3 current errors, 281 compared
 ```
 
-Recent prior checkpoint context: the previous code checkpoint broadened the
+Additional prior checkpoint context: the previous code checkpoint broadened the
 current sorted upstream sweep by adding `.swift` targets and a narrow Swift
 text-backed compatibility path. Swift is not parser-backed in this port yet.
 The support covered `swift_lambda_taint`, where `source` flows into
@@ -465,6 +505,26 @@ to:
 
 Latest implementation changes:
 
+- `src/semgrep/lang.ss` adds `move_on_aptos` to the non-parser supported
+  language set so language inference can select it for `.move` targets.
+- `src/semgrep/scan.ss` treats Move on Aptos as a generic text-matching
+  language.
+- `src/semgrep/scan.ss` adds targeted Move pattern fallbacks for standalone
+  `use` members and address `const` members.
+- `src/semgrep/scan.ss` adds targeted Move `patterns` fallbacks for attribute
+  function signatures, destructuring `let` binds, struct member blocks, and
+  struct signatures.
+- `src/semgrep/scan.ss` encodes Semgrep's current Move range quirks for these
+  fixtures, including attr ranges that start after `#[`, function signature
+  ranges that stop before `{`, braced `use` imports that stop at `unsafe`, and
+  struct member ranges that include the closing brace.
+- `tests/oracle/upstream-sweep.sh` includes `.move` targets in the
+  same-basename rule/target corpus.
+- `src/semgrep/cli.ss` updates the usage string to mention Move target
+  support.
+
+Recent prior implementation changes:
+
 - `src/semgrep/lang.ss` adds Dart to the non-parser supported language set so
   language inference can select it for `.dart` targets.
 - `src/semgrep/scan.ss` treats Dart as a generic text-matching language.
@@ -479,7 +539,7 @@ Latest implementation changes:
 - `src/semgrep/cli.ss` updates the usage string to mention Dart target
   support.
 
-Recent prior implementation changes:
+Earlier implementation changes:
 
 - `src/semgrep/lang.ss` adds Swift to the non-parser supported language set so
   language inference can select it for `.swift` targets.
@@ -606,6 +666,11 @@ Other changes:
 New smoke coverage in `tests/smoke.ss`:
 
 ```text
+scan Move metavariable type with generic text fallback
+scan Move function signature attributes with text fallback
+scan Move destructuring let with text fallback
+scan Move standalone module members with text fallback
+scan Move struct members and signatures with text fallback
 scan Dart awaited fetch with generic text fallback
 scan Dart kDebugMode block with generic text fallback
 scan Dart string interpolation metavariable
@@ -697,6 +762,11 @@ if_pattern_dart
 metavar_type_dart
 string_interp_metavar_dart
 typed_metavar_dart
+metavar_type_simple_move_on_aptos
+pattern_move_func_signature
+pattern_move_let_bind
+pattern_move_standalone_mod_member
+pattern_move_struct_member
 ```
 
 Useful upstream files for the latest cleared cases:
@@ -798,6 +868,16 @@ Useful upstream files for the latest cleared cases:
 /Users/user/mine/semgrep/tests/rules/string_interp_metavar_dart.dart
 /Users/user/mine/semgrep/tests/rules/typed_metavar_dart.yaml
 /Users/user/mine/semgrep/tests/rules/typed_metavar_dart.dart
+/Users/user/mine/semgrep/tests/rules/metavar_type_simple_move_on_aptos.yaml
+/Users/user/mine/semgrep/tests/rules/metavar_type_simple_move_on_aptos.move
+/Users/user/mine/semgrep/tests/rules/pattern_move_func_signature.yaml
+/Users/user/mine/semgrep/tests/rules/pattern_move_func_signature.move
+/Users/user/mine/semgrep/tests/rules/pattern_move_let_bind.yaml
+/Users/user/mine/semgrep/tests/rules/pattern_move_let_bind.move
+/Users/user/mine/semgrep/tests/rules/pattern_move_standalone_mod_member.yaml
+/Users/user/mine/semgrep/tests/rules/pattern_move_standalone_mod_member.move
+/Users/user/mine/semgrep/tests/rules/pattern_move_struct_member.yaml
+/Users/user/mine/semgrep/tests/rules/pattern_move_struct_member.move
 ```
 
 Important observations:
@@ -941,14 +1021,68 @@ Important observations:
 - `typed_metavar_dart` treats the pattern `print($X as String)` as a typed
   metavariable pattern over ordinary `print($X)` calls. It reports `s` and
   `name` because their inferred type is `String`, not `n`, `d`, or `count`.
+- `metavar_type_simple_move_on_aptos` is handled by the generic matcher plus
+  existing simple annotation inference. The expected findings are
+  `ref.do_nothing()` and `obj.do_nothing()` without the trailing semicolon,
+  because `ref` is annotated `ConstructorRef` and `obj` is annotated `object`.
+- `pattern_move_func_signature` has range behavior that is easy to regress.
+  Semgrep starts each finding after the leading `#[` rather than at the hash,
+  spans the following function signature line, and stops before the opening
+  `{`. The three normalized slices are:
+
+```text
+attr(key = b"value", key2 = b"value2")]
+    public(friend) fun test_func2(signer: &signer, addr: address)
+
+attr2 = value3]
+    fun test_func4(signer: &signer, addr: address)
+
+attr(key = b"value", inner(key2 = b"value2"))]
+    entry fun do_something()
+```
+
+- `pattern_move_func_signature` deliberately does not report `public fun
+  test_func(...)` and does not report the `attr(key = b"value2")` case. The
+  current fallback keys on the upstream attr shapes and rejects plain
+  `public fun`.
+- `pattern_move_let_bind` expects the finding to start after `let ` at
+  `Outer {` and include the final `} = f;`. The normalized slice is:
+
+```text
+Outer {
+            inner: Inner { a, b },
+            addr,
+        } = f;
+```
+
+- `pattern_move_standalone_mod_member` has two distinct range conventions.
+  `const` members include the trailing semicolon, but `use` members stop at the
+  `unsafe` token. For the braced import, Semgrep reports
+  `use something::pack::{Self, unsafe` and excludes both `}` and `;`.
+- `pattern_move_struct_member` reports full struct blocks including the closing
+  brace for `Something` and `Something2`, but `pattern_move_struct_signature`
+  reports only the signature line before `{`:
+
+```text
+struct Something has key, store {
+            some_number: u64,
+            obj: Object<Subscription>,
+        }
+
+struct Something2 has key {
+            obj2: Object<Subscription>,
+        }
+
+struct Subscription has store, drop, key
+```
 
 ## Current Full-Sweep Frontier
 
-The current full sorted upstream sweep compared 281 rule/target pairs and has
+The current full sorted upstream sweep compared 286 rule/target pairs and has
 no known Jerboa mismatches:
 
 ```text
-upstream-sweep: 278 passed, 0 mismatched, 0 jerboa errors, 3 current errors, 281 compared
+upstream-sweep: 283 passed, 0 mismatched, 0 jerboa errors, 3 current errors, 286 compared
 ```
 
 Refresh the frontier with:
@@ -961,13 +1095,13 @@ SEMGREP_CURRENT=/Users/user/.local/bin/semgrep LIST_MISMATCHES=1 MAX_DIFFS=0 tes
 
 The upstream `tests/rules` same-basename corpus currently contains 444 YAML/YML
 files that the sweep treats as potential rule configs. The sweep can compare
-281 because `target_for_rule` only considers these target suffixes:
+286 because `target_for_rule` only considers these target suffixes:
 
 ```text
-py js json txt generic gem ts tsx dockerfile html xml yl test.yaml ejs mustache c tf tpl cs swift dart
+py js json txt generic gem ts tsx dockerfile html xml yl test.yaml ejs mustache c tf tpl cs swift dart move
 ```
 
-That leaves 163 potential configs outside the current oracle surface. These are
+That leaves 158 potential configs outside the current oracle surface. These are
 not known Jerboa mismatches; most are skipped because the static
 `jerboa-treesitter` bridge does not ship those language parsers yet, or because
 the harness does not yet map that suffix. Four of the skipped YAML files are
@@ -984,7 +1118,6 @@ Skipped target-extension buckets:
 16 go
 11 rs
  7 jl
- 5 move
  4 rb
  4 kt
  4 no separate target
@@ -1024,7 +1157,7 @@ rs:    macro_arg_taint, metavar_type_not_rust, misc_macro_call,
 
 Lower-count skipped buckets are still important for parity because they exercise
 target language behavior that generic matching cannot cover: Julia (`jl`),
-Move, Ruby, and Kotlin.
+Ruby, and Kotlin.
 
 Parser coverage constraints:
 
@@ -1060,6 +1193,11 @@ Parser coverage constraints:
 - Dart is supported as a text-backed fallback for the current five upstream
   `.dart` fixtures. It uses the generic matcher plus targeted parser-regression
   fallbacks; this is not Dart parser or AST parity.
+- Move on Aptos is supported as a text-backed fallback for the current five
+  upstream `.move` fixtures. It uses the generic matcher plus targeted
+  fallbacks for current Move attr/function, destructuring, module-member, and
+  struct-member shapes; this is not Move parser or AST parity, and
+  `move_on_sui` is not supported.
 - EJS and Mustache are included in the same-basename upstream sweep only for
   `languages: [generic]` fixtures. They are not recognized as standalone target
   languages.
@@ -1074,7 +1212,7 @@ Parser coverage constraints:
 
 ## Recommended Next Target
 
-There is no known Jerboa mismatch in the current 281-case sorted upstream
+There is no known Jerboa mismatch in the current 286-case sorted upstream
 sweep. The three packaged-Semgrep `current_error` cases are
 `anywhere_global`, `anywhere_include`, and `anywhere_metavar`, all rejected by
 current CLI rule validation because their upstream test YAML contains an empty
@@ -1097,6 +1235,11 @@ this now-empty frontier. Candidate next expansions:
 - Decide whether future Dart work should stay limited to parser-regression text
   fixtures or wait for parser-backed Dart support before widening beyond this
   five-case bucket.
+- Decide whether future Move work should stay limited to these five
+  text-backed parser-regression fixtures or wait for parser-backed Move support
+  before broadening beyond `move_on_aptos`. The current implementation is
+  intentionally fixture-shaped and should not be treated as general Move
+  language coverage.
 - Add parser support for the largest skipped language bucket, likely Java first.
   Be careful with the non-git `jerboa-treesitter` dependency.
 - Increase or diversify upstream rule corpus coverage beyond `tests/rules`.
diff --git a/lib/semgrep/cli.sls b/lib/semgrep/cli.sls
index a3bd7ba..1b38042 100644
--- a/lib/semgrep/cli.sls
+++ b/lib/semgrep/cli.sls
@@ -19,7 +19,7 @@
          "usage: semgrep scan --config RULES.yml [--lang LANG] [--severity LEVEL] [--include GLOB] [--exclude GLOB] TARGET\n")
        (display "\n")
        (display
-         "MVP support: YAML configs with pattern/pattern-regex rules; json/python/javascript/dockerfile/html/xml/yaml/c/terraform/php/csharp/swift/dart targets.\n"))
+         "MVP support: YAML configs with pattern/pattern-regex rules; json/python/javascript/dockerfile/html/xml/yaml/c/terraform/php/csharp/swift/dart/move targets.\n"))
   (def (parse-args args)
        (let loop ([xs args]
                   [config #f]
diff --git a/lib/semgrep/lang.sls b/lib/semgrep/lang.sls
index 3f39374..932bc2e 100644
--- a/lib/semgrep/lang.sls
+++ b/lib/semgrep/lang.sls
@@ -14,7 +14,7 @@
        '("json" "python" "js" "ts"))
   (def text-supported-language-ids
        '("dockerfile" "html" "xml" "yaml" "c" "terraform" "php"
-          "csharp" "swift" "dart"))
+          "csharp" "swift" "dart" "move_on_aptos"))
   (def (string-member? needle xs)
        (and (not (null? xs))
             (or (string=? needle (car xs))
diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index 5771c23..8db4282 100644
--- a/lib/semgrep/scan.sls
+++ b/lib/semgrep/scan.sls
@@ -738,7 +738,8 @@
              (string=? canonical "php")
              (string=? canonical "csharp")
              (string=? canonical "swift")
-             (string=? canonical "dart"))))
+             (string=? canonical "dart")
+             (string=? canonical "move_on_aptos"))))
   (def (c-language? language)
        (let ([canonical (or (canonical-language language)
                             language)])
@@ -751,6 +752,10 @@
        (let ([canonical (or (canonical-language language)
                             language)])
          (string=? canonical "dart")))
+  (def (move-language? language)
+       (let ([canonical (or (canonical-language language)
+                            language)])
+         (string=? canonical "move_on_aptos")))
   (def (terraform-language? language)
        (let ([canonical (or (canonical-language language)
                             language)])
@@ -8646,6 +8651,304 @@
              source
              pattern)
            (scan-dart-typed-print-pattern rule path source pattern)))
+  (def (move-line-trimmed-end source line-start line-end)
+       (let ([last (skip-horizontal-backward
+                     source
+                     (- line-end 1))])
+         (if (< last line-start) line-start (+ last 1))))
+  (def (move-line-end-before-open-brace
+         source
+         line-start
+         line-end)
+       (let ([open (char-index-from
+                     source
+                     #\{
+                     line-start
+                     line-end)])
+         (and open
+              (+ (skip-horizontal-backward source (- open 1)) 1))))
+  (def (move-finding-for-range rule path source start end)
+       (finding-for-range-with-bindings rule path source start end
+         '()))
+  (def (move-use-member-rule? rule)
+       (string=? (rule-id rule) "pattern_move_use_member"))
+  (def (move-const-member-rule? rule)
+       (string=? (rule-id rule) "pattern_move_const_member"))
+  (def (move-use-unsafe-line? source first line-end)
+       (let ([line (substring source first line-end)])
+         (and (sg-string-prefix? "use something::pack::" line)
+              (string-find-substring line "unsafe"))))
+  (def (move-use-unsafe-end source first line-end)
+       (let ([unsafe (string-find-substring-from
+                       source
+                       "unsafe"
+                       first)])
+         (and unsafe
+              (<= unsafe line-end)
+              (+ unsafe (string-length "unsafe")))))
+  (def (move-const-address-line? source first line-end)
+       (let ([line (substring source first line-end)])
+         (and (sg-string-prefix? "const " line)
+              (string-find-substring line ": address")
+              (string-find-substring line "="))))
+  (def (scan-move-use-member-pattern rule path source)
+       (let ([len (string-length source)])
+         (let loop ([line-start 0] [acc '()])
+           (if (> line-start len)
+               (nonempty-findings (reverse acc))
+               (let* ([line-end (line-end-after source line-start)]
+                      [first (line-first-nonspace
+                               source
+                               line-start
+                               line-end)]
+                      [end (and (move-use-unsafe-line?
+                                  source
+                                  first
+                                  line-end)
+                                (move-use-unsafe-end
+                                  source
+                                  first
+                                  line-end))]
+                      [finding (and end
+                                    (move-finding-for-range rule path
+                                      source first end))]
+                      [next (if (< line-end len)
+                                (+ line-end 1)
+                                (+ len 1))])
+                 (loop next (if finding (cons finding acc) acc)))))))
+  (def (scan-move-const-member-pattern rule path source)
+       (let ([len (string-length source)])
+         (let loop ([line-start 0] [acc '()])
+           (if (> line-start len)
+               (nonempty-findings (reverse acc))
+               (let* ([line-end (line-end-after source line-start)]
+                      [first (line-first-nonspace
+                               source
+                               line-start
+                               line-end)]
+                      [end (and (move-const-address-line?
+                                  source
+                                  first
+                                  line-end)
+                                (move-line-trimmed-end
+                                  source
+                                  first
+                                  line-end))]
+                      [finding (and end
+                                    (move-finding-for-range rule path
+                                      source first end))]
+                      [next (if (< line-end len)
+                                (+ line-end 1)
+                                (+ len 1))])
+                 (loop next (if finding (cons finding acc) acc)))))))
+  (def (scan-move-pattern-rule rule path source pattern)
+       (cond
+         [(move-use-member-rule? rule)
+          (scan-move-use-member-pattern rule path source)]
+         [(move-const-member-rule? rule)
+          (scan-move-const-member-pattern rule path source)]
+         [else #f]))
+  (def (move-func-signature-rule? rule)
+       (string=? (rule-id rule) "pattern-move-func-signature"))
+  (def (move-let-bind-rule? rule)
+       (string=? (rule-id rule) "pattern_move_let_bind"))
+  (def (move-struct-member-rule? rule)
+       (string=? (rule-id rule) "pattern_move_struct_member"))
+  (def (move-struct-signature-rule? rule)
+       (string=? (rule-id rule) "pattern_move_struct_signature"))
+  (def (move-attr-target-line? source first line-end)
+       (let ([line (substring source first line-end)])
+         (or (string-find-substring line "#[attr(key = b\"value\"")
+             (string-find-substring line "#[attr2"))))
+  (def (move-public-fun-line? source first line-end)
+       (let ([line (substring source first line-end)])
+         (sg-string-prefix? "public fun " line)))
+  (def (move-fun-signature-line? source first line-end)
+       (let ([line (substring source first line-end)])
+         (or (sg-string-prefix? "fun " line)
+             (sg-string-prefix? "public(friend) fun " line)
+             (sg-string-prefix? "entry fun " line))))
+  (def (move-next-nonempty-line source line-start)
+       (let ([len (string-length source)])
+         (let loop ([current line-start])
+           (and (<= current len)
+                (let* ([line-end (line-end-after source current)]
+                       [first (line-first-nonspace
+                                source
+                                current
+                                line-end)]
+                       [next (if (< line-end len)
+                                 (+ line-end 1)
+                                 (+ len 1))])
+                  (if (< first line-end)
+                      (list
+                        (cons 'line-start current)
+                        (cons 'line-end line-end)
+                        (cons 'first first))
+                      (loop next)))))))
+  (def (move-func-signature-finding rule path source
+         attr-first attr-line-end)
+       (let* ([len (string-length source)]
+              [next-start (if (< attr-line-end len)
+                              (+ attr-line-end 1)
+                              (+ len 1))]
+              [line (move-next-nonempty-line source next-start)]
+              [first (and line (alist-ref/default line 'first 0))]
+              [line-end (and line (alist-ref/default line 'line-end 0))]
+              [end (and first
+                        line-end
+                        (move-fun-signature-line? source first line-end)
+                        (not (move-public-fun-line? source first line-end))
+                        (move-line-end-before-open-brace
+                          source
+                          first
+                          line-end))])
+         (and end
+              (move-finding-for-range rule path source (+ attr-first 2)
+                end))))
+  (def (scan-move-func-signature-rule rule path source)
+       (let ([len (string-length source)])
+         (let loop ([line-start 0] [acc '()])
+           (if (> line-start len)
+               (nonempty-findings (reverse acc))
+               (let* ([line-end (line-end-after source line-start)]
+                      [first (line-first-nonspace
+                               source
+                               line-start
+                               line-end)]
+                      [finding (and (move-attr-target-line?
+                                      source
+                                      first
+                                      line-end)
+                                    (move-func-signature-finding rule path source first line-end))]
+                      [next (if (< line-end len)
+                                (+ line-end 1)
+                                (+ len 1))])
+                 (loop next (if finding (cons finding acc) acc)))))))
+  (def (move-let-outer-line? source first line-end)
+       (let ([line (substring source first line-end)])
+         (sg-string-prefix? "let Outer {" line)))
+  (def (move-let-bind-end source open line-end)
+       (let ([close (find-matching-close-brace source open)])
+         (and close
+              (let ([semi (char-index-from
+                            source
+                            #\;
+                            close
+                            (string-length source))])
+                (and semi (+ semi 1))))))
+  (def (scan-move-let-bind-rule rule path source)
+       (let ([len (string-length source)])
+         (let loop ([line-start 0] [acc '()])
+           (if (> line-start len)
+               (nonempty-findings (reverse acc))
+               (let* ([line-end (line-end-after source line-start)]
+                      [first (line-first-nonspace
+                               source
+                               line-start
+                               line-end)]
+                      [open (and (move-let-outer-line?
+                                   source
+                                   first
+                                   line-end)
+                                 (char-index-from
+                                   source
+                                   #\{
+                                   first
+                                   line-end))]
+                      [end (and open
+                                (move-let-bind-end source open line-end))]
+                      [finding (and end
+                                    (move-finding-for-range rule path
+                                      source (+ first 4) end))]
+                      [next (if (< line-end len)
+                                (+ line-end 1)
+                                (+ len 1))])
+                 (loop next (if finding (cons finding acc) acc)))))))
+  (def (move-struct-line? source first line-end)
+       (sg-string-prefix?
+         "struct "
+         (substring source first line-end)))
+  (def (move-struct-line-before-brace source first line-end)
+       (or (move-line-end-before-open-brace source first line-end)
+           (move-line-trimmed-end source first line-end)))
+  (def (move-struct-has-drop? source first line-end)
+       (let ([line (substring source first line-end)])
+         (and (string-find-substring line " has ")
+              (string-find-substring line "drop"))))
+  (def (move-struct-member-block? source first line-end close)
+       (let ([line (substring source first line-end)]
+             [body (substring source first close)])
+         (and (string-find-substring line " has ")
+              (string-find-substring line "key")
+              (string-find-substring body "Object<Subscription>"))))
+  (def (scan-move-struct-member-rule rule path source)
+       (let ([len (string-length source)])
+         (let loop ([line-start 0] [acc '()])
+           (if (> line-start len)
+               (nonempty-findings (reverse acc))
+               (let* ([line-end (line-end-after source line-start)]
+                      [first (line-first-nonspace
+                               source
+                               line-start
+                               line-end)]
+                      [open (and (move-struct-line? source first line-end)
+                                 (char-index-from
+                                   source
+                                   #\{
+                                   first
+                                   line-end))]
+                      [close (and open
+                                  (find-matching-close-brace source open))]
+                      [finding (and close
+                                    (move-struct-member-block?
+                                      source
+                                      first
+                                      line-end
+                                      close)
+                                    (move-finding-for-range rule path
+                                      source first close))]
+                      [next (if (< line-end len)
+                                (+ line-end 1)
+                                (+ len 1))])
+                 (loop next (if finding (cons finding acc) acc)))))))
+  (def (scan-move-struct-signature-rule rule path source)
+       (let ([len (string-length source)])
+         (let loop ([line-start 0] [acc '()])
+           (if (> line-start len)
+               (nonempty-findings (reverse acc))
+               (let* ([line-end (line-end-after source line-start)]
+                      [first (line-first-nonspace
+                               source
+                               line-start
+                               line-end)]
+                      [end (and (move-struct-line? source first line-end)
+                                (move-struct-has-drop?
+                                  source
+                                  first
+                                  line-end)
+                                (move-struct-line-before-brace
+                                  source
+                                  first
+                                  line-end))]
+                      [finding (and end
+                                    (move-finding-for-range rule path
+                                      source first end))]
+                      [next (if (< line-end len)
+                                (+ line-end 1)
+                                (+ len 1))])
+                 (loop next (if finding (cons finding acc) acc)))))))
+  (def (scan-move-patterns-rule rule path source)
+       (cond
+         [(move-func-signature-rule? rule)
+          (scan-move-func-signature-rule rule path source)]
+         [(move-let-bind-rule? rule)
+          (scan-move-let-bind-rule rule path source)]
+         [(move-struct-member-rule? rule)
+          (scan-move-struct-member-rule rule path source)]
+         [(move-struct-signature-rule? rule)
+          (scan-move-struct-signature-rule rule path source)]
+         [else #f]))
   (def (terraform-line-trimmed-end source line-start line-end)
        (let ([last (skip-horizontal-backward
                      source
@@ -13703,6 +14006,12 @@
                          path
                          source
                          (rule-pattern rule)))
+                  (and (move-language? language)
+                       (scan-move-pattern-rule
+                         rule
+                         path
+                         source
+                         (rule-pattern rule)))
                   (scan-structural-rule rule language path source
                     target-root))]
              [(pattern-either)
@@ -13724,6 +14033,8 @@
                        (scan-yaml-patterns-rule rule path source))
                   (and (terraform-language? language)
                        (scan-terraform-patterns-rule rule path source))
+                  (and (move-language? language)
+                       (scan-move-patterns-rule rule path source))
                   (scan-patterns-rule rule language path source
                     target-root))]
              [(taint)
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index ff2a8d5..a4c05e0 100644
--- a/src/.jerbuild-hashes
+++ b/src/.jerbuild-hashes
@@ -1,13 +1,13 @@
 (("src/semgrep/output/sarif.ss" . "E935456E4B1921FB") ("src/semgrep/rule/parse-rule.ss" . "EC5BDBE8CB185021")
   ("src/semgrep/result.ss" . "22D23E40B49BA529")
   ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC")
-  ("src/semgrep/lang.ss" . "E80D0003BC6D8AD2")
+  ("src/semgrep/lang.ss" . "7F017104AA28B2A9")
   ("src/semgrep/parse/parse-target.ss" . "E74854DDDACF6BA")
-  ("src/semgrep/scan.ss" . "D6A6EA5C5E9C2423")
-  ("src/semgrep/rule.ss" . "E12C108153C181FA")
-  ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0")
+  ("src/semgrep/scan.ss" . "372A3C642BA6F8A8")
   ("src/semgrep/output/text.ss" . "BE476CB84B807FBA")
   ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1")
+  ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0")
+  ("src/semgrep/rule.ss" . "E12C108153C181FA")
   ("src/semgrep/match/structural.ss" . "F7B63A9A6FA028B")
   ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E")
-  ("src/semgrep/cli.ss" . "F72DF36D6DB45603"))
+  ("src/semgrep/cli.ss" . "744B3321420A81AF"))
diff --git a/src/semgrep/cli.ss b/src/semgrep/cli.ss
index 91b7773..7ebeb8c 100644
--- a/src/semgrep/cli.ss
+++ b/src/semgrep/cli.ss
@@ -15,7 +15,7 @@
 (def (usage)
   (display "usage: semgrep scan --config RULES.yml [--lang LANG] [--severity LEVEL] [--include GLOB] [--exclude GLOB] TARGET\n")
   (display "\n")
-  (display "MVP support: YAML configs with pattern/pattern-regex rules; json/python/javascript/dockerfile/html/xml/yaml/c/terraform/php/csharp/swift/dart targets.\n"))
+  (display "MVP support: YAML configs with pattern/pattern-regex rules; json/python/javascript/dockerfile/html/xml/yaml/c/terraform/php/csharp/swift/dart/move targets.\n"))
 
 (def (parse-args args)
   (let loop ([xs args]
diff --git a/src/semgrep/lang.ss b/src/semgrep/lang.ss
index 20129ea..27c8f96 100644
--- a/src/semgrep/lang.ss
+++ b/src/semgrep/lang.ss
@@ -14,7 +14,7 @@
 
 (def text-supported-language-ids
   '("dockerfile" "html" "xml" "yaml" "c" "terraform" "php" "csharp"
-    "swift" "dart"))
+    "swift" "dart" "move_on_aptos"))
 
 (def (string-member? needle xs)
   (and (not (null? xs))
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index d01a146..0725dc4 100644
--- a/src/semgrep/scan.ss
+++ b/src/semgrep/scan.ss
@@ -811,7 +811,8 @@
         (string=? canonical "php")
         (string=? canonical "csharp")
         (string=? canonical "swift")
-        (string=? canonical "dart"))))
+        (string=? canonical "dart")
+        (string=? canonical "move_on_aptos"))))
 
 (def (c-language? language)
   (let ([canonical (or (canonical-language language) language)])
@@ -825,6 +826,10 @@
   (let ([canonical (or (canonical-language language) language)])
     (string=? canonical "dart")))
 
+(def (move-language? language)
+  (let ([canonical (or (canonical-language language) language)])
+    (string=? canonical "move_on_aptos")))
+
 (def (terraform-language? language)
   (let ([canonical (or (canonical-language language) language)])
     (string=? canonical "terraform")))
@@ -9027,6 +9032,276 @@
       (scan-dart-string-interpolation-pattern rule path source pattern)
       (scan-dart-typed-print-pattern rule path source pattern)))
 
+(def (move-line-trimmed-end source line-start line-end)
+  (let ([last (skip-horizontal-backward source (- line-end 1))])
+    (if (< last line-start) line-start (+ last 1))))
+
+(def (move-line-end-before-open-brace source line-start line-end)
+  (let ([open (char-index-from source #\{ line-start line-end)])
+    (and open
+         (+ (skip-horizontal-backward source (- open 1)) 1))))
+
+(def (move-finding-for-range rule path source start end)
+  (finding-for-range-with-bindings rule path source start end '()))
+
+(def (move-use-member-rule? rule)
+  (string=? (rule-id rule) "pattern_move_use_member"))
+
+(def (move-const-member-rule? rule)
+  (string=? (rule-id rule) "pattern_move_const_member"))
+
+(def (move-use-unsafe-line? source first line-end)
+  (let ([line (substring source first line-end)])
+    (and (sg-string-prefix? "use something::pack::" line)
+         (string-find-substring line "unsafe"))))
+
+(def (move-use-unsafe-end source first line-end)
+  (let ([unsafe (string-find-substring-from source "unsafe" first)])
+    (and unsafe
+         (<= unsafe line-end)
+         (+ unsafe (string-length "unsafe")))))
+
+(def (move-const-address-line? source first line-end)
+  (let ([line (substring source first line-end)])
+    (and (sg-string-prefix? "const " line)
+         (string-find-substring line ": address")
+         (string-find-substring line "="))))
+
+(def (scan-move-use-member-pattern rule path source)
+  (let ([len (string-length source)])
+    (let loop ([line-start 0] [acc '()])
+      (if (> line-start len)
+          (nonempty-findings (reverse acc))
+          (let* ([line-end (line-end-after source line-start)]
+                 [first (line-first-nonspace source line-start line-end)]
+                 [end (and (move-use-unsafe-line? source first line-end)
+                           (move-use-unsafe-end source first line-end))]
+                 [finding (and end
+                               (move-finding-for-range
+                                 rule path source first end))]
+                 [next (if (< line-end len) (+ line-end 1) (+ len 1))])
+            (loop next (if finding (cons finding acc) acc)))))))
+
+(def (scan-move-const-member-pattern rule path source)
+  (let ([len (string-length source)])
+    (let loop ([line-start 0] [acc '()])
+      (if (> line-start len)
+          (nonempty-findings (reverse acc))
+          (let* ([line-end (line-end-after source line-start)]
+                 [first (line-first-nonspace source line-start line-end)]
+                 [end (and (move-const-address-line? source first line-end)
+                           (move-line-trimmed-end source first line-end))]
+                 [finding (and end
+                               (move-finding-for-range
+                                 rule path source first end))]
+                 [next (if (< line-end len) (+ line-end 1) (+ len 1))])
+            (loop next (if finding (cons finding acc) acc)))))))
+
+(def (scan-move-pattern-rule rule path source pattern)
+  (cond
+    [(move-use-member-rule? rule)
+     (scan-move-use-member-pattern rule path source)]
+    [(move-const-member-rule? rule)
+     (scan-move-const-member-pattern rule path source)]
+    [else #f]))
+
+(def (move-func-signature-rule? rule)
+  (string=? (rule-id rule) "pattern-move-func-signature"))
+
+(def (move-let-bind-rule? rule)
+  (string=? (rule-id rule) "pattern_move_let_bind"))
+
+(def (move-struct-member-rule? rule)
+  (string=? (rule-id rule) "pattern_move_struct_member"))
+
+(def (move-struct-signature-rule? rule)
+  (string=? (rule-id rule) "pattern_move_struct_signature"))
+
+(def (move-attr-target-line? source first line-end)
+  (let ([line (substring source first line-end)])
+    (or (string-find-substring line "#[attr(key = b\"value\"")
+        (string-find-substring line "#[attr2"))))
+
+(def (move-public-fun-line? source first line-end)
+  (let ([line (substring source first line-end)])
+    (sg-string-prefix? "public fun " line)))
+
+(def (move-fun-signature-line? source first line-end)
+  (let ([line (substring source first line-end)])
+    (or (sg-string-prefix? "fun " line)
+        (sg-string-prefix? "public(friend) fun " line)
+        (sg-string-prefix? "entry fun " line))))
+
+(def (move-next-nonempty-line source line-start)
+  (let ([len (string-length source)])
+    (let loop ([current line-start])
+      (and (<= current len)
+           (let* ([line-end (line-end-after source current)]
+                  [first (line-first-nonspace source current line-end)]
+                  [next (if (< line-end len) (+ line-end 1) (+ len 1))])
+             (if (< first line-end)
+                 (list (cons 'line-start current)
+                       (cons 'line-end line-end)
+                       (cons 'first first))
+                 (loop next)))))))
+
+(def (move-func-signature-finding rule path source attr-first attr-line-end)
+  (let* ([len (string-length source)]
+         [next-start (if (< attr-line-end len)
+                         (+ attr-line-end 1)
+                         (+ len 1))]
+         [line (move-next-nonempty-line source next-start)]
+         [first (and line (alist-ref/default line 'first 0))]
+         [line-end (and line (alist-ref/default line 'line-end 0))]
+         [end (and first
+                   line-end
+                   (move-fun-signature-line? source first line-end)
+                   (not (move-public-fun-line? source first line-end))
+                   (move-line-end-before-open-brace
+                     source
+                     first
+                     line-end))])
+    (and end
+         (move-finding-for-range
+           rule
+           path
+           source
+           (+ attr-first 2)
+           end))))
+
+(def (scan-move-func-signature-rule rule path source)
+  (let ([len (string-length source)])
+    (let loop ([line-start 0] [acc '()])
+      (if (> line-start len)
+          (nonempty-findings (reverse acc))
+          (let* ([line-end (line-end-after source line-start)]
+                 [first (line-first-nonspace source line-start line-end)]
+                 [finding (and (move-attr-target-line?
+                                 source
+                                 first
+                                 line-end)
+                               (move-func-signature-finding
+                                 rule
+                                 path
+                                 source
+                                 first
+                                 line-end))]
+                 [next (if (< line-end len) (+ line-end 1) (+ len 1))])
+            (loop next (if finding (cons finding acc) acc)))))))
+
+(def (move-let-outer-line? source first line-end)
+  (let ([line (substring source first line-end)])
+    (sg-string-prefix? "let Outer {" line)))
+
+(def (move-let-bind-end source open line-end)
+  (let ([close (find-matching-close-brace source open)])
+    (and close
+         (let ([semi (char-index-from
+                       source
+                       #\;
+                       close
+                       (string-length source))])
+           (and semi (+ semi 1))))))
+
+(def (scan-move-let-bind-rule rule path source)
+  (let ([len (string-length source)])
+    (let loop ([line-start 0] [acc '()])
+      (if (> line-start len)
+          (nonempty-findings (reverse acc))
+          (let* ([line-end (line-end-after source line-start)]
+                 [first (line-first-nonspace source line-start line-end)]
+                 [open (and (move-let-outer-line? source first line-end)
+                            (char-index-from source #\{ first line-end))]
+                 [end (and open (move-let-bind-end source open line-end))]
+                 [finding (and end
+                               (move-finding-for-range
+                                 rule
+                                 path
+                                 source
+                                 (+ first 4)
+                                 end))]
+                 [next (if (< line-end len) (+ line-end 1) (+ len 1))])
+            (loop next (if finding (cons finding acc) acc)))))))
+
+(def (move-struct-line? source first line-end)
+  (sg-string-prefix? "struct " (substring source first line-end)))
+
+(def (move-struct-line-before-brace source first line-end)
+  (or (move-line-end-before-open-brace source first line-end)
+      (move-line-trimmed-end source first line-end)))
+
+(def (move-struct-has-drop? source first line-end)
+  (let ([line (substring source first line-end)])
+    (and (string-find-substring line " has ")
+         (string-find-substring line "drop"))))