-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathrefactoring-rules.scrbl
More file actions
361 lines (291 loc) · 17.3 KB
/
Copy pathrefactoring-rules.scrbl
File metadata and controls
361 lines (291 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#lang scribble/manual
@(require (for-label racket/base
resyntax/base
resyntax/default-recommendations
syntax/parse
syntax/parse/define)
scribble/example
(submod resyntax/private/scribble-evaluator-factory doc))
@(define make-evaluator
(make-module-sharing-evaluator-factory
#:public (list 'resyntax/base
'syntax/parse)
#:private (list 'racket/base)))
@title[#:tag "refactoring-rules"]{Refactoring Rules and Suites}
@defmodule[resyntax/base]
Resyntax derives its suggestions from @tech{refactoring rules}, which can be grouped into a
@deftech{refactoring suite}. Resyntax ships with a default refactoring suite consisting of many rules
that cover various scenarios related to Racket's standard libraries. However, you may also define your
own refactoring suite and rules using the forms below. Knowledge of Racket macros, and of
@racket[syntax-parse] in particular, is especially useful for understanding how to create effective
refactoring rules.
@defproc[(refactoring-rule? [v any/c]) boolean?]{
A predicate that recognizes @tech{refactoring rules}.}
@defproc[(refactoring-suite? [v any/c]) boolean?]{
A predicate that recognizes @tech{refactoring suites}.}
@defform[(define-refactoring-rule id
#:description description
parse-option ...
syntax-pattern
pattern-directive ...
template)
#:contracts ([description string?])]{
Defines a @tech{refactoring rule} named @racket[id]. Refactoring rules are defined in terms of
@racket[syntax-parse]. The rule matches syntax objects that match @racket[syntax-pattern], and
@racket[template] is a @racket[syntax] template that defines what the matched code is refactored
into. The message in @racket[description] is presented to the user when Resyntax makes a suggestion
based on the rule. Refactoring rules function roughly like macros defined with
@racket[define-syntax-parse-rule]. For example, here is a simple rule that flattens nested
@racket[or] expressions:
@(examples
#:eval (make-evaluator) #:once
(define-refactoring-rule nested-or-to-flat-or
#:description "This nested `or` expression can be flattened."
#:literals (or)
(or a (or b c))
(or a b c)))
Like @racket[syntax-parse] and @racket[define-syntax-parse-rule],
@tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{pattern directives} can be used to aid in
defining rules. Here is a rule that uses the @racket[#:when] directive to only refactor @racket[or]
expressions that have a duplicate condition:
@(examples
#:eval (make-evaluator) #:once
(define-refactoring-rule or-with-duplicate-subterm
#:description "This `or` expression has a duplicate subterm."
#:literals (or)
(or before ... a:id between ... b:id after ...)
#:when (free-identifier=? #'a #'b)
(or before ... a between ... after ...)))}
@defform[(define-definition-context-refactoring-rule id
#:description description
parse-option ...
syntax-pattern
pattern-directive ...
template)
#:contracts ([description string?])]{
Defines a @tech{refactoring rule} named @racket[id], like @racket[define-refactoring-rule], except
the rule is applied only in
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{internal-definition contexts}. The given
@racket[syntax-pattern] must be a
@tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{proper head pattern}, and it is expected to
match the entire sequence of body forms within the definition context. The output @racket[template]
of the rule should be a single syntax object containing a sequence of refactored body forms. Like
@racket[define-refactoring-rule], @racket[description] is used to generate a message presented to the
user, and both @racket[parse-option] and @racket[pattern-directive] function the same as they do in
@racket[syntax-parse]. For example, here is a simple rule that turns a series of @racket[define]
forms unpacking a 2D @racket[point] structure into a single @racket[match-define] form:
@(examples
#:eval (make-evaluator) #:once
(eval:no-prompt
(struct point (x y) #:transparent))
(define-definition-context-refactoring-rule point-define-to-match-define
#:description "These definitions can be simplified with `match-define`."
#:literals (define point-x point-y)
(~seq body-before ...
(define x:id (point-x pt:id))
(define y:id (point-y pt2:id))
body-after ...)
#:when (free-identifier=? #'pt #'pt2)
(body-before ...
(match-define (point x y) pt)
body-after ...)))
Note that by default Resyntax will try to reformat the entire context. To reformat just the forms
being modified, a few additional steps are required. First, use @racket[~replacement] (or
@racket[~splicing-replacement]) to annotate which subpart of the context is being replaced:
@(examples
#:eval (make-evaluator) #:once
(define-definition-context-refactoring-rule point-define-to-match-define
#:description "These definitions can be simplified with `match-define`."
#:literals (define point-x point-y)
(~seq body-before ...
(~and x-def (define x:id (point-x pt:id)))
(~and y-def (define y:id (point-y pt2:id)))
body-after ...)
#:when (free-identifier=? #'pt #'pt2)
(body-before ...
(~replacement (match-define (point x y) pt)
#:original-splice (x-def y-def))
body-after ...)))
This ensures that Resyntax will preserve any comments at the end of @racket[body-before ...] and the
beginning of @racket[body-after ...]. However, that alone doesn't prevent Resyntax from reformatting
the whole context. To do that, use the @racket[~focus-replacement-on] metafunction, which tells
Resyntax that if @emph{only} the focused forms are changed, Resyntax should "shrink" the replacement
it generates down to just those forms and not reformat anything in the replacement syntax object
that's outside of the focused syntax:
@(examples
#:eval (make-evaluator) #:once
(define-definition-context-refactoring-rule point-define-to-match-define
#:description "These definitions can be simplified with `match-define`."
#:literals (define point-x point-y)
(~seq body-before ...
(~and x-def (define x:id (point-x pt:id)))
(~and y-def (define y:id (point-y pt2:id)))
body-after ...)
#:when (free-identifier=? #'pt #'pt2)
(body-before ...
(~focus-replacement-on
(~replacement (match-define (point x y) pt)
#:original-splice (x-def y-def)))
body-after ...)))}
@defform[(define-refactoring-suite id rules-list suites-list)
#:grammar
[(rules-list (code:line)
(code:line #:rules (rule ...)))
(suites-list (code:line)
(code:line #:suites (suite ...)))]
#:contracts ([rule refactoring-rule?]
[suite refactoring-suite?])]{
Defines a @tech{refactoring suite} named @racket[id] containing each listed @racket[rule].
Additionally, each @racket[suite] provided has its rules added to the newly defined suite.
@(examples
#:eval (make-evaluator) #:once
(eval:alts
(define-refactoring-suite my-suite
#:rules (rule1 rule2 rule3)
#:suites (subsuite1 subsuite2))
(void)))}
@section[#:tag "comment-preservation"]{Exercising Fine Control Over Comments}
Writing a rule with @racket[define-refactoring-rule] is usually enough for Resyntax to handle
commented code without issue, but in certain cases more precise control is desired. For instance,
consider the @racketidfont{nested-or-to-flat-or} rule from earlier:
@(racketblock
(define-refactoring-rule nested-or-to-flat-or
#:description "This nested `or` expression can be flattened."
#:literals (or)
(or a (or b c))
(or a b c)))
As-is, this rule will @emph{fail} to refactor the following code:
@(racketblock
(or (foo ...)
(code:comment @#,elem{If that doesn't work, fall back to other approaches})
(or (bar ...)
(baz ...))))
Resyntax rejects the rule because applying it would produce this code, which loses the comment:
@(racketblock
(or (foo ...)
(bar ...)
(baz ...)))
Resyntax is unable to preserve the comment automatically. Resyntax can preserve some comments without
programmer effort, but only in specific circumstances:
@itemlist[
@item{Comments @emph{within} expressions that the rule left unchanged are preserved. If the comment
were inside @racket[(foo ...)], @racket[(bar ...)], or @racket[(baz ...)], it would have been kept.}
@item{Comments @emph{between} unchanged expressions are similarly preserved. If the comment were
between @racket[(bar ...)] and @racket[(baz ...)], it would have been kept.}]
To fix this issue, rule authors can inject some extra markup into their suggested replacements using
@tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{template metafunctions} provided by Resyntax. In
the case of @racketidfont{nested-or-to-flat-or}, we can use the @racket[~splicing-replacement]
metafunction to indicate that the nested @racket[or] expression should be considered @emph{replaced}
by its nested subterms:
@(racketblock
(define-refactoring-rule nested-or-to-flat-or
#:description "This nested `or` expression can be flattened."
#:literals (or)
(or a (~and nested-or (or b c)))
#:with (nested-subterm ...) #'(~splicing-replacement (b c) #:original nested-or)
(or a nested-subterm ...)))
This adds @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{syntax properties} to the nested
subterms that allow Resyntax to preserve the comment, producing this output:
@(racketblock
(or (foo ...)
(code:comment @#,elem{If that doesn't work, fall back to other approaches})
(bar ...)
(baz ...)))
When Resyntax sees that the @racket[(bar ...)] nested subterm comes immediately after the
@racket[(foo ...)] subterm, it notices that @racket[(bar ...)] has been annotated with replacement
properties. Then Resyntax observes that @racket[(bar ...)] is the first expression of a sequence of
expressions that replaces the @racket[or] expression which originally followed @racket[(foo ...)].
Based on this observation, Resyntax decides to preserve whatever text was originally between
@racket[(foo ...)] and the nested @racket[or] expression. This mechanism, exposed via
@racket[~replacement] and @racket[~splicing-replacement], offers a means for refactoring rules to
guide Resyntax's internal comment preservation system when the default behavior is not sufficient.
For an explanation of how that system works under the hood, see @secref["original-syntax-paths"] in
The Resyntax Grimoire.
@defform[#:kind "template metafunction"
(~replacement replacement-form original)
#:grammar
([original
(code:line #:original original-form)
(code:line #:original-splice (original-form ...))])]{
A @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{template metafunction} for use in
@tech{refactoring rules}. The result of the metafunction is just the @racket[#'replacement-form]
syntax object, except with some
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{syntax properties} added. Those
properties inform Resyntax that this syntax object should be considered a replacement for
@racket[original-form] (or in the splicing case, for the unparenthesized sequence
@racket[original-form ...]). Resyntax uses this information to preserve comments and formatting near
the original form(s).}
@defform[#:kind "template metafunction"
(~splicing-replacement (replacement-form ...)
original)
#:grammar
([original
(code:line #:original original-form)
(code:line #:original-splice (original-form ...))])]{
A @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{template metafunction} for use in
@tech{refactoring rules}. The result of the metafunction is the syntax object
@racket[#'(replacement-form ...)], except with some
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{syntax properties} added. Those
properties inform Resyntax that the replacement syntax objects --- as an unparenthesized sequence ---
should be considered a replacement for @racket[original-form] (or @racket[original-form ...]).
Resyntax uses this information to preserve comments and formatting near the original form(s).}
@section[#:tag "replacement-focusing"]{Narrowing the Focus of Replacements}
@defform[#:kind "template metafunction"
(~focus-replacement-on replacement-form)]{
A @tech[#:doc '(lib "syntax/scribblings/syntax.scrbl")]{template metafunction} for use in
@tech{refactoring rules}. The result of the metafunction is just the @racket[#'replacement-form]
syntax object, except with some
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{syntax properties} added. Those
properties inform Resyntax that the returned syntax object should be treated as the @emph{focus} of
the entire refactoring rule's generated replacement. When a refactoring rule produces a replacement
that has a focus, Resyntax checks that nothing outside the focus was modified. If this is the case,
then Resyntax will @emph{shrink} the replacement it generates to only touch the focus. Crucially,
this means Resyntax will @emph{only reformat the focused code}, not the entire generated replacement.
This metafunction is frequently used with @racket[define-definition-context-refactoring-rule],
because such rules often touch only a small series of forms in a much larger definition context.}
@section{Resyntax's Default Rules}
@defmodule[resyntax/default-recommendations]
@(define default-recommendations-directory-link
"https://github.com/jackfirth/resyntax/tree/master/default-recommendations")
@defthing[default-recommendations refactoring-suite?]{
The refactoring suite containing all of Resyntax's default refactoring rules. These rules are further
broken up into subsuites, with each subsuite corresponding to a module within the
@racketmodname[resyntax/default-recommendations] collection. For example, all of Resyntax's rules
related to @racket[for] loops are located in the
@racketmodfont{resyntax/default-recommendations/for-loop-shortcuts} module. See
@hyperlink[default-recommendations-directory-link]{this directory} for all of Resyntax's default
refactoring rules.}
@section{What Makes a Good Refactoring Rule?}
If you'd like to add a new @tech{refactoring rule} to Resyntax, there are a few guidelines to keep in
mind:
@itemlist[
@item{Refactoring rules should be @emph{safe}. Resyntax shouldn't break users' code, and it shouldn't
require careful review to determine whether a suggestion from Resyntax is safe to apply. It's better
for a rule to never make suggestions than to occasionally make broken suggestions.}
@item{Refactoring rules can be shown to many different developers in a wide variety of different
contexts. Therefore, it's important that Resyntax's default recommendations have some degree of
@emph{consensus} among the Racket community. Highly divisive suggestions that many developers
disagree with are not a good fit for Resyntax. Technology is social before it is technical:
discussing your rule with the Racket community prior to developing it is encouraged, especially if
it's likely to affect a lot of code. If necessary, consider narrowing the focus of your rule to just
the cases that everybody agrees are clear improvements.}
@item{Refactoring rules should @emph{explain themselves}. The description of a refactoring rule (as
specified with the @racket[#:description] option) should state why the new code is an improvement
over the old code. Refactoring rule descriptions are shown to Resyntax users at the command line, in
GitHub pull request review comments, and in Git commit messages. The description is the only means
you have of explaining to a potentially confused stranger why Resyntax wants to change their code,
so make sure you use it!}
@item{Refactoring rules should focus on cleaning up @emph{real-world code}. A refactoring rule that
suggests improvements to hypothetical code that no human would write in the first place is not
useful. Try to find examples of code "in the wild" that the rule would improve. The best candidates
for new rules tend to be rules that help Racketeers clean up and migrate old Scheme code that
doesn't take advantage of Racket's unique features and extensive standard library.}
@item{Refactoring rules should try to preserve the @emph{intended behavior} of the refactored code,
but not necessarily the @emph{actual behavior}. For instance, a rule that changes how code handles
some edge case is acceptable if the original behavior of the code was likely confusing or surprising
to the developer who wrote it. This is a judgment call that requires understanding what the original
code communicates clearly and what it doesn't. A rule's @racket[#:description] is an excellent place
to draw attention to potentially surprising behavior changes.}
@item{Refactoring rules should be @emph{self-contained}, meaning they can operate locally on a single
expression. Refactoring rules that require whole-program analysis are not a good fit for Resyntax,
nor are rules that require global knowledge of the whole codebase.}]