Skip to content

Commit d89034e

Browse files
feat(rules): AffineScript hand-port pitfalls — HANDLE-as-fn-name + OCaml float ops (#332)
## Summary Adds 5 patterns under language `"affine"`/`"affinescript"` to catch parse-blocking hand-port leaks that surface when `.res` / `.ml` / `.re` code is migrated to AffineScript. Both classes were discovered manually during the sustainabot ReScript→AffineScript migration (gitbot-fleet#148) and resolved via 5 parser PRs + a hand-port-rewrite PR — but the underlying *signal* ("this code won't parse") only emerged after `affinescript check`. Capturing them as Hypatia rules surfaces the signal at PR-review time, before the iteration loop. ## The two pitfalls 1. **`handle` as a fn name.** AffineScript's `handle` is a reserved keyword (HANDLE token), used by the effect-handler expression form `handle body { handler_arms }`. So `pub fn handle(...)` is a parse error. Severity: `:high`. Recommended rename: `dispatch`, `handle_request`, `handle_event`. 2. **OCaml-style float operators.** AffineScript uses unified `+ - * /` for both Int and Float. The OCaml form `+. -. *. /.` is never accepted. Four separate patterns (one per operator) — each requires an operand-character preceding the operator-and-dot, so `.0`-suffixed numeric literals (e.g. `1.0 + x`) don't false-positive. Severity: `:high` (parse blocker). ## Integration - New `@affine_hand_port_patterns` module attribute in `lib/rules/code_safety.ex` (immediately after `@rescript_patterns`). - Dispatch registered for both `"affine"` and `"affinescript"` language keys (mirrors the `javascript`/`typescript` alias precedent at lines 384-385). - File-extension fallback in `lib/rules/rules.ex` after the existing JS/TS web-security fallback: any `.affine` file gets the hand-port scan even when the caller passes `language = nil` or an unrelated upstream default. ## Test plan - [x] Regex spot-check via `elixir -e 'Regex.scan(...)'`: `pub fn handle(` matches; `let x = a /. 1.0` matches; `let x = a / 1.0` does NOT match (true negative). All three confirmed. - [x] `elixirc` of both modified files succeeds (only pre-existing module-resolution warnings unrelated to this patch). - [ ] `mix compile` blocked by a Phoenix-dep / Elixir-1.15 mismatch in the sandbox env; not in scope for this PR. ## Cross-refs - Refs hyperpolymath/gitbot-fleet#148 - Refs hyperpolymath/affinescript#370, #371, #372, #373, #376 - Refs hyperpolymath/gitbot-fleet#206 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7d69b8a commit d89034e

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

lib/rules/code_safety.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,32 @@ defmodule Hypatia.Rules.CodeSafety do
6868
description: "JSON decode without validation"}
6969
]
7070

71+
# AffineScript hand-port pitfalls — patterns that hand-ports from
72+
# ReScript/OCaml routinely leak into `.affine` files but that the
73+
# AffineScript grammar rejects. Both surfaced during the sustainabot
74+
# ReScript->AffineScript migration (gitbot-fleet#148, 2026-05-26).
75+
# Cross-references: affinescript repo CLAUDE.md "Hypatia and gitbot-fleet
76+
# standing rules" + the agent-memory entries
77+
# `feedback_affinescript_handle_keyword_gotcha.md` /
78+
# `feedback_affinescript_no_ocaml_float_ops.md`.
79+
@affine_hand_port_patterns [
80+
%{id: :handle_as_fn_name, severity: :high,
81+
pattern: ~r/(?:^|\n)\s*(?:pub\s+)?(?:total\s+)?fn\s+handle\s*[\(\<]/, cwe: "CWE-1109",
82+
description: "Function named `handle` collides with AffineScript HANDLE keyword token (used by `handle body { handlers }` effect-handler expression form). Rename to `dispatch`, `handle_request`, `handle_event`, etc."},
83+
%{id: :ocaml_style_float_div, severity: :high,
84+
pattern: ~r/[a-zA-Z0-9_)\]]\s*\/\.\s/, cwe: "CWE-704",
85+
description: "OCaml-style float division `/.` is not accepted by AffineScript — use unified `/` for both Int and Float"},
86+
%{id: :ocaml_style_float_mul, severity: :high,
87+
pattern: ~r/[a-zA-Z0-9_)\]]\s*\*\.\s/, cwe: "CWE-704",
88+
description: "OCaml-style float multiplication `*.` is not accepted by AffineScript — use unified `*` for both Int and Float"},
89+
%{id: :ocaml_style_float_add, severity: :high,
90+
pattern: ~r/[a-zA-Z0-9_)\]]\s*\+\.\s/, cwe: "CWE-704",
91+
description: "OCaml-style float addition `+.` is not accepted by AffineScript — use unified `+` for both Int and Float"},
92+
%{id: :ocaml_style_float_sub, severity: :high,
93+
pattern: ~r/[a-zA-Z0-9_)\]]\s*-\.\s/, cwe: "CWE-704",
94+
description: "OCaml-style float subtraction `-.` is not accepted by AffineScript — use unified `-` for both Int and Float"}
95+
]
96+
7197
@idris2_banned [
7298
%{id: :believe_me, severity: :critical,
7399
pattern: ~r/believe_me/, cwe: "CWE-704",
@@ -359,6 +385,8 @@ defmodule Hypatia.Rules.CodeSafety do
359385

360386
def patterns_for_language("rust"), do: @rust_patterns
361387
def patterns_for_language("rescript"), do: @rescript_patterns
388+
def patterns_for_language("affine"), do: @affine_hand_port_patterns
389+
def patterns_for_language("affinescript"), do: @affine_hand_port_patterns
362390
def patterns_for_language("idris2"), do: @idris2_banned
363391
def patterns_for_language("haskell"), do: @haskell_banned
364392
def patterns_for_language("ocaml"), do: @ocaml_banned

lib/rules/rules.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ defmodule Hypatia.Rules do
101101
findings
102102
end
103103

104+
# AffineScript hand-port patterns for `.affine` files not already covered
105+
# by language patterns. Mirrors the JS/TS extension fallback above so
106+
# callers that pass `language=nil` or an upstream-default language still
107+
# get the hand-port-pitfall scan on every `.affine` file (gitbot-fleet#148).
108+
findings =
109+
if language not in ["affine", "affinescript"] and
110+
String.ends_with?(file_path, ".affine") do
111+
findings ++ CodeSafety.scan_content(content, "affine")
112+
else
113+
findings
114+
end
115+
104116
# Container code patterns for Containerfiles and shell scripts
105117
findings =
106118
if String.ends_with?(file_path, "Containerfile") or

0 commit comments

Comments
 (0)