• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jackfirth / resyntax / #350

13 Nov 2025 08:15AM UTC coverage: 93.268% (-0.4%) from 93.661%
#350

Pull #754

cover

Copilot
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>
Pull Request #754: Add support for warning-only refactoring rules

179 of 322 new or added lines in 10 files covered. (55.59%)

1 existing line in 1 file now uncovered.

15378 of 16488 relevant lines covered (93.27%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.37
/cli.rkt
1
#lang racket/base
1✔
2

3

4
(require fancy-app
1✔
5
         json
1✔
6
         racket/cmdline
1✔
7
         racket/format
1✔
8
         racket/logging
1✔
9
         racket/match
1✔
10
         racket/path
1✔
11
         racket/port
1✔
12
         rebellion/base/comparator
1✔
13
         rebellion/base/range
1✔
14
         rebellion/collection/entry
1✔
15
         rebellion/collection/hash
1✔
16
         rebellion/collection/list
1✔
17
         rebellion/collection/multiset
1✔
18
         rebellion/collection/range-set
1✔
19
         rebellion/collection/vector/builder
1✔
20
         rebellion/streaming/reducer
1✔
21
         rebellion/streaming/transducer
1✔
22
         rebellion/type/enum
1✔
23
         rebellion/type/record
1✔
24
         resyntax
1✔
25
         resyntax/base
1✔
26
         resyntax/default-recommendations
1✔
27
         resyntax/private/file-group
1✔
28
         resyntax/private/github
1✔
29
         resyntax/private/refactoring-result
1✔
30
         resyntax/private/source
1✔
31
         resyntax/private/string-indent
1✔
32
         resyntax/private/syntax-replacement)
1✔
33

34

35
;@----------------------------------------------------------------------------------------------------
36

37

38
(define-enum-type resyntax-output-format (plain-text github-pull-request-review git-commit-message json))
1✔
39
(define-enum-type resyntax-fix-method (modify-files create-multiple-git-commits))
1✔
40
(define-record-type resyntax-analyze-options (targets suite output-format output-destination analyzer-timeout-ms))
1✔
41

42

43
(define-record-type resyntax-fix-options
1✔
44
  (targets
1✔
45
   suite
1✔
46
   fix-method
1✔
47
   output-format
1✔
48
   max-fixes
1✔
49
   max-modified-files
1✔
50
   max-modified-lines
1✔
51
   max-pass-count
1✔
52
   analyzer-timeout-ms))
1✔
53

54

55
(define all-lines (range-set (unbounded-range #:comparator natural<=>)))
1✔
56

57

58
(define (resyntax-analyze-parse-command-line)
1✔
59
  (define targets (make-vector-builder))
×
60
  (define suite default-recommendations)
×
61
  (define selected-rule #false)
×
62
  (define output-format plain-text)
×
63
  (define output-destination 'console)
×
64
  (define analyzer-timeout-ms 10000)
×
65

66
  (command-line
1✔
67
   #:program "resyntax analyze"
×
68

69
   #:multi
1✔
70

71
   ("--file"
1✔
72
    filepath
1✔
73
    "A file to analyze."
×
74
    (vector-builder-add targets (single-file-group filepath all-lines)))
×
75

76
   ("--directory"
1✔
77
    dirpath
1✔
78
    "A directory to anaylze, including subdirectories."
×
79
    (vector-builder-add targets (directory-file-group dirpath)))
×
80

81
   ("--package"
1✔
82
    pkgname
1✔
83
    "An installed package to analyze."
×
84
    (vector-builder-add targets (package-file-group pkgname)))
×
85

86
   ("--local-git-repository"
1✔
87
    repopath baseref
1✔
88
    "A Git repository to search for modified files to analyze. The repopath argument is a directory
×
89
path to the root of a Git repository, and the baseref argument is a Git reference (in the form \
×
90
\"remotename/branchname\") to use as the base state of the repository. Any files that have been \
×
91
changed relative to baseref are analyzed."
×
92
    (vector-builder-add targets (git-repository-file-group repopath baseref)))
×
93

94
   #:once-each
1✔
95

96
   ("--refactoring-suite"
1✔
97
    modpath
1✔
98
    suite-name
1✔
99
    "The refactoring suite to analyze code with."
×
100
    (define parsed-modpath (read (open-input-string modpath)))
×
101
    (define parsed-suite-name (read (open-input-string suite-name)))
×
102
    (set! suite (dynamic-require parsed-modpath parsed-suite-name)))
×
103

104
   ("--refactoring-rule"
1✔
105
    rule-name
1✔
106
    "Only use this refactoring rule from the refactoring suite, instead of all of the suite's rules."
×
107
    (define rule-sym (string->symbol rule-name))
×
108
    (set! selected-rule rule-sym))
×
109

110
   ("--analyzer-timeout"
1✔
111
    timeout-ms
1✔
112
    "The timeout in milliseconds for expansion analyzers. Defaults to 10000 (10 seconds)."
×
113
    (set! analyzer-timeout-ms (string->number timeout-ms)))
×
114

115
   ("--output-to-file"
1✔
116
    outputpath
1✔
117
    "Store results in a file instead of printing them to the console."
×
118
    (set! output-destination (simple-form-path outputpath)))
×
119

120
   ("--output-as-github-review"
1✔
121
    "Report results by leaving a GitHub review on the pull request currently being analyzed, as \
×
122
determined by the GITHUB_REPOSITORY and GITHUB_REF environment variables."
×
123
    (set! output-format github-pull-request-review)))
×
124

125
  (when selected-rule
×
126
    (define filtered-suite
×
127
      (refactoring-suite
×
128
       #:name (object-name suite)
×
129
       #:rules (for/list ([rule (in-list (refactoring-suite-rules suite))]
×
130
                          #:when (equal? (object-name rule) selected-rule))
×
131
                 rule)))
×
132
    (set! suite filtered-suite))
×
133
  
134
  (resyntax-analyze-options
×
135
   #:targets (build-vector targets)
×
136
   #:suite suite
×
137
   #:output-format output-format
×
138
   #:output-destination output-destination
×
139
   #:analyzer-timeout-ms analyzer-timeout-ms))
×
140

141

142
(define (resyntax-fix-parse-command-line)
1✔
143
  (define targets (make-vector-builder))
×
144
  (define suite default-recommendations)
×
145
  (define selected-rule #false)
×
146
  (define (add-target! target)
×
147
    (vector-builder-add targets target))
×
148
  (define fix-method modify-files)
×
149
  (define output-format plain-text)
×
150
  (define max-fixes +inf.0)
×
151
  (define max-pass-count 10)
×
152
  (define max-modified-files +inf.0)
×
153
  (define max-modified-lines +inf.0)
×
154
  (define analyzer-timeout-ms 10000)
×
155

156
  (command-line
1✔
157
   #:program "resyntax fix"
×
158

159
   #:multi
1✔
160

161
   ("--file" filepath "A file to fix." (add-target! (single-file-group filepath all-lines)))
×
162

163
   ("--directory"
1✔
164
    dirpath
1✔
165
    "A directory to fix, including subdirectories."
×
166
    (add-target! (directory-file-group dirpath)))
×
167
   
168
   ("--package"
1✔
169
    pkgname
1✔
170
    "An installed package to fix."
×
171
    (add-target! (package-file-group pkgname)))
×
172
   
173
   ("--local-git-repository"
1✔
174
    repopath baseref
1✔
175
    "A Git repository to search for modified files to fix. The repopath argument is a directory
×
176
path to the root of a Git repository, and the baseref argument is a Git reference (in the form \
×
177
\"remotename/branchname\") to use as the base state of the repository. Any files that have been \
×
178
changed relative to baseref are analyzed and fixed."
×
179
    (add-target! (git-repository-file-group repopath baseref)))
×
180

181
   #:once-each
1✔
182

183
   ("--create-multiple-commits"
1✔
184
    "Modify files by creating a series of individual Git commits."
×
185
    (set! fix-method create-multiple-git-commits))
×
186

187
   ("--output-as-commit-message"
1✔
188
    "Report results in the form of a Git commit message printed to stdout."
×
189
    (set! output-format git-commit-message))
×
190

191
   ("--output-as-json"
1✔
192
    "Report results in the form of a JSON object printed to stdout."
×
193
    (set! output-format json))
×
194

195
   ("--refactoring-suite"
1✔
196
    modpath
1✔
197
    suite-name
1✔
198
    "The refactoring suite to analyze code with."
×
199
    (define parsed-modpath (read (open-input-string modpath)))
×
200
    (define parsed-suite-name (read (open-input-string suite-name)))
×
201
    (set! suite (dynamic-require parsed-modpath parsed-suite-name)))
×
202

203
   ("--refactoring-rule"
1✔
204
    rule-name
1✔
205
    "Only use this refactoring rule from the refactoring suite, instead of all of the suite's rules."
×
206
    (define rule-sym (string->symbol rule-name))
×
207
    (set! selected-rule rule-sym))
×
208

209
   ("--analyzer-timeout"
1✔
210
    timeout-ms
1✔
211
    "The timeout in milliseconds for expansion analyzers. Defaults to 10000 (10 seconds)."
×
212
    (set! analyzer-timeout-ms (string->number timeout-ms)))
×
213

214
   ("--max-pass-count"
1✔
215
    passcount
1✔
216
    "The maximum number of times Resyntax will fix each file. By default, Resyntax runs at most 10 \
×
217
passes over each file (or fewer, if no fixes would be made by additional passes). Multiple passes \
×
218
are needed when applying a fix unlocks further fixes."
×
219
    (set! max-pass-count (string->number passcount)))
×
220

221
   ("--max-fixes"
1✔
222
    fixlimit
1✔
223
    "The maximum number of fixes to apply. If not specified, all fixes found will be applied."
×
224
    (set! max-fixes (string->number fixlimit)))
×
225

226
   ("--max-modified-files"
1✔
227
    modifiedlimit
1✔
228
    "The maximum number of files to modify. If not specified, fixes will be applied to all files."
×
229
    (set! max-modified-files (string->number modifiedlimit)))
×
230

231
   ("--max-modified-lines"
1✔
232
    modifiedlines
1✔
233
    "The maximum number of lines to modify. If not specified, no line limit is applied."
×
234
    (set! max-modified-lines (string->number modifiedlines))))
×
235

236
  (when selected-rule
×
237
    (define filtered-suite
×
238
      (refactoring-suite
×
239
       #:name (object-name suite)
×
240
       #:rules (for/list ([rule (in-list (refactoring-suite-rules suite))]
×
241
                          #:when (equal? (object-name rule) selected-rule))
×
242
                 rule)))
×
243
    (set! suite filtered-suite))
×
244

245
  (resyntax-fix-options #:targets (build-vector targets)
×
246
                        #:suite suite
×
247
                        #:fix-method fix-method
×
248
                        #:output-format output-format
×
249
                        #:max-fixes max-fixes
×
250
                        #:max-modified-files max-modified-files
×
251
                        #:max-modified-lines max-modified-lines
×
252
                        #:max-pass-count max-pass-count
×
253
                        #:analyzer-timeout-ms analyzer-timeout-ms))
×
254

255

256
(define (resyntax-run)
1✔
257
  (command-line
1✔
258
   #:program "resyntax"
×
259

260
   #:usage-help
1✔
261
   "\n<command> is one of
×
262

263
\tanalyze
×
264
\tfix
×
265

266
For help on these, use 'analyze --help' or 'fix --help'."
×
267

268
   #:ps "\nSee https://docs.racket-lang.org/resyntax/index.html for details."
×
269
   #:args (command . leftover-args)
1✔
270
   (define leftover-arg-vector (vector->immutable-vector (list->vector leftover-args)))
×
271

272
   (define (call-command command-thunk)
×
273
     (parameterize ([current-command-line-arguments leftover-arg-vector])
×
274
       (with-logging-to-port (current-error-port)
×
275
         command-thunk
×
276
         #:logger (current-logger)
×
277
         'info 'resyntax
×
278
         'error)))
×
279

280
   (match command
×
281
     ["analyze" (call-command resyntax-analyze-run)]
×
282
     ["fix" (call-command resyntax-fix-run)])))
×
283

284

285
(define (resyntax-analyze-run)
1✔
286
  (define options (resyntax-analyze-parse-command-line))
×
287
  (define sources (file-groups-resolve (resyntax-analyze-options-targets options)))
×
288
  (define analysis
×
289
    (resyntax-analyze-all sources
×
290
                          #:suite (resyntax-analyze-options-suite options)
×
291
                          #:max-passes 1
×
292
                          #:timeout-ms (resyntax-analyze-options-analyzer-timeout-ms options)))
×
293
  (define results
×
294
    (transduce (resyntax-analysis-all-results analysis)
×
295
               (append-mapping in-hash-values)
×
296
               (append-mapping refactoring-result-set-results)
×
297
               #:into into-list))
×
298

299
  (define (display-results)
×
300
    (match (resyntax-analyze-options-output-format options)
×
301
      [(== plain-text)
×
302
       (for ([result (in-list results)])
×
NEW
303
         (define source (refactoring-result-source result))
×
NEW
304
         (define path (file-source-path source))
×
305
         (define line (refactoring-result-original-line result))
×
306
         (define column (refactoring-result-original-column result))
×
307
         (printf "resyntax: ~a:~a:~a [~a]\n" path line column (refactoring-result-rule-name result))
×
308
         (printf "\n\n~a\n" (string-indent (refactoring-result-message result) #:amount 2))
×
309
         (define old-code (refactoring-result-original-code result))
×
310
         (define new-code (refactoring-result-new-code result))
×
NEW
311
         (if new-code
×
NEW
312
             (printf "\n\n~a\n\n\n~a\n\n\n"
×
NEW
313
                     (string-indent (~a old-code) #:amount 2)
×
NEW
314
                     (string-indent (~a new-code) #:amount 2))
×
NEW
315
             (printf "\n\n~a\n\n\n"
×
NEW
316
                     (string-indent (~a old-code) #:amount 2))))]
×
317
      [(== github-pull-request-review)
×
318
       (define req (refactoring-results->github-review results #:file-count (hash-count sources)))
×
319
       (write-json (github-review-request-jsexpr req))]))
×
320

321
  (match (resyntax-analyze-options-output-destination options)
×
322
    ['console
×
323
     (displayln "resyntax: --- displaying results ---")
×
324
     (display-results)]
×
325
    [(? path? output-path)
×
326
     (displayln "resyntax: --- writing results to file ---")
×
327
     (with-output-to-file output-path display-results #:mode 'text)]))
×
328

329

330
(define (resyntax-fix-run)
1✔
331
  (define options (resyntax-fix-parse-command-line))
×
332
  (define fix-method (resyntax-fix-options-fix-method options))
×
333
  (define output-format (resyntax-fix-options-output-format options))
×
334
  (define sources (file-groups-resolve (resyntax-fix-options-targets options)))
×
335
  (define max-modified-files (resyntax-fix-options-max-modified-files options))
×
336
  (define max-modified-lines (resyntax-fix-options-max-modified-lines options))
×
337
  (define analysis
×
338
    (resyntax-analyze-all sources
×
339
                          #:suite (resyntax-fix-options-suite options)
×
340
                          #:max-fixes (resyntax-fix-options-max-fixes options)
×
341
                          #:max-passes (resyntax-fix-options-max-pass-count options)
×
342
                          #:max-modified-sources max-modified-files
×
343
                          #:max-modified-lines max-modified-lines
×
344
                          #:timeout-ms (resyntax-fix-options-analyzer-timeout-ms options)))
×
345
  (match fix-method
×
346
    [(== modify-files)
×
347
     (resyntax-analysis-write-file-changes! analysis)]
×
348
    [(== create-multiple-git-commits)
×
349
     (resyntax-analysis-commit-fixes! analysis)])
×
350
  (match output-format
×
351
    [(== git-commit-message)
×
352
     (resyntax-fix-print-git-commit-message analysis)]
×
353
    [(== json)
×
354
     (resyntax-fix-print-json analysis)]
×
355
    [(== plain-text)
×
356
     (resyntax-fix-print-plain-text-summary analysis)]))
×
357

358

359
(define (resyntax-fix-print-git-commit-message analysis)
1✔
360
  (define total-fixes (resyntax-analysis-total-fixes analysis))
×
361
  (define total-files (resyntax-analysis-total-sources-modified analysis))
×
362
  (define fix-counts-by-rule
×
363
    (transduce (in-hash-entries (multiset-frequencies (resyntax-analysis-rules-applied analysis)))
×
364
               (sorting #:key entry-value #:descending? #true)
×
365
               #:into into-list))
×
366
  (define issue-string (if (> total-fixes 1) "issues" "issue"))
×
367
  (define file-string (if (> total-files 1) "files" "file"))
×
368
  (if (zero? total-fixes)
×
369
      (displayln "Resyntax found no issues.")
×
370
      (printf "Resyntax fixed ~a ~a in ~a ~a.\n\n" total-fixes issue-string total-files file-string))
×
371
  (for ([rule+count (in-list fix-counts-by-rule)])
×
372
    (match-define (entry rule count) rule+count)
×
373
    (define occurrence-string (if (> count 1) "occurrences" "occurrence"))
×
374
    (printf "  * Fixed ~a ~a of `~a`\n" count occurrence-string rule))
×
375
  (unless (zero? total-fixes)
×
376
    (newline)))
×
377

378

379
(define (resyntax-fix-print-plain-text-summary analysis)
1✔
380
  (displayln "resyntax: --- summary ---\n")
×
381
  (define total-fixes (resyntax-analysis-total-fixes analysis))
×
382
  (define total-files (resyntax-analysis-total-sources-modified analysis))
×
383
  (define message
×
384
    (cond
×
385
      [(zero? total-fixes) "No issues found."]
×
386
      [(equal? total-fixes 1) "Fixed 1 issue in 1 file."]
×
387
      [(equal? total-files 1) (format "Fixed ~a issues in 1 file." total-fixes)]
×
388
      [else (format "Fixed ~a issues in ~a files." total-fixes total-files)]))
×
389
  (printf "  ~a\n\n" message)
×
390
  (define rules-applied (resyntax-analysis-rules-applied analysis))
×
391
  (transduce (in-hash-entries (multiset-frequencies rules-applied))
×
392
             (sorting #:key entry-value #:descending? #true)
×
393
             (mapping
×
394
              (λ (e)
×
395
                (match-define (entry rule-name rule-fixes) e)
×
396
                (define message
×
397
                  (if (equal? rule-fixes 1)
×
398
                      (format "Fixed 1 occurrence of ~a" rule-name)
×
399
                      (format "Fixed ~a occurrences of ~a" rule-fixes rule-name)))
×
400
                (format "  * ~a\n" message)))
×
401
             #:into (into-for-each display))
×
402
  (when (positive? total-fixes)
×
403
    (newline)))
×
404

405

406
(define (resyntax-fix-print-json analysis)
1✔
407
  (define total-fixes (resyntax-analysis-total-fixes analysis))
×
408
  (define total-files (resyntax-analysis-total-sources-modified analysis))
×
409
  (define fix-counts-by-rule
×
410
    (transduce (in-hash-entries (multiset-frequencies (resyntax-analysis-rules-applied analysis)))
×
411
               (sorting #:key entry-value #:descending? #true)
×
412
               #:into into-list))
×
413
  
414
  ;; Build commit message
415
  (define commit-message
×
416
    (with-output-to-string
×
417
      (λ ()
×
418
        (define issue-string (if (> total-fixes 1) "issues" "issue"))
×
419
        (define file-string (if (> total-files 1) "files" "file"))
×
420
        (if (zero? total-fixes)
×
421
            (display "Resyntax found no issues.")
×
422
            (printf "Resyntax fixed ~a ~a in ~a ~a." 
×
423
                    total-fixes issue-string total-files file-string))
×
424
        (unless (zero? total-fixes)
×
425
          (newline)
×
426
          (for ([rule+count (in-list fix-counts-by-rule)])
×
427
            (match-define (entry rule count) rule+count)
×
428
            (define occurrence-string (if (> count 1) "occurrences" "occurrence"))
×
429
            (printf "\n  * Fixed ~a ~a of `~a`" count occurrence-string rule))))))
×
430
  
431
  ;; Output JSON
432
  (write-json
×
433
   (hasheq 'commit_message_body commit-message
×
434
           'fix_count total-fixes))
×
435
  (newline))
×
436

437

438
(module+ main
439
  (resyntax-run))
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc