From 674ff307a8ea42f996b43dfead667741d43765ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:07:19 +0000 Subject: [PATCH 1/9] Initial plan From 2e84755397d19ae13c02797c77a7e2e03176cd44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:22:27 +0000 Subject: [PATCH 2/9] Add #:suggested-fixes option to refactoring rules and support warning-only results Co-authored-by: jackfirth <8175575+jackfirth@users.noreply.github.com> --- base.rkt | 8 +- main.rkt | 101 ++++++++++++--------- private/refactoring-result.rkt | 160 +++++++++++++++++++++++---------- 3 files changed, 178 insertions(+), 91 deletions(-) diff --git a/base.rkt b/base.rkt index 1d14d28..7db49cd 100644 --- a/base.rkt +++ b/base.rkt @@ -15,6 +15,7 @@ [refactoring-rule? (-> any/c boolean?)] [refactoring-rule-description (-> refactoring-rule? immutable-string?)] [refactoring-rule-analyzers (-> refactoring-rule? (set/c expansion-analyzer?))] + [refactoring-rule-suggested-fixes (-> refactoring-rule? (or/c 'none 'one))] [refactoring-suite? (-> any/c boolean?)] [refactoring-suite (->* () @@ -108,7 +109,7 @@ [(_ new-stx) (syntax-property #'new-stx 'focus-replacement-on #true)])) -(define-object-type refactoring-rule (transformer description uses-universal-tagged-syntax? analyzers) +(define-object-type refactoring-rule (transformer description uses-universal-tagged-syntax? analyzers suggested-fixes) #:omit-root-binding #:constructor-name constructor:refactoring-rule) @@ -140,12 +141,14 @@ #:description description (~optional (~seq #:uses-universal-tagged-syntax? uses-universal-tagged-syntax?)) (~optional (~seq #:analyzers analyzers)) + (~optional (~seq #:suggested-fixes suggested-fixes)) parse-option:syntax-parse-option ... pattern pattern-directive:syntax-parse-pattern-directive ... replacement) #:declare description (expr/c #'string?) #:declare analyzers (expr/c #'(sequence/c expansion-analyzer?)) + #:declare suggested-fixes (expr/c #'(or/c 'none 'one)) #:attr partial-match-log-statement (and (not (empty? (attribute pattern-directive))) @@ -162,6 +165,7 @@ #:description (string->immutable-string description.c) #:uses-universal-tagged-syntax? (~? uses-universal-tagged-syntax? #false) #:analyzers (for/set ([analyzer (~? analyzers.c '())]) analyzer) + #:suggested-fixes (~? suggested-fixes.c 'one) #:transformer (λ (stx) (syntax-parse stx @@ -176,6 +180,7 @@ (define-definition-context-refactoring-rule id:id #:description (~var description (expr/c #'string?)) (~optional (~seq #:analyzers (~var analyzers (expr/c #'(sequence/c expansion-analyzer?))))) + (~optional (~seq #:suggested-fixes (~var suggested-fixes (expr/c #'(or/c 'none 'one))))) parse-option:syntax-parse-option ... splicing-pattern pattern-directive:syntax-parse-pattern-directive ... @@ -229,6 +234,7 @@ (define-refactoring-rule id #:description description (~? (~@ #:analyzers analyzers)) + (~? (~@ #:suggested-fixes suggested-fixes)) (~var expression expression-matching-id) expression.refactored))) diff --git a/main.rkt b/main.rkt index a9db4eb..41e8a9e 100644 --- a/main.rkt +++ b/main.rkt @@ -382,48 +382,65 @@ syntax (string-indent (exn-message e) #:amount 3)) absent)]) - (guarded-block - (guard-match (present replacement) - (parameterize ([current-namespace (source-code-analysis-namespace analysis)]) - (refactoring-rule-refactor rule syntax (source-code-analysis-code analysis))) - #:else absent) - (guard (syntax-replacement-introduces-incorrect-bindings? replacement) #:else - (define bad-ids (syntax-replacement-introduced-incorrect-identifiers replacement)) - (define orig-stx (syntax-replacement-original-syntax replacement)) - (define intro (syntax-replacement-introduction-scope replacement)) - (log-resyntax-warning - (string-append - "~a: suggestion discarded because it introduces identifiers with incorrect bindings\n" - " incorrect identifiers: ~a\n" - " bindings in original context: ~a\n" - " bindings in syntax replacement: ~a\n" - " replaced syntax: ~a") - (object-name rule) - bad-ids - (for/list ([id (in-list bad-ids)]) - (identifier-binding (datum->syntax orig-stx (syntax->datum id)))) - (for/list ([id (in-list bad-ids)]) - (identifier-binding (intro id 'remove))) - orig-stx) - absent) - (guard (syntax-replacement-preserves-comments? replacement comments) #:else - (log-resyntax-warning - (string-append "~a: suggestion discarded because it does not preserve all comments\n" - " dropped comment locations: ~v\n" - " original syntax:\n" - " ~v\n" - " replacement syntax:\n" - " ~v") - (object-name rule) - (syntax-replacement-dropped-comment-locations replacement comments) - (syntax-replacement-original-syntax replacement) - (syntax-replacement-new-syntax replacement)) - absent) - (present - (refactoring-result - #:rule-name (object-name rule) - #:message (refactoring-rule-description rule) - #:syntax-replacement replacement))))) + ;; Check if this is a warning-only rule + (cond + [(eq? (refactoring-rule-suggested-fixes rule) 'none) + ;; For warning-only rules, try to match the pattern + (define match-result + (parameterize ([current-namespace (source-code-analysis-namespace analysis)]) + (refactoring-rule-refactor rule syntax (source-code-analysis-code analysis)))) + ;; If pattern matched, create a warning result + (option-map match-result + (λ (_) + (warning-result + #:rule-name (object-name rule) + #:message (refactoring-rule-description rule) + #:source (source-code-analysis-code analysis) + #:original-syntax syntax)))] + [else + ;; For rules with fixes, validate and create a regular refactoring result + (guarded-block + (guard-match (present replacement) + (parameterize ([current-namespace (source-code-analysis-namespace analysis)]) + (refactoring-rule-refactor rule syntax (source-code-analysis-code analysis))) + #:else absent) + (guard (syntax-replacement-introduces-incorrect-bindings? replacement) #:else + (define bad-ids (syntax-replacement-introduced-incorrect-identifiers replacement)) + (define orig-stx (syntax-replacement-original-syntax replacement)) + (define intro (syntax-replacement-introduction-scope replacement)) + (log-resyntax-warning + (string-append + "~a: suggestion discarded because it introduces identifiers with incorrect bindings\n" + " incorrect identifiers: ~a\n" + " bindings in original context: ~a\n" + " bindings in syntax replacement: ~a\n" + " replaced syntax: ~a") + (object-name rule) + bad-ids + (for/list ([id (in-list bad-ids)]) + (identifier-binding (datum->syntax orig-stx (syntax->datum id)))) + (for/list ([id (in-list bad-ids)]) + (identifier-binding (intro id 'remove))) + orig-stx) + absent) + (guard (syntax-replacement-preserves-comments? replacement comments) #:else + (log-resyntax-warning + (string-append "~a: suggestion discarded because it does not preserve all comments\n" + " dropped comment locations: ~v\n" + " original syntax:\n" + " ~v\n" + " replacement syntax:\n" + " ~v") + (object-name rule) + (syntax-replacement-dropped-comment-locations replacement comments) + (syntax-replacement-original-syntax replacement) + (syntax-replacement-new-syntax replacement)) + absent) + (present + (refactoring-result + #:rule-name (object-name rule) + #:message (refactoring-rule-description rule) + #:syntax-replacement replacement)))]))) (falsey->option (for*/first ([rule (in-list rules)] diff --git a/private/refactoring-result.rkt b/private/refactoring-result.rkt index 9b5d5ed..e019323 100644 --- a/private/refactoring-result.rkt +++ b/private/refactoring-result.rkt @@ -8,22 +8,29 @@ (contract-out [refactoring-result? (-> any/c boolean?)] [refactoring-result + (->* (#:rule-name interned-symbol? + #:message string?) + (#:syntax-replacement (or/c syntax-replacement? #false)) + refactoring-result?)] + [warning-result (-> #:rule-name interned-symbol? #:message string? - #:syntax-replacement syntax-replacement? + #:source source? + #:original-syntax syntax? refactoring-result?)] [refactoring-result-rule-name (-> refactoring-result? interned-symbol?)] [refactoring-result-message (-> refactoring-result? immutable-string?)] [refactoring-result-source (-> refactoring-result? source?)] + [refactoring-result-has-fix? (-> refactoring-result? boolean?)] [refactoring-result-modified-range (-> refactoring-result? range?)] [refactoring-result-modified-line-range (-> refactoring-result? range?)] - [refactoring-result-syntax-replacement (-> refactoring-result? syntax-replacement?)] - [refactoring-result-string-replacement (-> refactoring-result? string-replacement?)] - [refactoring-result-line-replacement (-> refactoring-result? line-replacement?)] + [refactoring-result-syntax-replacement (-> refactoring-result? (or/c syntax-replacement? #false))] + [refactoring-result-string-replacement (-> refactoring-result? (or/c string-replacement? #false))] + [refactoring-result-line-replacement (-> refactoring-result? (or/c line-replacement? #false))] [refactoring-result-original-line (-> refactoring-result? exact-positive-integer?)] [refactoring-result-original-column (-> refactoring-result? exact-nonnegative-integer?)] [refactoring-result-original-code (-> refactoring-result? code-snippet?)] - [refactoring-result-new-code (-> refactoring-result? code-snippet?)] + [refactoring-result-new-code (-> refactoring-result? (or/c code-snippet? #false))] [refactoring-result-set? (-> any/c boolean?)] [refactoring-result-set (-> #:base-source source? #:results (sequence/c refactoring-result?) refactoring-result-set?)] @@ -62,45 +69,82 @@ (define-record-type refactoring-result - (rule-name message syntax-replacement string-replacement line-replacement) + (rule-name message source original-syntax syntax-replacement string-replacement line-replacement) #:omit-root-binding) -(define (refactoring-result #:rule-name rule-name #:message message #:syntax-replacement replacement) - (define str-replacement (syntax-replacement-render replacement)) - (define full-orig-code (source->string (syntax-replacement-source replacement))) +(define (refactoring-result #:rule-name rule-name + #:message message + #:syntax-replacement [replacement #false]) + (if replacement + (let ([str-replacement (syntax-replacement-render replacement)] + [full-orig-code (source->string (syntax-replacement-source replacement))]) + (constructor:refactoring-result + #:rule-name rule-name + #:message (string->immutable-string message) + #:source (syntax-replacement-source replacement) + #:original-syntax (syntax-replacement-original-syntax replacement) + #:syntax-replacement replacement + #:string-replacement str-replacement + #:line-replacement (string-replacement->line-replacement str-replacement full-orig-code))) + (raise-arguments-error 'refactoring-result + "must provide either #:syntax-replacement" + "rule-name" rule-name + "message" message))) + + +(define (warning-result #:rule-name rule-name #:message message #:source source #:original-syntax original-syntax) (constructor:refactoring-result #:rule-name rule-name #:message (string->immutable-string message) - #:syntax-replacement replacement - #:string-replacement str-replacement - #:line-replacement (string-replacement->line-replacement str-replacement full-orig-code))) + #:source source + #:original-syntax original-syntax + #:syntax-replacement #false + #:string-replacement #false + #:line-replacement #false)) -(define (refactoring-result-source result) - (syntax-replacement-source (refactoring-result-syntax-replacement result))) +(define (refactoring-result-has-fix? result) + (and (refactoring-result-syntax-replacement result) #true)) (define (refactoring-result-modified-range result) - (define replacement (refactoring-result-string-replacement result)) - (closed-open-range (add1 (string-replacement-start replacement)) - (add1 (string-replacement-original-end replacement)) - #:comparator natural<=>)) + (if (refactoring-result-has-fix? result) + (let ([replacement (refactoring-result-string-replacement result)]) + (closed-open-range (add1 (string-replacement-start replacement)) + (add1 (string-replacement-original-end replacement)) + #:comparator natural<=>)) + (let* ([orig-stx (refactoring-result-original-syntax result)] + [pos (syntax-position orig-stx)] + [span (syntax-span orig-stx)]) + (if (and pos span) + (closed-open-range pos (+ pos span) #:comparator natural<=>) + (closed-open-range 1 2 #:comparator natural<=>))))) (define (refactoring-result-modified-line-range result) - (define replacement (refactoring-result-line-replacement result)) - (closed-open-range (line-replacement-start-line replacement) - (line-replacement-original-end-line replacement) - #:comparator natural<=>)) + (if (refactoring-result-has-fix? result) + (let ([replacement (refactoring-result-line-replacement result)]) + (closed-open-range (line-replacement-start-line replacement) + (line-replacement-original-end-line replacement) + #:comparator natural<=>)) + (let* ([orig-stx (refactoring-result-original-syntax result)] + [line (syntax-line orig-stx)]) + (if line + (closed-range line line #:comparator natural<=>) + (closed-range 1 1 #:comparator natural<=>))))) (define (refactoring-result-original-line result) - (line-replacement-start-line (refactoring-result-line-replacement result))) + (if (refactoring-result-has-fix? result) + (line-replacement-start-line (refactoring-result-line-replacement result)) + (or (syntax-line (refactoring-result-original-syntax result)) 1))) (define (refactoring-result-original-column result) - (code-snippet-start-column (refactoring-result-original-code result))) + (if (refactoring-result-has-fix? result) + (code-snippet-start-column (refactoring-result-original-code result)) + (or (syntax-column (refactoring-result-original-syntax result)) 0))) (define-record-type refactoring-result-set (base-source results) @@ -116,6 +160,7 @@ (define (refactoring-result-set-updated-source result-set) (define replacement (transduce (refactoring-result-set-results result-set) + (filtering refactoring-result-has-fix?) (mapping refactoring-result-string-replacement) #:into union-into-string-replacement)) (define base (refactoring-result-set-base-source result-set)) @@ -141,6 +186,7 @@ (define rule-names (transduce (in-hash-values result-map) (append-mapping refactoring-result-set-results) + (filtering refactoring-result-has-fix?) ; Only include results with fixes (mapping refactoring-result-rule-name) (deduplicating) #:into into-list)) @@ -154,13 +200,15 @@ (define rule-results (for*/list ([results (in-hash-values result-map)] [result (in-list (refactoring-result-set-results results))] - #:when (equal? (refactoring-result-rule-name result) rule)) + #:when (and (equal? (refactoring-result-rule-name result) rule) + (refactoring-result-has-fix? result))) result)) (define replacements (for/hash ([(source results) (in-hash result-map)]) (define source-replacements (transduce (refactoring-result-set-results results) - (filtering (λ (r) (equal? (refactoring-result-rule-name r) rule))) + (filtering (λ (r) (and (equal? (refactoring-result-rule-name r) rule) + (refactoring-result-has-fix? r)))) (mapping refactoring-result-string-replacement) #:into (into-sorted-set string-replacement<=>))) (values source source-replacements))) @@ -186,28 +234,44 @@ (define (refactoring-result-original-code result) - (define replacement (refactoring-result-string-replacement result)) - (define full-orig-code - (source->string (syntax-replacement-source (refactoring-result-syntax-replacement result)))) - (define lmap (string-linemap full-orig-code)) - (define start (string-replacement-start replacement)) - (define end (string-replacement-original-end replacement)) - (define start-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))) - (define raw-text (string->immutable-string (substring full-orig-code start end))) - (code-snippet raw-text start-column (linemap-position-to-line lmap (add1 start)))) + (if (refactoring-result-has-fix? result) + (let* ([replacement (refactoring-result-string-replacement result)] + [full-orig-code + (source->string (syntax-replacement-source (refactoring-result-syntax-replacement result)))] + [lmap (string-linemap full-orig-code)] + [start (string-replacement-start replacement)] + [end (string-replacement-original-end replacement)] + [start-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))] + [raw-text (string->immutable-string (substring full-orig-code start end))]) + (code-snippet raw-text start-column (linemap-position-to-line lmap (add1 start)))) + (let* ([orig-stx (refactoring-result-original-syntax result)] + [source (refactoring-result-source result)] + [full-orig-code (source->string source)] + [pos (syntax-position orig-stx)] + [span (syntax-span orig-stx)] + [line (or (syntax-line orig-stx) 1)] + [col (or (syntax-column orig-stx) 0)]) + (if (and pos span) + (let* ([start (sub1 pos)] + [end (+ start span)] + [raw-text (string->immutable-string (substring full-orig-code start end))]) + (code-snippet raw-text col line)) + (code-snippet "" col line))))) (define (refactoring-result-new-code result) - (define replacement (refactoring-result-string-replacement result)) - (define full-orig-code - (source->string (syntax-replacement-source (refactoring-result-syntax-replacement result)))) - (define lmap (string-linemap full-orig-code)) - (define start (string-replacement-start replacement)) - (define original-line (linemap-position-to-line lmap (add1 start))) - (define original-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))) - (define refactored-source-code (string-apply-replacement full-orig-code replacement)) - (define new-code-string - (substring refactored-source-code - (string-replacement-start replacement) - (string-replacement-new-end replacement))) - (code-snippet new-code-string original-column original-line)) + (if (refactoring-result-has-fix? result) + (let* ([replacement (refactoring-result-string-replacement result)] + [full-orig-code + (source->string (syntax-replacement-source (refactoring-result-syntax-replacement result)))] + [lmap (string-linemap full-orig-code)] + [start (string-replacement-start replacement)] + [original-line (linemap-position-to-line lmap (add1 start))] + [original-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))] + [refactored-source-code (string-apply-replacement full-orig-code replacement)] + [new-code-string + (substring refactored-source-code + (string-replacement-start replacement) + (string-replacement-new-end replacement))]) + (code-snippet new-code-string original-column original-line)) + #false)) From 0f321f722ba01bff34d97d1994154f6f921da5a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:26:26 +0000 Subject: [PATCH 3/9] Update CLI and add tests for warning-only rules Co-authored-by: jackfirth <8175575+jackfirth@users.noreply.github.com> --- cli.rkt | 14 +++++---- private/github.rkt | 55 +++++++++++++++++++++++------------ private/warning-rule-test.rkt | 46 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 private/warning-rule-test.rkt diff --git a/cli.rkt b/cli.rkt index f1917b1..7d880c8 100644 --- a/cli.rkt +++ b/cli.rkt @@ -300,18 +300,20 @@ For help on these, use 'analyze --help' or 'fix --help'." (match (resyntax-analyze-options-output-format options) [(== plain-text) (for ([result (in-list results)]) - (define path - (file-source-path - (syntax-replacement-source (refactoring-result-syntax-replacement result)))) + (define source (refactoring-result-source result)) + (define path (file-source-path source)) (define line (refactoring-result-original-line result)) (define column (refactoring-result-original-column result)) (printf "resyntax: ~a:~a:~a [~a]\n" path line column (refactoring-result-rule-name result)) (printf "\n\n~a\n" (string-indent (refactoring-result-message result) #:amount 2)) (define old-code (refactoring-result-original-code result)) (define new-code (refactoring-result-new-code result)) - (printf "\n\n~a\n\n\n~a\n\n\n" - (string-indent (~a old-code) #:amount 2) - (string-indent (~a new-code) #:amount 2)))] + (if new-code + (printf "\n\n~a\n\n\n~a\n\n\n" + (string-indent (~a old-code) #:amount 2) + (string-indent (~a new-code) #:amount 2)) + (printf "\n\n~a\n\n\n" + (string-indent (~a old-code) #:amount 2))))] [(== github-pull-request-review) (define req (refactoring-results->github-review results #:file-count (hash-count sources))) (write-json (github-review-request-jsexpr req))])) diff --git a/private/github.rkt b/private/github.rkt index dcfe4f1..f93ec00 100644 --- a/private/github.rkt +++ b/private/github.rkt @@ -93,11 +93,14 @@ (define (refactoring-result->github-review-comment result) - (define path - (file-source-path (syntax-replacement-source (refactoring-result-syntax-replacement result)))) - (define replacement (refactoring-result-line-replacement result)) - (define body - (format #< EOS - (refactoring-result-rule-name result) - (refactoring-result-message result) - (line-replacement-new-text replacement) - (string-indent (pretty-format replacement) #:amount 2) - (string-indent (pretty-format (refactoring-result-syntax-replacement result)) - #:amount 2))) - (github-review-comment - #:path (first (git-path path)) - #:body body - #:start-line (line-replacement-start-line replacement) - #:end-line (line-replacement-original-end-line replacement) - #:start-side "RIGHT" - #:end-side "RIGHT")) + (refactoring-result-rule-name result) + (refactoring-result-message result) + (line-replacement-new-text replacement) + (string-indent (pretty-format replacement) #:amount 2) + (string-indent (pretty-format (refactoring-result-syntax-replacement result)) + #:amount 2))) + (github-review-comment + #:path (first (git-path path)) + #:body body + #:start-line (line-replacement-start-line replacement) + #:end-line (line-replacement-original-end-line replacement) + #:start-side "RIGHT" + #:end-side "RIGHT")] + [else + ;; For warning-only results, generate a comment without a suggestion + (define source (refactoring-result-source result)) + (define path (file-source-path source)) + (define line (refactoring-result-original-line result)) + (define body + (format "**`~a`:** ~a" + (refactoring-result-rule-name result) + (refactoring-result-message result))) + (github-review-comment + #:path (first (git-path path)) + #:body body + #:start-line line + #:end-line line + #:start-side "RIGHT" + #:end-side "RIGHT")])) (define branch-ref (getenv "GITHUB_REF")) diff --git a/private/warning-rule-test.rkt b/private/warning-rule-test.rkt new file mode 100644 index 0000000..1be98c4 --- /dev/null +++ b/private/warning-rule-test.rkt @@ -0,0 +1,46 @@ +#lang racket/base + +(require racket/list + resyntax/base + resyntax + resyntax/private/refactoring-result + resyntax/private/source + rackunit) + +;; Define a warning-only rule that matches any (equal? x y) +(define-refactoring-rule test-warning-rule + #:description "This is a test warning rule for equal?" + #:suggested-fixes 'none + #:literals (equal?) + (equal? x y) + (void)) + +;; Test that the rule works +(define test-suite (refactoring-suite #:rules (list test-warning-rule))) + +(define test-source (string-source "#lang racket/base\n(define a 5)\n(equal? a a)\n")) + +(define result-set (resyntax-analyze test-source #:suite test-suite)) + +(define results (refactoring-result-set-results result-set)) + +(test-case "warning-only rule produces a result" + (check-equal? (length results) 1 "Should have one result")) + +(test-case "warning-only result has no fix" + (define result (first results)) + (check-false (refactoring-result-has-fix? result) "Should not have a fix") + (check-false (refactoring-result-syntax-replacement result) "Should have no syntax replacement") + (check-false (refactoring-result-new-code result) "Should have no new code")) + +( test-case "warning-only result has message and location" + (define result (first results)) + (check-equal? (refactoring-result-message result) "This is a test warning rule for equal?") + (check-equal? (refactoring-result-rule-name result) 'test-warning-rule) + (check-true (positive? (refactoring-result-original-line result)))) + +(test-case "warning-only result doesn't modify source" + (define updated (refactoring-result-set-updated-source result-set)) + (define updated-contents (modified-source-contents updated)) + (check-equal? updated-contents (source->string test-source) + "Source should not be modified")) From 9212eeb5d52091fe04a4530284fd471265a92e05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:41:21 +0000 Subject: [PATCH 4/9] Add comment-only-test statement for testing warning-only rules Co-authored-by: jackfirth <8175575+jackfirth@users.noreply.github.com> --- test-warning-suite.rkt | 16 ++++++++++++ test.rkt | 26 ++++++++++++++++--- test/comment-only-test-demo.rkt | 15 +++++++++++ test/private/rackunit.rkt | 44 ++++++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 test-warning-suite.rkt create mode 100644 test/comment-only-test-demo.rkt diff --git a/test-warning-suite.rkt b/test-warning-suite.rkt new file mode 100644 index 0000000..e3c836c --- /dev/null +++ b/test-warning-suite.rkt @@ -0,0 +1,16 @@ +#lang racket/base + +(require resyntax/base) + +(provide test-warning-suite) + +;; Define a warning-only rule that matches any (equal? x y) +(define-refactoring-rule test-warning-rule + #:description "Test warning rule for equal?" + #:suggested-fixes 'none + #:literals (equal?) + (equal? x y) + (void)) + +(define test-warning-suite + (refactoring-suite #:rules (list test-warning-rule))) diff --git a/test.rkt b/test.rkt index 2703e32..d5af787 100644 --- a/test.rkt +++ b/test.rkt @@ -8,7 +8,8 @@ header test no-change-test - analysis-test) + analysis-test + comment-only-test) (require (for-syntax racket/base @@ -228,6 +229,25 @@ 'expected-value)))])))) +(define-syntax comment-only-test + (statement-transformer + (λ (stx) + (syntax-parse stx + #:track-literals + #:datum-literals (option @within @inspect @assertMatch) + [(#:statement _ name:str + code:literal-code + (~seq (#:option #:within context-block:literal-code) ... + (#:option #:inspect target-block:literal-code) + (#:option #:assertMatch rule-name:id))) + #`(test-case 'name + #,(syntax/loc this-syntax + (check-suite-comment-only code + (list context-block ...) + target-block + 'rule-name)))])))) + + ;; Helper function to check if any require: statements are present (begin-for-syntax (define (has-require-statements? body-stxs) @@ -400,7 +420,7 @@ (define (add-uts-properties stx) (syntax-traverse stx - #:datum-literals (require header test no-change-test analysis-test) + #:datum-literals (require header test no-change-test analysis-test comment-only-test) [:id (define as-string (symbol->string (syntax-e this-syntax))) @@ -425,7 +445,7 @@ (add-uts-properties (attribute code)))) (datum->syntax #false new-datum this-syntax this-syntax)] - [((~and tag #:statement) (~and test-id (~or test no-change-test analysis-test)) arg ...) + [((~and tag #:statement) (~and test-id (~or test no-change-test analysis-test comment-only-test)) arg ...) (define separators (append (list "" ": " "\n") (make-list (length (attribute arg)) ""))) (define tag-with-prop (syntax-property (attribute tag) 'uts-separators separators)) (define new-datum diff --git a/test/comment-only-test-demo.rkt b/test/comment-only-test-demo.rkt new file mode 100644 index 0000000..4188060 --- /dev/null +++ b/test/comment-only-test-demo.rkt @@ -0,0 +1,15 @@ +#lang resyntax/test + + +require: resyntax/test-warning-suite test-warning-suite + + +comment-only-test: "warning-only rule should produce a comment" +-------------------- +#lang racket/base + +(define a 5) +(equal? a a) +-------------------- +@inspect - (equal? a a) +@assertMatch test-warning-rule diff --git a/test/private/rackunit.rkt b/test/private/rackunit.rkt index 424d4f4..4779aca 100644 --- a/test/private/rackunit.rkt +++ b/test/private/rackunit.rkt @@ -12,7 +12,8 @@ add-suite-under-test! check-suite-refactors check-suite-does-not-refactor - check-suite-analysis) + check-suite-analysis + check-suite-comment-only) (require racket/logging @@ -312,6 +313,47 @@ (fail-check "analysis assigned an incorrect value for the given syntax property key")))) +(define-check (check-suite-comment-only program context-list target rule-name) + (define suite (current-suite-under-test)) + (set! program (code-block-append (current-header) program)) + (define program-src (string-source (code-block-raw-string program))) + (define-values (call-with-logs-captured build-logs-info) (make-log-capture-utilities)) + + (define result-set + (call-with-logs-captured + (λ () (resyntax-analyze program-src + #:suite suite + #:timeout-ms (current-analyzer-timeout-millis))))) + + (define results (refactoring-result-set-results result-set)) + + ;; Find target location + (define target-src (string-source (string-trim (code-block-raw-string target)))) + (define context-src-list + (for/list ([ctx (in-list context-list)]) + (string-source (string-trim (code-block-raw-string ctx))))) + + ;; Try to find a result that matches the target location and rule name + (define matching-results + (for/list ([result (in-list results)] + #:when (and (equal? (refactoring-result-rule-name result) rule-name) + (not (refactoring-result-has-fix? result)))) + result)) + + (with-check-info (['logs (build-logs-info)] + ['program (string-block-info (string-source-contents program-src))] + ['target (string-block-info (string-source-contents target-src))] + ['rule-name rule-name]) + (when (empty? matching-results) + (fail-check "no warning-only result found for the specified rule")) + + (when (> (length matching-results) 1) + (fail-check (format "found ~a warning-only results, expected exactly 1" (length matching-results)))) + + ;; Success - we found exactly one matching warning-only result + (void))) + + (define (source-find-path-of src target-src #:contexts [context-srcs '()]) (define stx (syntax-label-paths (source-read-syntax src) 'source-path)) (define target-as-string (string-source-contents target-src)) From 932b31d8af54a47d39e54fbfe0dd9b2ede04af5b Mon Sep 17 00:00:00 2001 From: Jacqueline Firth Date: Wed, 12 Nov 2025 19:34:41 -0800 Subject: [PATCH 5/9] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- private/refactoring-result.rkt | 122 +++++++++++++++++---------------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/private/refactoring-result.rkt b/private/refactoring-result.rkt index e019323..7c27edb 100644 --- a/private/refactoring-result.rkt +++ b/private/refactoring-result.rkt @@ -109,30 +109,34 @@ (define (refactoring-result-modified-range result) - (if (refactoring-result-has-fix? result) - (let ([replacement (refactoring-result-string-replacement result)]) - (closed-open-range (add1 (string-replacement-start replacement)) - (add1 (string-replacement-original-end replacement)) - #:comparator natural<=>)) - (let* ([orig-stx (refactoring-result-original-syntax result)] - [pos (syntax-position orig-stx)] - [span (syntax-span orig-stx)]) - (if (and pos span) - (closed-open-range pos (+ pos span) #:comparator natural<=>) - (closed-open-range 1 2 #:comparator natural<=>))))) + (cond + [(refactoring-result-has-fix? result) + (define replacement (refactoring-result-string-replacement result)) + (closed-open-range (add1 (string-replacement-start replacement)) + (add1 (string-replacement-original-end replacement)) + #:comparator natural<=>)] + [else + (define orig-stx (refactoring-result-original-syntax result)) + (define pos (syntax-position orig-stx)) + (define span (syntax-span orig-stx)) + (if (and pos span) + (closed-open-range pos (+ pos span) #:comparator natural<=>) + (closed-open-range 1 2 #:comparator natural<=>))])) (define (refactoring-result-modified-line-range result) - (if (refactoring-result-has-fix? result) - (let ([replacement (refactoring-result-line-replacement result)]) - (closed-open-range (line-replacement-start-line replacement) - (line-replacement-original-end-line replacement) - #:comparator natural<=>)) - (let* ([orig-stx (refactoring-result-original-syntax result)] - [line (syntax-line orig-stx)]) - (if line - (closed-range line line #:comparator natural<=>) - (closed-range 1 1 #:comparator natural<=>))))) + (cond + [(refactoring-result-has-fix? result) + (define replacement (refactoring-result-line-replacement result)) + (closed-open-range (line-replacement-start-line replacement) + (line-replacement-original-end-line replacement) + #:comparator natural<=>)] + [else + (define orig-stx (refactoring-result-original-syntax result)) + (define line (syntax-line orig-stx)) + (if line + (closed-range line line #:comparator natural<=>) + (closed-range 1 1 #:comparator natural<=>))])) (define (refactoring-result-original-line result) @@ -234,44 +238,44 @@ (define (refactoring-result-original-code result) - (if (refactoring-result-has-fix? result) - (let* ([replacement (refactoring-result-string-replacement result)] - [full-orig-code - (source->string (syntax-replacement-source (refactoring-result-syntax-replacement result)))] - [lmap (string-linemap full-orig-code)] - [start (string-replacement-start replacement)] - [end (string-replacement-original-end replacement)] - [start-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))] - [raw-text (string->immutable-string (substring full-orig-code start end))]) - (code-snippet raw-text start-column (linemap-position-to-line lmap (add1 start)))) - (let* ([orig-stx (refactoring-result-original-syntax result)] - [source (refactoring-result-source result)] - [full-orig-code (source->string source)] - [pos (syntax-position orig-stx)] - [span (syntax-span orig-stx)] - [line (or (syntax-line orig-stx) 1)] - [col (or (syntax-column orig-stx) 0)]) - (if (and pos span) - (let* ([start (sub1 pos)] - [end (+ start span)] - [raw-text (string->immutable-string (substring full-orig-code start end))]) - (code-snippet raw-text col line)) - (code-snippet "" col line))))) + (cond + [(refactoring-result-has-fix? result) + (define replacement (refactoring-result-string-replacement result)) + (define full-orig-code + (source->string (syntax-replacement-source (refactoring-result-syntax-replacement result)))) + (define lmap (string-linemap full-orig-code)) + (define start (string-replacement-start replacement)) + (define end (string-replacement-original-end replacement)) + (define start-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))) + (define raw-text (string->immutable-string (substring full-orig-code start end))) + (code-snippet raw-text start-column (linemap-position-to-line lmap (add1 start)))] + [else + (define orig-stx (refactoring-result-original-syntax result)) + (define source (refactoring-result-source result)) + (define full-orig-code (source->string source)) + (define pos (syntax-position orig-stx)) + (define span (syntax-span orig-stx)) + (define line (or (syntax-line orig-stx) 1)) + (define col (or (syntax-column orig-stx) 0)) + (if (and pos span) + (let* ([start (sub1 pos)] + [end (+ start span)] + [raw-text (string->immutable-string (substring full-orig-code start end))]) + (code-snippet raw-text col line)) + (code-snippet "" col line))])) (define (refactoring-result-new-code result) - (if (refactoring-result-has-fix? result) - (let* ([replacement (refactoring-result-string-replacement result)] - [full-orig-code - (source->string (syntax-replacement-source (refactoring-result-syntax-replacement result)))] - [lmap (string-linemap full-orig-code)] - [start (string-replacement-start replacement)] - [original-line (linemap-position-to-line lmap (add1 start))] - [original-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))] - [refactored-source-code (string-apply-replacement full-orig-code replacement)] - [new-code-string - (substring refactored-source-code - (string-replacement-start replacement) - (string-replacement-new-end replacement))]) - (code-snippet new-code-string original-column original-line)) - #false)) + (and (refactoring-result-has-fix? result) + (let* ([replacement (refactoring-result-string-replacement result)] + [full-orig-code (source->string (syntax-replacement-source + (refactoring-result-syntax-replacement result)))] + [lmap (string-linemap full-orig-code)] + [start (string-replacement-start replacement)] + [original-line (linemap-position-to-line lmap (add1 start))] + [original-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))] + [refactored-source-code (string-apply-replacement full-orig-code replacement)] + [new-code-string (substring refactored-source-code + (string-replacement-start replacement) + (string-replacement-new-end replacement))]) + (code-snippet new-code-string original-column original-line)))) From 6e1533cdea9465f6e770406ad27c20525e007e80 Mon Sep 17 00:00:00 2001 From: Jacqueline Firth Date: Wed, 12 Nov 2025 19:48:26 -0800 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- private/refactoring-result.rkt | 74 ++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/private/refactoring-result.rkt b/private/refactoring-result.rkt index 7c27edb..fef0e9b 100644 --- a/private/refactoring-result.rkt +++ b/private/refactoring-result.rkt @@ -76,21 +76,23 @@ (define (refactoring-result #:rule-name rule-name #:message message #:syntax-replacement [replacement #false]) - (if replacement - (let ([str-replacement (syntax-replacement-render replacement)] - [full-orig-code (source->string (syntax-replacement-source replacement))]) - (constructor:refactoring-result - #:rule-name rule-name - #:message (string->immutable-string message) - #:source (syntax-replacement-source replacement) - #:original-syntax (syntax-replacement-original-syntax replacement) - #:syntax-replacement replacement - #:string-replacement str-replacement - #:line-replacement (string-replacement->line-replacement str-replacement full-orig-code))) - (raise-arguments-error 'refactoring-result - "must provide either #:syntax-replacement" - "rule-name" rule-name - "message" message))) + (unless replacement + (raise-arguments-error 'refactoring-result + "must provide either #:syntax-replacement" + "rule-name" + rule-name + "message" + message)) + (let ([str-replacement (syntax-replacement-render replacement)] + [full-orig-code (source->string (syntax-replacement-source replacement))]) + (constructor:refactoring-result + #:rule-name rule-name + #:message (string->immutable-string message) + #:source (syntax-replacement-source replacement) + #:original-syntax (syntax-replacement-original-syntax replacement) + #:syntax-replacement replacement + #:string-replacement str-replacement + #:line-replacement (string-replacement->line-replacement str-replacement full-orig-code)))) (define (warning-result #:rule-name rule-name #:message message #:source source #:original-syntax original-syntax) @@ -257,25 +259,29 @@ (define span (syntax-span orig-stx)) (define line (or (syntax-line orig-stx) 1)) (define col (or (syntax-column orig-stx) 0)) - (if (and pos span) - (let* ([start (sub1 pos)] - [end (+ start span)] - [raw-text (string->immutable-string (substring full-orig-code start end))]) - (code-snippet raw-text col line)) - (code-snippet "" col line))])) + (cond + [(and pos span) + (define start (sub1 pos)) + (define end (+ start span)) + (define raw-text (string->immutable-string (substring full-orig-code start end))) + (code-snippet raw-text col line)] + [else (code-snippet "" col line)])])) (define (refactoring-result-new-code result) - (and (refactoring-result-has-fix? result) - (let* ([replacement (refactoring-result-string-replacement result)] - [full-orig-code (source->string (syntax-replacement-source - (refactoring-result-syntax-replacement result)))] - [lmap (string-linemap full-orig-code)] - [start (string-replacement-start replacement)] - [original-line (linemap-position-to-line lmap (add1 start))] - [original-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))] - [refactored-source-code (string-apply-replacement full-orig-code replacement)] - [new-code-string (substring refactored-source-code - (string-replacement-start replacement) - (string-replacement-new-end replacement))]) - (code-snippet new-code-string original-column original-line)))) + (cond + [(refactoring-result-has-fix? result) + (define replacement (refactoring-result-string-replacement result)) + (define full-orig-code + (source->string (syntax-replacement-source (refactoring-result-syntax-replacement result)))) + (define lmap (string-linemap full-orig-code)) + (define start (string-replacement-start replacement)) + (define original-line (linemap-position-to-line lmap (add1 start))) + (define original-column (- (add1 start) (linemap-position-to-start-of-line lmap (add1 start)))) + (define refactored-source-code (string-apply-replacement full-orig-code replacement)) + (define new-code-string + (substring refactored-source-code + (string-replacement-start replacement) + (string-replacement-new-end replacement))) + (code-snippet new-code-string original-column original-line)] + [else #f])) From 031ac67961f0d46b9e294f933ed57487fb8e78b0 Mon Sep 17 00:00:00 2001 From: Jacqueline Firth Date: Wed, 12 Nov 2025 20:00:42 -0800 Subject: [PATCH 7/9] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- private/refactoring-result.rkt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/private/refactoring-result.rkt b/private/refactoring-result.rkt index fef0e9b..175a9e2 100644 --- a/private/refactoring-result.rkt +++ b/private/refactoring-result.rkt @@ -83,16 +83,16 @@ rule-name "message" message)) - (let ([str-replacement (syntax-replacement-render replacement)] - [full-orig-code (source->string (syntax-replacement-source replacement))]) - (constructor:refactoring-result - #:rule-name rule-name - #:message (string->immutable-string message) - #:source (syntax-replacement-source replacement) - #:original-syntax (syntax-replacement-original-syntax replacement) - #:syntax-replacement replacement - #:string-replacement str-replacement - #:line-replacement (string-replacement->line-replacement str-replacement full-orig-code)))) + (define str-replacement (syntax-replacement-render replacement)) + (define full-orig-code (source->string (syntax-replacement-source replacement))) + (constructor:refactoring-result + #:rule-name rule-name + #:message (string->immutable-string message) + #:source (syntax-replacement-source replacement) + #:original-syntax (syntax-replacement-original-syntax replacement) + #:syntax-replacement replacement + #:string-replacement str-replacement + #:line-replacement (string-replacement->line-replacement str-replacement full-orig-code))) (define (warning-result #:rule-name rule-name #:message message #:source source #:original-syntax original-syntax) From 367ae06f0343e166b24ee0c9bbcb5beafdde57c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 07:01:09 +0000 Subject: [PATCH 8/9] Refactor API: use suggestion-count and #:no-suggestion keyword - Changed `refactoring-rule-suggested-fixes` to `refactoring-rule-suggestion-count` returning exact-nonnegative-integer - Replaced `#:suggested-fixes 'none` parameter with `#:no-suggestion` keyword as replacement template - Updated all test files to use new API - Tests pass Co-authored-by: jackfirth <8175575+jackfirth@users.noreply.github.com> --- base.rkt | 17 ++++++++--------- main.rkt | 2 +- private/warning-rule-test.rkt | 3 +-- test-warning-suite.rkt | 3 +-- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/base.rkt b/base.rkt index 7db49cd..d7f8180 100644 --- a/base.rkt +++ b/base.rkt @@ -15,7 +15,7 @@ [refactoring-rule? (-> any/c boolean?)] [refactoring-rule-description (-> refactoring-rule? immutable-string?)] [refactoring-rule-analyzers (-> refactoring-rule? (set/c expansion-analyzer?))] - [refactoring-rule-suggested-fixes (-> refactoring-rule? (or/c 'none 'one))] + [refactoring-rule-suggestion-count (-> refactoring-rule? exact-nonnegative-integer?)] [refactoring-suite? (-> any/c boolean?)] [refactoring-suite (->* () @@ -109,7 +109,7 @@ [(_ new-stx) (syntax-property #'new-stx 'focus-replacement-on #true)])) -(define-object-type refactoring-rule (transformer description uses-universal-tagged-syntax? analyzers suggested-fixes) +(define-object-type refactoring-rule (transformer description uses-universal-tagged-syntax? analyzers suggestion-count) #:omit-root-binding #:constructor-name constructor:refactoring-rule) @@ -141,14 +141,12 @@ #:description description (~optional (~seq #:uses-universal-tagged-syntax? uses-universal-tagged-syntax?)) (~optional (~seq #:analyzers analyzers)) - (~optional (~seq #:suggested-fixes suggested-fixes)) parse-option:syntax-parse-option ... pattern pattern-directive:syntax-parse-pattern-directive ... - replacement) + (~or (~and #:no-suggestion no-suggestion-kw) replacement)) #:declare description (expr/c #'string?) #:declare analyzers (expr/c #'(sequence/c expansion-analyzer?)) - #:declare suggested-fixes (expr/c #'(or/c 'none 'one)) #:attr partial-match-log-statement (and (not (empty? (attribute pattern-directive))) @@ -158,6 +156,8 @@ (syntax-parse directive [(#:when condition:expr) #'(#:when (log-resyntax-rule-condition condition))] [_ directive])) + + #:with suggestion-count-val (datum->syntax #'id (if (attribute no-suggestion-kw) 0 1)) (define id (constructor:refactoring-rule @@ -165,14 +165,15 @@ #:description (string->immutable-string description.c) #:uses-universal-tagged-syntax? (~? uses-universal-tagged-syntax? #false) #:analyzers (for/set ([analyzer (~? analyzers.c '())]) analyzer) - #:suggested-fixes (~? suggested-fixes.c 'one) + #:suggestion-count suggestion-count-val #:transformer (λ (stx) (syntax-parse stx (~@ . parse-option) ... [pattern (~? (~@ #:do [partial-match-log-statement])) - (~@ . wrapped-pattern-directive) ... (present #'replacement)] + (~@ . wrapped-pattern-directive) ... + (~? (present #'replacement) (present #'(void)))] [_ absent]))))) @@ -180,7 +181,6 @@ (define-definition-context-refactoring-rule id:id #:description (~var description (expr/c #'string?)) (~optional (~seq #:analyzers (~var analyzers (expr/c #'(sequence/c expansion-analyzer?))))) - (~optional (~seq #:suggested-fixes (~var suggested-fixes (expr/c #'(or/c 'none 'one))))) parse-option:syntax-parse-option ... splicing-pattern pattern-directive:syntax-parse-pattern-directive ... @@ -234,7 +234,6 @@ (define-refactoring-rule id #:description description (~? (~@ #:analyzers analyzers)) - (~? (~@ #:suggested-fixes suggested-fixes)) (~var expression expression-matching-id) expression.refactored))) diff --git a/main.rkt b/main.rkt index 41e8a9e..f00563a 100644 --- a/main.rkt +++ b/main.rkt @@ -384,7 +384,7 @@ absent)]) ;; Check if this is a warning-only rule (cond - [(eq? (refactoring-rule-suggested-fixes rule) 'none) + [(zero? (refactoring-rule-suggestion-count rule)) ;; For warning-only rules, try to match the pattern (define match-result (parameterize ([current-namespace (source-code-analysis-namespace analysis)]) diff --git a/private/warning-rule-test.rkt b/private/warning-rule-test.rkt index 1be98c4..318adc6 100644 --- a/private/warning-rule-test.rkt +++ b/private/warning-rule-test.rkt @@ -10,10 +10,9 @@ ;; Define a warning-only rule that matches any (equal? x y) (define-refactoring-rule test-warning-rule #:description "This is a test warning rule for equal?" - #:suggested-fixes 'none #:literals (equal?) (equal? x y) - (void)) + #:no-suggestion) ;; Test that the rule works (define test-suite (refactoring-suite #:rules (list test-warning-rule))) diff --git a/test-warning-suite.rkt b/test-warning-suite.rkt index e3c836c..1e00711 100644 --- a/test-warning-suite.rkt +++ b/test-warning-suite.rkt @@ -7,10 +7,9 @@ ;; Define a warning-only rule that matches any (equal? x y) (define-refactoring-rule test-warning-rule #:description "Test warning rule for equal?" - #:suggested-fixes 'none #:literals (equal?) (equal? x y) - (void)) + #:no-suggestion) (define test-warning-suite (refactoring-suite #:rules (list test-warning-rule))) From f514b0fd380f473a9784b5b77348544113d76da0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 08:15:31 +0000 Subject: [PATCH 9/9] Return #t instead of (void) for no-suggestion rules When a rule uses #:no-suggestion, the transformer now returns (present #t) instead of (present #'(void)) for pattern matches. Co-authored-by: jackfirth <8175575+jackfirth@users.noreply.github.com> --- base.rkt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base.rkt b/base.rkt index d7f8180..29f7b41 100644 --- a/base.rkt +++ b/base.rkt @@ -173,7 +173,7 @@ [pattern (~? (~@ #:do [partial-match-log-statement])) (~@ . wrapped-pattern-directive) ... - (~? (present #'replacement) (present #'(void)))] + (~? (present #'replacement) (present #t))] [_ absent])))))