diff --git a/default-recommendations/conditional-shortcuts.rkt b/default-recommendations/conditional-shortcuts.rkt index 7968b5e..375bfb2 100644 --- a/default-recommendations/conditional-shortcuts.rkt +++ b/default-recommendations/conditional-shortcuts.rkt @@ -195,11 +195,9 @@ (~and else-expr:if-arm (~not (void)))) #:when (or (attribute then-expr.uses-begin?) (attribute else-expr.uses-begin?)) #:with (true-branch ...) - (if (or (multiline-syntax? #'condition) - (multiline-syntax? #'then-expr) - (attribute then-expr.uses-begin?)) + (if (attribute then-expr.uses-begin?) #'([condition (ORIGINAL-GAP condition then-expr) then-expr.refactored ...]) - #'((ORIGINAL-GAP condition then-expr) [condition then-expr.refactored ...])) + #'([condition then-expr.refactored ...])) #:with (false-branch ...) (if (attribute else-expr.uses-begin?) #'([else (ORIGINAL-GAP then-expr else-expr) else-expr.refactored ...]) @@ -218,11 +216,9 @@ (~and else-expr:if-arm (~not (void)))) #:when (or (attribute then-expr.uses-let?) (attribute else-expr.uses-let?)) #:with (true-branch ...) - (if (or (multiline-syntax? #'condition) - (multiline-syntax? #'then-expr) - (attribute then-expr.uses-let?)) + (if (attribute then-expr.uses-let?) #'([condition (ORIGINAL-GAP condition then-expr) then-expr.refactored ...]) - #'((ORIGINAL-GAP condition then-expr) [condition then-expr.refactored ...])) + #'([condition then-expr.refactored ...])) #:with (false-branch ...) (if (attribute else-expr.uses-let?) #'([else (ORIGINAL-GAP then-expr else-expr) else-expr.refactored ...]) diff --git a/default-recommendations/gap-preservation-test.rkt b/default-recommendations/gap-preservation-test.rkt new file mode 100644 index 0000000..6e838ae --- /dev/null +++ b/default-recommendations/gap-preservation-test.rkt @@ -0,0 +1,67 @@ +#lang resyntax/testing/refactoring-test + + +require: resyntax/default-recommendations/gap-preservation gap-preservation-rules + + +header: +- #lang racket/base + + +test: "comments preserved in splice when form inserted at front" +----------------------------------- +(define (code insert-foo-first a b) + (insert-foo-first a + ; comment + b)) +----------------------------------- +----------------------------------- +(define (code insert-foo-first a b) + ("foo" a + ; comment + b)) +----------------------------------- + + +test: "later comments preserved in splice when form inserted after first" +----------------------------------- +(define (code insert-foo-second a b c) + (insert-foo-second a + b + ; preserved comment + c)) +----------------------------------- +----------------------------------- +(define (code insert-foo-second a b c) + (a "foo" + b + ; preserved comment + c)) +----------------------------------- + + +test: "not refactorable when comment dropped due to inserted form" +----------------------------------- +(define (code insert-foo-second a b c) + (insert-foo-second a + ; dropped comment + b + c)) +----------------------------------- + + +test: "comments preserved in splice when form inserted at end" +----------------------------------- +(define (code insert-foo-last a b c) + (insert-foo-last a + b + ; comment + c)) +----------------------------------- +----------------------------------- +(define (code insert-foo-last a b c) + (a b + ; comment + c + "foo")) +----------------------------------- diff --git a/default-recommendations/gap-preservation.rkt b/default-recommendations/gap-preservation.rkt new file mode 100644 index 0000000..961451d --- /dev/null +++ b/default-recommendations/gap-preservation.rkt @@ -0,0 +1,67 @@ +#lang racket/base + + +;; These aren't real refactoring rules. They're only used for testing Resyntax. Specifically, they're +;; used to test that Resyntax properly preserves comments in between sequences of forms that are left +;; unchanged by a refactoring rule, even when forms before and after that sequence are changed. See +;; the accompanying tests in gap-preservation-test.rkt for examples. + + +(require racket/contract/base) + + +(provide + (contract-out + [gap-preservation-rules refactoring-suite?])) + + +(require (for-syntax racket/base) + racket/list + rebellion/private/static-name + resyntax/default-recommendations/private/boolean + resyntax/default-recommendations/private/definition-context + resyntax/default-recommendations/private/exception + resyntax/default-recommendations/private/let-binding + resyntax/default-recommendations/private/metafunction + resyntax/default-recommendations/private/syntax-lines + resyntax/refactoring-rule + resyntax/refactoring-suite + resyntax/private/syntax-replacement + syntax/parse) + + +;@---------------------------------------------------------------------------------------------------- + + +(define-refactoring-rule suggest-inserting-foo-first + #:description "This refactoring rule is for testing Resyntax, ignore its suggestions." + #:datum-literals (insert-foo-first) + [(insert-foo-first a ...) ("foo" a ...)]) + + +(define-refactoring-rule suggest-inserting-foo-second + #:description "This refactoring rule is for testing Resyntax, ignore its suggestions." + #:datum-literals (insert-foo-second) + [(insert-foo-second a0 a ...) (a0 "foo" a ...)]) + + +(define-refactoring-rule suggest-inserting-foo-last + #:description "This refactoring rule is for testing Resyntax, ignore its suggestions." + #:datum-literals (insert-foo-last) + [(insert-foo-last a ...) (a ... "foo")]) + + +(define-refactoring-rule suggest-inserting-foo-first-and-last + #:description "This refactoring rule is for testing Resyntax, ignore its suggestions." + #:datum-literals (insert-foo-first-and-last) + [(insert-foo-first-and-last a ...) ("foo" a ... "foo")]) + + +(define gap-preservation-rules + (refactoring-suite + #:name (name gap-preservation-rules) + #:rules + (list suggest-inserting-foo-first + suggest-inserting-foo-second + suggest-inserting-foo-last + suggest-inserting-foo-first-and-last))) \ No newline at end of file diff --git a/default-recommendations/let-binding-suggestions-comment-test.rkt b/default-recommendations/let-binding-suggestions-comment-test.rkt index bb17765..be8bfd2 100644 --- a/default-recommendations/let-binding-suggestions-comment-test.rkt +++ b/default-recommendations/let-binding-suggestions-comment-test.rkt @@ -25,7 +25,7 @@ test: "let binding with commented right-hand-side expression" ------------------------------ -test: "let binding with commented body not refactorable (yet)" +test: "let binding with commented body refactorable" ------------------------------ (define (f) (let ([x 1]) @@ -33,6 +33,13 @@ test: "let binding with commented body not refactorable (yet)" ;; Comment 1)) ------------------------------ +------------------------------ +(define (f) + (define x 1) + (void) + ;; Comment + 1) +------------------------------ test: "let binding with comments before let form not refactorable (yet)" diff --git a/private/string-replacement.rkt b/private/string-replacement.rkt index a76de3c..646cdaf 100644 --- a/private/string-replacement.rkt +++ b/private/string-replacement.rkt @@ -25,6 +25,7 @@ [string-replacement-new-span (-> string-replacement? natural?)] [string-replacement-contents (-> string-replacement? (listof (or/c inserted-string? copied-string?)))] + [string-replacement-preserved-locations (-> string-replacement? range-set?)] [string-replacement-overlaps? (-> string-replacement? string-replacement? boolean?)] [string-replacement-union (->i ([replacement1 string-replacement?] @@ -62,6 +63,7 @@ rebellion/base/option rebellion/base/range rebellion/collection/list + rebellion/collection/range-set rebellion/private/static-name rebellion/streaming/reducer rebellion/streaming/transducer @@ -250,3 +252,15 @@ #:contents (list (inserted-string "goodbye") (copied-string 5 6) (inserted-string "friend")))) (check-equal? union expected) (check-equal? (transduce (list r1 r2) #:into union-into-string-replacement) expected))) + + +(define (string-replacement-preserved-locations replacement) + (define before-replacement + (less-than-range (string-replacement-start replacement) #:comparator natural<=>)) + (define after-replacement + (at-least-range (string-replacement-original-end replacement) #:comparator natural<=>)) + (for/fold ([ranges (range-set before-replacement after-replacement)]) + ([piece (in-list (string-replacement-contents replacement))] + #:when (copied-string? piece)) + (match-define (copied-string start end) piece) + (range-set-add ranges (closed-open-range start end #:comparator natural<=>)))) diff --git a/private/syntax-neighbors.rkt b/private/syntax-neighbors.rkt new file mode 100644 index 0000000..bd464cc --- /dev/null +++ b/private/syntax-neighbors.rkt @@ -0,0 +1,109 @@ +#lang racket/base + + +;; This module provides a means to track which subforms of a syntax object have remained +;; unchanged and remained neighbors after a syntax object transformation. The function +;; syntax-mark-original-neighbors traverses all forms within a syntax object and stores +;; their original neighboring forms within syntax properties. Storing this metadata in +;; properties allows it to be preserved when macros, refactoring rules, and other syntax +;; transformations shuffle subforms around. This is used by Resyntax to preserve formatting +;; and comments when sequences of adjacent forms are left unchanged by a refactoring rule. + + +(require racket/contract/base) + + +(provide + (contract-out + [syntax-original-leading-neighbor (-> syntax? (or/c syntax? #false))] + [syntax-original-trailing-neighbor (-> syntax? (or/c syntax? #false))] + [syntax-originally-neighbors? (-> syntax? syntax? boolean?)] + [syntax-mark-original-neighbors (-> syntax? syntax?)])) + + +(require guard + racket/syntax-srcloc + syntax/parse) + + +(module+ test + (require (submod "..") + racket/syntax + rackunit)) + + +;@---------------------------------------------------------------------------------------------------- + + +(define (syntax-mark-original-neighbors stx) + (syntax-parse stx + [(~and (subform ...+) (_ trailing-neighbor ...) (leading-neighbor ... _)) + (define leading-neighbors (cons #false (attribute leading-neighbor))) + (define trailing-neighbors (append (attribute trailing-neighbor) (list #false))) + (define results + (for/list ([leading (in-list leading-neighbors)] + [trailing (in-list trailing-neighbors)] + [subform-stx (in-list (attribute subform))]) + (mark-neighbors (syntax-mark-original-neighbors subform-stx) + #:leading-neighbor leading + #:trailing-neighbor trailing))) + (datum->syntax stx results stx stx)] + [_ stx])) + + +(define (mark-neighbors stx #:leading-neighbor leading-stx #:trailing-neighbor trailing-stx) + (define stx-with-leading + (if leading-stx + (syntax-property stx 'original-leading-neighbor leading-stx) + stx)) + (if trailing-stx + (syntax-property stx-with-leading + 'original-trailing-neighbor + trailing-stx) + stx-with-leading)) + + +(define (syntax-original-leading-neighbor stx) + (syntax-property stx 'original-leading-neighbor)) + + +(define (syntax-original-trailing-neighbor stx) + (syntax-property stx 'original-trailing-neighbor)) + + +(define/guard (syntax-originally-neighbors? left-stx right-stx) + (define left-trailer (syntax-original-trailing-neighbor left-stx)) + (define right-leader (syntax-original-leading-neighbor right-stx)) + (guard (and left-trailer right-leader) #:else #false) + (define left-srcloc (syntax-srcloc left-stx)) + (define left-trailer-srcloc (syntax-srcloc left-trailer)) + (define right-srcloc (syntax-srcloc right-stx)) + (define right-leader-srcloc (syntax-srcloc right-leader)) + (guard (and left-srcloc left-trailer-srcloc right-srcloc right-leader-srcloc) #:else #false) + (and (equal? left-trailer-srcloc right-srcloc) + (equal? right-leader-srcloc left-srcloc))) + + +(module+ test + (test-case "syntax-mark-original-neighbors" + (define stx #'(foo (a b c) bar (baz))) + (define marked (syntax-mark-original-neighbors stx)) + (check-equal? (syntax->datum marked) (syntax->datum stx)) + (define/with-syntax (foo* (a* b* c*) bar* (baz*)) marked) + (check-false (syntax-original-leading-neighbor #'foo*)) + (check-equal? (syntax->datum (syntax-original-trailing-neighbor #'foo*)) '(a b c)) + (check-false (syntax-original-leading-neighbor #'a*)) + (check-equal? (syntax->datum (syntax-original-trailing-neighbor #'a*)) 'b) + (check-equal? (syntax->datum (syntax-original-leading-neighbor #'b*)) 'a) + (check-equal? (syntax->datum (syntax-original-trailing-neighbor #'b*)) 'c) + (check-equal? (syntax->datum (syntax-original-leading-neighbor #'c*)) 'b) + (check-false (syntax-original-trailing-neighbor #'c*)) + (check-equal? (syntax->datum (syntax-original-leading-neighbor #'bar*)) '(a b c)) + (check-equal? (syntax->datum (syntax-original-trailing-neighbor #'bar*)) '(baz)) + (check-false (syntax-original-leading-neighbor #'baz*)) + (check-false (syntax-original-trailing-neighbor #'baz*)) + (check-false (syntax-originally-neighbors? #'foo* #'b*)) + (check-true (syntax-originally-neighbors? #'a* #'b*)) + (check-true (syntax-originally-neighbors? #'b* #'c*)) + (check-false (syntax-originally-neighbors? #'c* #'bar*)) + (check-false (syntax-originally-neighbors? #'bar* #'baz*)))) diff --git a/private/syntax-replacement.rkt b/private/syntax-replacement.rkt index ce5b466..b4f089b 100644 --- a/private/syntax-replacement.rkt +++ b/private/syntax-replacement.rkt @@ -33,6 +33,7 @@ rebellion/private/static-name rebellion/type/record resyntax/private/string-replacement + resyntax/private/syntax-neighbors (only-in resyntax/default-recommendations/private/syntax-identifier-sets in-syntax-identifiers) syntax/parse) @@ -105,26 +106,40 @@ (define opener (match shape [#false "("] [#\[ "["] [#\{ "{"])) (define closer (match shape [#false ")"] [#\[ "]"] [#\{ "}"])) (define subform-piece-lists - (for/list ([subform-stx (in-list (attribute subform))]) - (pieces subform-stx))) + (for/list ([subform-stx (in-list (attribute subform))] + [trailing-stx (in-list (shift-left (attribute subform)))]) + (define separator-piece + (and trailing-stx + (or (original-separator-piece subform-stx trailing-stx) (inserted-string " ")))) + (if separator-piece + (append (pieces subform-stx) (list separator-piece)) + (pieces subform-stx)))) (append (list (inserted-string opener)) - (join-piece-lists subform-piece-lists) + (apply append subform-piece-lists) (list (inserted-string closer)))] [(subform ... . tail-form) (define shape (syntax-property stx 'paren-shape)) (define opener (match shape [#false "("] [#\[ "["] [#\{ "{"])) (define closer (match shape [#false ")"] [#\[ "]"] [#\{ "}"])) - (define subform-pieces - (join-piece-lists - (for/list ([subform-stx (in-syntax #'(subform ...))]) (pieces subform-stx)))) + (define subform-piece-lists + (for/list ([subform-stx (in-list (attribute subform))] + [trailing-stx (in-list (shift-left (attribute subform)))]) + (define separator-piece + (and trailing-stx + (or (original-separator-piece subform-stx trailing-stx) (inserted-string " ")))) + (if separator-piece + (append (pieces subform-stx) (list separator-piece)) + (pieces subform-stx)))) (define tail-pieces (pieces #'tail-form)) - (define dot-string " . ") + (define dot-piece + (or (original-separator-piece (last (attribute subform)) #'tail-form) + (inserted-string " . "))) (append (list (inserted-string opener)) - subform-pieces - (list (inserted-string dot-string)) + (apply append subform-piece-lists) + (list dot-piece) tail-pieces (list (inserted-string closer)))])) @@ -134,9 +149,16 @@ #:start start #:end (+ start (syntax-span orig-stx)) #:contents (pieces new-stx))) -(define/guard (join-piece-lists piece-lists) - (guard (not (empty? piece-lists)) #:else '()) - (apply append (add-between piece-lists (list (inserted-string " "))))) +(define/guard (original-separator-piece stx trailing-stx) + (guard (syntax-originally-neighbors? stx trailing-stx) #:else #false) + (define stx-end (+ (sub1 (syntax-position stx)) (syntax-span stx))) + (define trailing-start (sub1 (syntax-position trailing-stx))) + (copied-string stx-end trailing-start)) + + +(define/guard (shift-left vs) + (guard-match (cons _ shifted-vs) vs #:else '()) + (append shifted-vs (list #false))) (module+ test @@ -197,42 +219,7 @@ (define (syntax-replacement-preserved-locations replacement) - (define stx (syntax-replacement-new-syntax replacement)) - - (define/guard (pieces stx) - (guard (not (syntax-original? stx)) #:else (list (syntax-source-range stx))) - (syntax-parse stx - #:literals (quote ORIGINAL-GAP ORIGINAL-SPLICE) - - [(ORIGINAL-GAP ~! before after) - (define before-end (+ (sub1 (syntax-position #'before)) (syntax-span #'before))) - (define after-start (sub1 (syntax-position #'after))) - (list (closed-open-range before-end after-start #:comparator natural<=>))] - - [(ORIGINAL-SPLICE ~! original-subform ...) - (guarded-block - (define subforms (syntax->list #'(original-subform ...))) - (guard (not (empty? subforms)) #:else (list)) - (for ([subform-stx (in-list subforms)]) - (unless (syntax-original? subform-stx) - (raise-arguments-error - (name syntax-replacement-render) - "replacement subform within an ORIGINAL-SPLICE form is not original syntax" - "subform" subform-stx - "splice" stx))) - (define start (sub1 (syntax-position (first subforms)))) - (define end (+ (sub1 (syntax-position (last subforms))) (syntax-span (last subforms)))) - (list (closed-open-range start end #:comparator natural<=>)))] - - [(~or v:id v:boolean v:char v:keyword v:number v:regexp v:byte-regexp v:string v:bytes) (list)] - - [(quote datum) (pieces #'datum)] - - [(subform ...) (append-map pieces (syntax->list #'(subform ...)))] - - [(subform ... . tail-form) (append-map pieces (syntax->list #'(subform ... tail-form)))])) - - (sequence->range-set (pieces stx) #:comparator natural<=>)) + (string-replacement-preserved-locations (syntax-replacement-render replacement))) (define (syntax-source-range stx) diff --git a/refactoring-rule.rkt b/refactoring-rule.rkt index b8dfbbb..f8a7ddc 100644 --- a/refactoring-rule.rkt +++ b/refactoring-rule.rkt @@ -26,6 +26,7 @@ rebellion/type/object resyntax/private/source resyntax/private/syntax-replacement + resyntax/private/syntax-neighbors syntax/parse syntax/parse/define) @@ -42,9 +43,18 @@ (define (refactoring-rule-refactor rule syntax #:analysis analysis) + + ;; Before refactoring the input syntax, we do two things: create a new scope and add it, and + ;; traverse the syntax object making a note of each subform's original neighbors. Combined, + ;; these two things allow us to tell when two neighboring subforms within the output syntax object + ;; are originally from the input and were originally next to each other in the input. This allows + ;; Resyntax to preserve any formatting and comments between those two subform when rendering the + ;; resulting syntax replacement into a string transformation. (define rule-introduction-scope (make-syntax-introducer)) + (define prepared-syntax (rule-introduction-scope (syntax-mark-original-neighbors syntax))) + (option-map - ((refactoring-rule-transformer rule) (rule-introduction-scope syntax) analysis) + ((refactoring-rule-transformer rule) prepared-syntax analysis) (λ (new-syntax) (syntax-replacement #:original-syntax syntax