diff --git a/cli.rkt b/cli.rkt index ea98861..6701102 100644 --- a/cli.rkt +++ b/cli.rkt @@ -4,8 +4,10 @@ (require fancy-app json racket/cmdline + racket/file racket/format racket/hash + (except-in racket/list range) racket/logging racket/match racket/path @@ -14,6 +16,7 @@ rebellion/collection/entry rebellion/collection/hash rebellion/collection/list + rebellion/collection/multiset rebellion/collection/range-set rebellion/collection/vector/builder rebellion/streaming/reducer @@ -218,15 +221,15 @@ For help on these, use 'analyze --help' or 'fix --help'." (define (resyntax-analyze-run) (define options (resyntax-analyze-parse-command-line)) - (define files (file-groups-resolve (resyntax-analyze-options-targets options))) - (printf "resyntax: --- analyzing code ---\n") + (define sources (file-groups-resolve (resyntax-analyze-options-targets options))) + (define analysis + (resyntax-analyze-all sources + #:suite (resyntax-analyze-options-suite options) + #:max-passes 1)) (define results - (transduce files - (append-mapping - (λ (portion) - (resyntax-analyze (file-source (file-portion-path portion)) - #:suite (resyntax-analyze-options-suite options) - #:lines (file-portion-lines portion)))) + (transduce (resyntax-analysis-all-results analysis) + (append-mapping in-hash-values) + (append-mapping refactoring-result-set-results) #:into into-list)) (define (display-results) @@ -244,7 +247,7 @@ For help on these, use 'analyze --help' or 'fix --help'." (string-indent (~a old-code) #:amount 2) (string-indent (~a new-code) #:amount 2)))] [(== github-pull-request-review) - (define req (refactoring-results->github-review results #:file-count (length files))) + (define req (refactoring-results->github-review results #:file-count (length sources))) (write-json (github-review-request-jsexpr req))])) (match (resyntax-analyze-options-output-destination options) @@ -259,191 +262,91 @@ For help on these, use 'analyze --help' or 'fix --help'." (define (resyntax-fix-run) (define options (resyntax-fix-parse-command-line)) (define output-format (resyntax-fix-options-output-format options)) - (match output-format - [(== git-commit-message) - (display "This is an automated change generated by Resyntax.\n\n")] - [_ (void)]) - (define files - (transduce (file-groups-resolve (resyntax-fix-options-targets options)) - (indexing file-portion-path) - (grouping into-list) - #:into into-hash)) + (define sources (file-groups-resolve (resyntax-fix-options-targets options))) (define max-modified-files (resyntax-fix-options-max-modified-files options)) (define max-modified-lines (resyntax-fix-options-max-modified-lines options)) - (define results-by-path - (for/fold ([all-results (hash)] - [files files] - [max-fixes (resyntax-fix-options-max-fixes options)] - [lines-to-analyze-by-file (hash)] - #:result all-results) - ([pass-number (in-inclusive-range 1 (resyntax-fix-options-max-pass-count options))] - #:do [(define pass-results - (resyntax-fix-run-one-pass options files - #:lines lines-to-analyze-by-file - #:max-fixes max-fixes - #:max-modified-files max-modified-files - #:max-modified-lines max-modified-lines - #:pass-number pass-number)) - (define pass-fix-count - (for/sum ([(_ results) (in-hash pass-results)]) - (length results))) - (define pass-modified-file-count (hash-count pass-results)) - (define new-max-fixes (- max-fixes pass-fix-count))] - #:break (hash-empty? pass-results) - #:final (zero? new-max-fixes)) - (define new-files (hash-filter-keys files (hash-has-key? pass-results _))) - (define new-lines-to-analyze - (for/hash ([(path results) (in-hash pass-results)]) - (values path - (transduce results - (mapping refactoring-result-modified-line-range) - (filtering nonempty-range?) - #:into (into-range-set natural<=>))))) - (values (hash-union all-results pass-results #:combine append) - new-files - new-max-fixes - new-lines-to-analyze))) + (define analysis + (resyntax-analyze-all sources + #:suite (resyntax-fix-options-suite options) + #:max-fixes (resyntax-fix-options-max-fixes options) + #:max-passes (resyntax-fix-options-max-pass-count options) + #:max-modified-sources max-modified-files + #:max-modified-lines max-modified-lines)) + (resyntax-analysis-write-file-changes! analysis) (match output-format - [(== plain-text) (printf "resyntax: --- summary ---\n")] - [(== git-commit-message) (printf "## Summary\n\n")]) - (define total-fixes - (for/sum ([(_ results) (in-hash results-by-path)]) - (length results))) - (define total-files (hash-count results-by-path)) + [(== git-commit-message) + (resyntax-fix-print-git-commit-message analysis)] + [(== plain-text) + (resyntax-fix-print-plain-text-summary analysis)])) + + +(define (resyntax-fix-print-git-commit-message analysis) + (display "This is an automated change generated by Resyntax.\n\n") + (for ([pass-results (resyntax-analysis-all-results analysis)] + [pass-number (in-naturals 1)]) + (unless (hash-empty? pass-results) + (printf "#### Pass ~a\n\n" pass-number)) + (for ([(source result-set) (in-hash pass-results)]) + (define result-count (length (refactoring-result-set-results result-set))) + (define fix-string (if (> result-count 1) "fixes" "fix")) + ;; For a commit message, we always use a relative path since we're likely running inside + ;; some CI runner. Additionally, we make the path a link to the corresponding file at HEAD, + ;; since making file paths clickable is pleasant. + (define relative-path (find-relative-path (current-directory) (source-path source))) + (define repo-head-path (format "../blob/HEAD/~a" relative-path)) + (printf "Applied ~a ~a to [`~a`](~a)\n\n" + result-count fix-string relative-path repo-head-path) + (for ([result (in-list (refactoring-result-set-results result-set))]) + (define line (refactoring-result-original-line result)) + (define rule (refactoring-result-rule-name result)) + (define message (refactoring-result-message result)) + (printf " * Line ~a, `~a`: ~a\n" line rule message)) + (newline))) + (printf "## Summary\n\n") + (define total-fixes (resyntax-analysis-total-fixes analysis)) + (define total-files (resyntax-analysis-total-sources-modified analysis)) (define fix-counts-by-rule - (transduce (hash-values results-by-path) - (append-mapping values) - (indexing refactoring-result-rule-name) - (grouping into-count) + (transduce (in-hash-entries (multiset-frequencies (resyntax-analysis-rules-applied analysis))) (sorting #:key entry-value #:descending? #true) #:into into-list)) (define issue-string (if (> total-fixes 1) "issues" "issue")) (define file-string (if (> total-files 1) "files" "file")) - (define summary-message - (if (zero? total-fixes) - "No issues found.\n" - (format "Fixed ~a ~a in ~a ~a.\n\n" total-fixes issue-string total-files file-string))) - (match output-format - [(== plain-text) (printf "\n ~a" summary-message)] - [(== git-commit-message) (printf summary-message)]) + (if (zero? total-fixes) + (printf "No issues found.\n") + (printf "Fixed ~a ~a in ~a ~a.\n\n" total-fixes issue-string total-files file-string)) (for ([rule+count (in-list fix-counts-by-rule)]) (match-define (entry rule count) rule+count) (define occurrence-string (if (> count 1) "occurrences" "occurrence")) - (define rule-string - (match output-format - [(== plain-text) rule] - [(== git-commit-message) (format "`~a`" rule)])) - (printf " * Fixed ~a ~a of ~a\n" count occurrence-string rule-string)) + (printf " * Fixed ~a ~a of `~a`\n" count occurrence-string rule)) (unless (zero? total-fixes) (newline))) -(define (resyntax-fix-run-one-pass options files - #:lines lines-to-analyze-by-file - #:max-fixes max-fixes - #:max-modified-files max-modified-files - #:max-modified-lines max-modified-lines - #:pass-number pass-number) - (define output-format (resyntax-fix-options-output-format options)) - (match output-format - [(== plain-text) - (unless (equal? pass-number 1) - (printf "resyntax: --- pass ~a ---\n" pass-number)) - (printf "resyntax: --- analyzing code ---\n")] - [_ (void)]) - (define all-results - (transduce (in-hash-entries files) ; entries with file path keys and lists of file-portion? values - - ;; The following steps perform a kind of layered shuffle: the files to refactor are - ;; shuffled such that files in the same directory remain together. When combined with - ;; the #:max-modified-files argument, this makes Resyntax prefer to refactor closely - ;; related files instead of selecting arbitrary unrelated files from across an entire - ;; codebase. This limits potential for merge conflicts and makes changes easier to - ;; review, since it's more likely the refactored files will have shared context. - - ; key by directory - (indexing (λ (e) (simple-form-path (build-path (entry-key e) 'up)))) - - ; group by key and shuffle within each group - (grouping (into-transduced (shuffling) #:into into-list)) - - ; shuffle groups - (shuffling) - - ; ungroup and throw away directory - (append-mapping entry-value) - - ;; Now the stream contains exactly what it did before the above steps, but shuffled in - ;; a convenient manner. - - (append-mapping entry-value) ; throw away the file path, we don't need it anymore - (mapping (filter-file-portion _ lines-to-analyze-by-file)) - (append-mapping - (λ (portion) - (resyntax-analyze (file-source (file-portion-path portion)) - #:suite (resyntax-fix-options-suite options) - #:lines (file-portion-lines portion)))) - (limiting max-modified-lines - #:by (λ (result) - (define replacement (refactoring-result-line-replacement result)) - (add1 (- (line-replacement-original-end-line replacement) - (line-replacement-start-line replacement))))) - (if (equal? max-fixes +inf.0) (transducer-pipe) (taking max-fixes)) - (if (equal? max-modified-files +inf.0) - (transducer-pipe) - (transducer-pipe - (indexing - (λ (result) - (syntax-replacement-source (refactoring-result-syntax-replacement result)))) - (grouping into-list) - (taking max-modified-files) - (append-mapping entry-value))) - #:into into-list)) - (define results-by-path - (transduce - all-results - (indexing - (λ (result) - (file-source-path - (syntax-replacement-source (refactoring-result-syntax-replacement result))))) - (grouping (into-transduced (sorting #:key refactoring-result-original-line) #:into into-list)) - #:into into-hash)) - (match output-format - [(== plain-text) (printf "resyntax: --- fixing code ---\n")] - [(== git-commit-message) - (unless (hash-empty? results-by-path) - (printf "#### Pass ~a\n\n" pass-number))]) - (for ([(path results) (in-hash results-by-path)]) - (define result-count (length results)) - (define fix-string (if (> result-count 1) "fixes" "fix")) - (match output-format - [(== plain-text) - (printf "resyntax: applying ~a ~a to ~a\n\n" result-count fix-string path)] - [(== git-commit-message) - ;; For a commit message, we always use a relative path since we're likely running inside - ;; some CI runner. Additionally, we make the path a link to the corresponding file at HEAD, - ;; since making file paths clickable is pleasant. - (define relative-path (find-relative-path (current-directory) path)) - (define repo-head-path (format "../blob/HEAD/~a" relative-path)) - (printf "Applied ~a ~a to [`~a`](~a)\n\n" - result-count fix-string relative-path repo-head-path)]) - (for ([result (in-list results)]) - (define line (refactoring-result-original-line result)) - (define rule (refactoring-result-rule-name result)) - (define message (refactoring-result-message result)) - (match output-format - [(== plain-text) (printf " * [line ~a] ~a: ~a\n" line rule message)] - [(== git-commit-message) (printf " * Line ~a, `~a`: ~a\n" line rule message)])) - (refactor! results) - (newline)) - results-by-path) - - -(define (filter-file-portion portion lines-by-path) - (define path (file-portion-path portion)) - (define lines (file-portion-lines portion)) - (define ranges-to-remove (range-set-complement (hash-ref lines-by-path path all-lines))) - (file-portion path (range-set-remove-all lines ranges-to-remove))) +(define (resyntax-fix-print-plain-text-summary analysis) + (printf "resyntax: --- summary ---\n\n") + (define total-fixes (resyntax-analysis-total-fixes analysis)) + (define total-files (resyntax-analysis-total-sources-modified analysis)) + (define message + (cond + [(zero? total-fixes) "No issues found."] + [(equal? total-fixes 1) "Fixed 1 issue in 1 file."] + [(equal? total-files 1) (format "Fixed ~a issues in 1 file." total-fixes)] + [else (format "Fixed ~a issues in ~a files." total-fixes total-files)])) + (printf " ~a\n\n" message) + (define rules-applied (resyntax-analysis-rules-applied analysis)) + (transduce (in-hash-entries (multiset-frequencies rules-applied)) + (sorting #:key entry-value #:descending? #true) + (mapping + (λ (e) + (match-define (entry rule-name rule-fixes) e) + (define message + (if (equal? rule-fixes 1) + (format "Fixed 1 occurrence of ~a" rule-name) + (format "Fixed ~a occurrences of ~a" rule-fixes rule-name))) + (format " * ~a\n" message))) + #:into (into-for-each display)) + (when (positive? total-fixes) + (newline))) (module+ main diff --git a/main.rkt b/main.rkt index 31dfc93..6dd82fe 100644 --- a/main.rkt +++ b/main.rkt @@ -6,29 +6,49 @@ (provide (contract-out + [resyntax-analysis? (-> any/c boolean?)] + [resyntax-analysis-all-results + (-> resyntax-analysis? + (listof (hash/c source? refactoring-result-set? #:flat? #true #:immutable #true)))] + [resyntax-analysis-final-sources (-> resyntax-analysis? (listof modified-source?))] + [resyntax-analysis-total-fixes (-> resyntax-analysis? exact-nonnegative-integer?)] + [resyntax-analysis-total-sources-modified (-> resyntax-analysis? exact-nonnegative-integer?)] + [resyntax-analysis-rules-applied (-> resyntax-analysis? multiset?)] + [resyntax-analysis-write-file-changes! (-> resyntax-analysis? void?)] [resyntax-analyze - (->* (source?) (#:suite refactoring-suite? #:lines range-set?) (listof refactoring-result?))] + (->* (source?) (#:suite refactoring-suite? #:lines range-set?) refactoring-result-set?)] + [resyntax-analyze-all + (->* ((hash/c source? range-set? #:flat? #true)) + (#:suite refactoring-suite? + #:max-fixes (or/c exact-nonnegative-integer? +inf.0) + #:max-passes exact-nonnegative-integer? + #:max-modified-sources (or/c exact-nonnegative-integer? +inf.0) + #:max-modified-lines (or/c exact-nonnegative-integer? +inf.0)) + resyntax-analysis?)] [refactor! (-> (sequence/c refactoring-result?) void?)])) (require fancy-app guard + racket/file racket/match - racket/port racket/sequence - racket/syntax-srcloc rebellion/base/comparator rebellion/base/option rebellion/base/range rebellion/collection/entry rebellion/collection/hash rebellion/collection/list + rebellion/collection/multiset rebellion/collection/range-set + rebellion/streaming/reducer rebellion/streaming/transducer + rebellion/type/record resyntax/base resyntax/default-recommendations resyntax/private/comment-reader - resyntax/private/file-group + resyntax/private/limiting + resyntax/private/line-replacement resyntax/private/logger resyntax/private/refactoring-result resyntax/private/source @@ -36,6 +56,7 @@ resyntax/private/string-replacement resyntax/private/syntax-range resyntax/private/syntax-replacement + (except-in racket/list range) (submod resyntax/base private)) @@ -48,6 +69,51 @@ ;@---------------------------------------------------------------------------------------------------- +(define-record-type resyntax-analysis (all-results) #:omit-root-binding) + + +(define (resyntax-analysis #:all-results all-results) + (constructor:resyntax-analysis #:all-results (sequence->list all-results))) + + +(define (resyntax-analysis-final-sources analysis) + (transduce (resyntax-analysis-all-results analysis) + (append-mapping in-hash-values) + (mapping refactoring-result-set-updated-source) + (indexing modified-source-original) + (grouping nonempty-into-last) + (mapping entry-value) + #:into into-list)) + + +(define (resyntax-analysis-total-fixes analysis) + (for*/sum ([pass-results (in-list (resyntax-analysis-all-results analysis))] + [result-set (in-hash-values pass-results)]) + (length (refactoring-result-set-results result-set)))) + + +(define/guard (resyntax-analysis-total-sources-modified analysis) + (define all-results (resyntax-analysis-all-results analysis)) + (guard (not (empty? all-results)) #:else 0) + (hash-count (first all-results))) + + +(define (resyntax-analysis-rules-applied analysis) + (for*/multiset ([pass-results (in-list (resyntax-analysis-all-results analysis))] + [result-set (in-hash-values pass-results)] + [result (in-list (refactoring-result-set-results result-set))]) + (refactoring-result-rule-name result))) + + +(define (resyntax-analysis-write-file-changes! analysis) + (log-resyntax-info "--- fixing code ---") + (for ([source (in-list (resyntax-analysis-final-sources analysis))] + #:when (source-path source)) + (log-resyntax-info "fixing ~a" (source-path source)) + (display-to-file (modified-source-contents source) (source-path source) + #:mode 'text #:exists 'replace))) + + (define (resyntax-analyze source #:suite [suite default-recommendations] #:lines [lines (range-set (unbounded-range #:comparator natural<=>))]) @@ -63,14 +129,114 @@ (or (source-path source) "string source") (string-indent (exn-message e) #:amount 3)) empty-list) + + (define results + (with-handlers ([exn:fail:syntax? skip] + [exn:fail:filesystem:missing-module? skip] + [exn:fail:contract:variable? skip]) + (define analysis + (parameterize ([current-namespace (make-base-namespace)]) + (source-analyze source #:lines lines))) + (refactor-visited-forms #:analysis analysis #:suite suite #:comments comments #:lines lines))) - (with-handlers ([exn:fail:syntax? skip] - [exn:fail:filesystem:missing-module? skip]) - (define analysis - (parameterize ([current-namespace (make-base-namespace)]) - (source-analyze source #:lines lines))) - (refactor-visited-forms #:analysis analysis #:suite suite #:comments comments #:lines lines))) - + (refactoring-result-set #:base-source source #:results results)) + + +(define (resyntax-analyze-all sources + #:suite [suite default-recommendations] + #:max-fixes [max-fixes +inf.0] + #:max-passes [max-passes 10] + #:max-modified-sources [max-modified-sources +inf.0] + #:max-modified-lines [max-modified-lines +inf.0]) + (log-resyntax-info "--- analyzing code ---") + (for/fold ([pass-result-lists '()] + [sources sources] + [max-fixes max-fixes] + #:result (resyntax-analysis #:all-results (reverse pass-result-lists))) + ([pass-index (in-range max-passes)] + #:do [(unless (zero? pass-index) + (log-resyntax-info "--- pass ~a ---" (add1 pass-index))) + (define pass-results + (resyntax-analyze-all-once sources + #:suite suite + #:max-fixes max-fixes + #:max-modified-sources max-modified-sources + #:max-modified-lines max-modified-lines)) + (define pass-fix-count (count-total-results pass-results)) + (define new-max-fixes (- max-fixes pass-fix-count))] + #:break (hash-empty? pass-results) + #:final (zero? new-max-fixes)) + (define modified-sources (build-modified-source-map pass-results)) + (values (cons pass-results pass-result-lists) modified-sources new-max-fixes))) + + +(define (count-total-results pass-results) + (for/sum ([(_ result-set) (in-hash pass-results)]) + (length (refactoring-result-set-results result-set)))) + + +(define (build-modified-source-map pass-results) + (transduce (in-hash-values pass-results) + (bisecting refactoring-result-set-updated-source refactoring-result-set-modified-lines) + #:into into-hash)) + + +(define (resyntax-analyze-all-once sources + #:suite suite + #:max-fixes max-fixes + #:max-modified-sources max-modified-sources + #:max-modified-lines max-modified-lines) + (transduce (in-hash-entries sources) ; entries with source keys and line range set values + + ;; The following steps perform a kind of layered shuffle: the files to refactor are + ;; shuffled such that files in the same directory remain together. When combined with + ;; the #:max-modified-sources argument, this makes Resyntax prefer to refactor closely + ;; related files instead of selecting arbitrary unrelated files from across an entire + ;; codebase. This limits potential for merge conflicts and makes changes easier to + ;; review, since it's more likely the refactored files will have shared context. + + ; key by directory + (indexing (λ (e) (source-directory (entry-key e)))) + + ; group by key and shuffle within each group + (grouping (into-transduced (shuffling) #:into into-list)) + + ; shuffle groups + (shuffling) + + ; ungroup and throw away directory + (append-mapping entry-value) + + ;; Now the stream contains exactly what it did before the above steps, but shuffled in + ;; a convenient manner. + + (append-mapping + (λ (e) + (match-define (entry source lines) e) + (define result-set (resyntax-analyze source #:suite suite #:lines lines)) + (refactoring-result-set-results result-set))) + (limiting max-modified-lines + #:by (λ (result) + (define replacement (refactoring-result-line-replacement result)) + (add1 (- (line-replacement-original-end-line replacement) + (line-replacement-start-line replacement))))) + (if (equal? max-fixes +inf.0) (transducer-pipe) (taking max-fixes)) + (if (equal? max-modified-sources +inf.0) + (transducer-pipe) + (transducer-pipe + (indexing + (λ (result) + (syntax-replacement-source (refactoring-result-syntax-replacement result)))) + (grouping into-list) + (taking max-modified-sources) + (append-mapping entry-value))) + (indexing refactoring-result-source) + (grouping into-list) + (mapping + (λ (e) (refactoring-result-set #:base-source (entry-key e) #:results (entry-value e)))) + (indexing refactoring-result-set-base-source) + #:into into-hash)) + (define (refactoring-rules-refactor rules syntax #:comments comments #:analysis analysis) @@ -102,7 +268,7 @@ " original syntax:\n" " ~v\n" " replacement syntax:\n" - " ~v\n") + " ~v") (object-name rule) (syntax-replacement-dropped-comment-locations replacement comments) (syntax-replacement-original-syntax replacement) @@ -142,12 +308,10 @@ (log-resyntax-info (string-append "~a: suggestion discarded because it's outside the analyzed line range\n" " analyzed lines: ~a\n" - " lines modified by result: ~a\n" - " result: ~a") + " lines modified by result: ~a\n") (refactoring-result-rule-name result) lines - modified-lines - result)) + modified-lines)) enclosed?) @@ -186,7 +350,9 @@ (module+ test (test-case "resyntax-analyze" - (define results (resyntax-analyze (string-source "#lang racket (or 1 (or 2 3))"))) + (define results + (refactoring-result-set-results + (resyntax-analyze (string-source "#lang racket (or 1 (or 2 3))")))) (check-equal? (length results) 1) (check-equal? (refactoring-result-string-replacement (first results)) (string-replacement #:start 13 diff --git a/private/file-group.rkt b/private/file-group.rkt index 8f92574..3c388c0 100644 --- a/private/file-group.rkt +++ b/private/file-group.rkt @@ -9,8 +9,8 @@ [file-portion? (-> any/c boolean?)] [file-portion (-> path-string? range-set? file-portion?)] [file-portion-path (-> file-portion? complete-path?)] - [file-portion-lines (-> file-portion? range-set?)] - [file-groups-resolve (-> (sequence/c file-group?) (listof file-portion?))] + [file-portion-lines (-> file-portion? immutable-range-set?)] + [file-groups-resolve (-> (sequence/c file-group?) (hash/c file-source? immutable-range-set?))] [file-group? (-> any/c boolean?)] [single-file-group? (-> any/c boolean?)] [single-file-group (-> path-string? immutable-range-set? single-file-group?)] @@ -32,11 +32,15 @@ racket/string rebellion/base/comparator rebellion/base/range + rebellion/collection/entry + rebellion/collection/hash rebellion/collection/list rebellion/collection/range-set + rebellion/streaming/reducer rebellion/streaming/transducer resyntax/private/git - resyntax/private/logger) + resyntax/private/logger + resyntax/private/source) (module+ test @@ -80,14 +84,9 @@ (define (file-groups-resolve groups) (transduce groups (append-mapping file-group-resolve) - - ;; TODO: this is incorrect - there could be overlapping portions of the same file. The - ;; fix is to group the portions by filename and merge their range sets together. I don't - ;; think I've implemented that operation for range sets yet so I'll get back to that. The - ;; bug only occurs if the same file is included in multiple groups with different ranges. - (deduplicating) - - #:into into-list)) + (bisecting (λ (portion) (file-source (file-portion-path portion))) file-portion-lines) + (grouping (make-fold-reducer range-set-add-all (range-set #:comparator natural<=>))) + #:into into-hash)) (define (file-group-resolve group) diff --git a/private/refactoring-result.rkt b/private/refactoring-result.rkt index 456e555..f701666 100644 --- a/private/refactoring-result.rkt +++ b/private/refactoring-result.rkt @@ -14,6 +14,7 @@ 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-modified-range (-> refactoring-result? range?)] [refactoring-result-modified-line-range (-> refactoring-result? range?)] [refactoring-result-syntax-replacement (-> refactoring-result? syntax-replacement?)] @@ -21,13 +22,24 @@ [refactoring-result-line-replacement (-> refactoring-result? line-replacement?)] [refactoring-result-original-line (-> refactoring-result? exact-positive-integer?)] [refactoring-result-original-code (-> refactoring-result? code-snippet?)] - [refactoring-result-new-code (-> refactoring-result? code-snippet?)])) - - -(require rebellion/base/comparator + [refactoring-result-new-code (-> refactoring-result? code-snippet?)] + [refactoring-result-set? (-> any/c boolean?)] + [refactoring-result-set + (-> #:base-source source? #:results (sequence/c refactoring-result?) refactoring-result-set?)] + [refactoring-result-set-base-source (-> refactoring-result-set? source?)] + [refactoring-result-set-updated-source (-> refactoring-result-set? modified-source?)] + [refactoring-result-set-results (-> refactoring-result-set? (listof refactoring-result?))] + [refactoring-result-set-modified-lines (-> refactoring-result-set? immutable-range-set?)])) + + +(require racket/sequence + rebellion/base/comparator rebellion/base/immutable-string rebellion/base/range rebellion/base/symbol + rebellion/collection/list + rebellion/collection/range-set + rebellion/streaming/transducer rebellion/type/record resyntax/private/code-snippet resyntax/private/line-replacement @@ -56,6 +68,10 @@ #:line-replacement (string-replacement->line-replacement str-replacement full-orig-code))) +(define (refactoring-result-source result) + (syntax-replacement-source (refactoring-result-syntax-replacement result))) + + (define (refactoring-result-modified-range result) (define replacement (refactoring-result-string-replacement result)) (closed-open-range (add1 (string-replacement-start replacement)) @@ -74,6 +90,33 @@ (line-replacement-start-line (refactoring-result-line-replacement result))) +(define-record-type refactoring-result-set (base-source results) + #:omit-root-binding) + + +(define (refactoring-result-set #:base-source base-source #:results results) + (define sorted-results + (transduce results (sorting #:key refactoring-result-original-line) #:into into-list)) + (constructor:refactoring-result-set #:base-source base-source #:results sorted-results)) + + +(define (refactoring-result-set-updated-source result-set) + (define replacement + (transduce (refactoring-result-set-results result-set) + (mapping refactoring-result-string-replacement) + #:into union-into-string-replacement)) + (define base (refactoring-result-set-base-source result-set)) + (define new-contents (string-apply-replacement (source->string base) replacement)) + (modified-source (source-original base) new-contents)) + + +(define (refactoring-result-set-modified-lines result-set) + (transduce (refactoring-result-set-results result-set) + (mapping refactoring-result-modified-line-range) + (filtering nonempty-range?) + #:into (into-range-set natural<=>))) + + (define (refactoring-result-original-code result) (define replacement (refactoring-result-string-replacement result)) (define full-orig-code diff --git a/private/source.rkt b/private/source.rkt index e7a3714..bde8713 100644 --- a/private/source.rkt +++ b/private/source.rkt @@ -11,6 +11,7 @@ [source->string (-> source? immutable-string?)] [source-path (-> source? (or/c path? #false))] [source-directory (-> source? (or/c path? #false))] + [source-original (-> source? unmodified-source?)] [source-read-syntax (-> source? syntax?)] [source-analyze (->* (source?) (#:lines range-set?) source-code-analysis?)] [file-source? (-> any/c boolean?)] @@ -118,6 +119,12 @@ (and path (path-only path))) +(define (source-original code) + (if (unmodified-source? code) + code + (modified-source-original code))) + + (define (source-analyze code #:lines [lines (range-set (unbounded-range #:comparator natural<=>))]) (parameterize ([current-directory (or (source-directory code) (current-directory))]) (define code-linemap (string-linemap (source->string code))) diff --git a/test/private/rackunit.rkt b/test/private/rackunit.rkt index d7082b1..4ef4ba9 100644 --- a/test/private/rackunit.rkt +++ b/test/private/rackunit.rkt @@ -120,7 +120,7 @@ (define (build-logs-info) (string-info (string-join (vector->list (build-vector logged-messages-builder)) "\n"))) - (define results + (define result-set (call-with-logs-captured (λ () (resyntax-analyze (string-source (code-block-raw-string original-program)) @@ -128,11 +128,11 @@ #:lines modified-line-mask)))) (with-check-info* - (if (empty? results) + (if (empty? (refactoring-result-set-results result-set)) '() - (list (check-info 'matched-rules (refactoring-results-matched-rules-info results)))) + (list (check-info 'matched-rules (refactoring-result-set-matched-rules-info result-set)))) (λ () - (define replacement + (define refactored-program (with-handlers ([exn:fail? (λ (e) @@ -142,17 +142,11 @@ ['exception e]) (fail-check "an error occurred while processing refactoring results")))]) (call-with-logs-captured - (λ () (transduce results - (mapping refactoring-result-string-replacement) - #:into union-into-string-replacement))))) - (define refactored-program - (string-apply-replacement (source->string - (string-source (code-block-raw-string original-program))) - replacement)) + (λ () (modified-source-contents (refactoring-result-set-updated-source result-set)))))) (with-check-info (['logs (build-logs-info)] ['actual (code-block refactored-program)] ['expected expected-program]) - (when (empty? results) + (when (empty? (refactoring-result-set-results result-set)) (fail-check "no changes were made")) (when (equal? refactored-program (code-block-raw-string original-program)) (fail-check "fixes were made, but they left the program unchanged")) @@ -188,20 +182,16 @@ (define (build-logs-info) (string-info (string-join (vector->list (build-vector logged-messages-builder)) "\n"))) - (define results + (define result-set (call-with-logs-captured (λ () (resyntax-analyze (string-source (code-block-raw-string original-program)) #:suite suite)))) - (define replacement - (transduce results - (mapping refactoring-result-string-replacement) - #:into union-into-string-replacement)) (define refactored-program - (string-apply-replacement (code-block-raw-string original-program) replacement)) + (modified-source-contents (refactoring-result-set-updated-source result-set))) (with-check-info* - (if (empty? results) + (if (empty? (refactoring-result-set-results result-set)) '() - (list (check-info 'matched-rules (refactoring-results-matched-rules-info results)))) + (list (check-info 'matched-rules (refactoring-result-set-matched-rules-info result-set)))) (λ () (with-check-info (['logs (build-logs-info)] ['actual (code-block refactored-program)] @@ -210,12 +200,15 @@ (fail-check "expected no changes, but changes were made"))) (with-check-info (['logs (build-logs-info)] ['actual (code-block refactored-program)]) - (unless (empty? results) + (unless (empty? (refactoring-result-set-results result-set)) (fail-check "the program was not changed, but no-op fixes were suggested")))))) -(define (refactoring-results-matched-rules-info results) - (define matches (transduce results (mapping refactoring-result-rule-name) #:into into-multiset)) +(define (refactoring-result-set-matched-rules-info result-set) + (define matches + (transduce (refactoring-result-set-results result-set) + (mapping refactoring-result-rule-name) + #:into into-multiset)) (nested-info (transduce (in-hash-entries (multiset-frequencies matches)) (mapping-values