-
Notifications
You must be signed in to change notification settings - Fork 0
/
refactor.go
464 lines (412 loc) · 12.9 KB
/
refactor.go
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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
package main
// This file defines the refactoring.
// TODO(adonovan): fix:
// - exported API functions may be moved into internal subpackages,
// making them invisible. We'll need shims/delegates for func and const.
// Types and vars are trickier.
// - use nice import names (e.g. core not _core) when it would be unambiguous to do so.
// - preserve comments before/in import decls.
// - look at files for non-linux/amd64 platforms
// - deal with assembly, compiler entrypoints
// - check for all conflicts: struct fields, concrete methods, interface methods.
// - check for definition conflicts at file scope
// - check for field definition conflicts
// - check for (abstract and concrete) method definition conflicts
// - check for renamed package-level types used as embedded fields, etc.
// - check for reference conflicts (hard)
// - emit 'git mv' commands so that new files are treated as moves, not adds.
// - struct literals T{1,2} may need field names T{X:1, Y:2}.
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"go/types"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strings"
"unicode"
"unicode/utf8"
)
func (o *organizer) refactor(clusters []*cluster) error {
// new names for objects that must become exported
exportNames := make(map[types.Object]string)
export := func(obj types.Object) {
if !ast.IsExported(obj.Name()) {
if _, ok := exportNames[obj]; !ok {
exportNames[obj] = exportedName(obj.Name())
}
}
}
// Find objects requiring a name change for export:
// the heads of node-graph edges that span clusters.
for _, n := range o.nodes {
for succ := range n.succs {
if n.cluster != succ.cluster {
if !succ.mustExport {
succ.mustExport = true
for _, obj := range succ.objects {
export(obj)
}
}
}
}
}
// Fix up package-level definition conflicts in each cluster.
for _, c := range clusters {
// For now, all import names will be "_" + the last segment.
// TODO(adonovan): avoid _ when not needed and make sure
// the last segment is a valid identifier.
// Alternatively, apply gorename on a file-by-file basis
// to eliminate the underscores.
c.name = "_" + path.Base(c.importPath) // (default)
c.scope = make(map[string]*node)
for n := range c.nodes {
for _, obj := range n.objects {
if !isPackageLevel(obj) {
continue
}
// NB: only exported symbols may conflict.
// That may change when we deal with imports.
name := obj.Name()
if new, ok := exportNames[obj]; ok {
name = new
}
if prev := c.scope[name]; prev != nil {
fmt.Fprintf(os.Stderr, "%s: warning: exporting %s\n",
o.fset.Position(n.syntax.Pos()),
obj.Name())
fmt.Fprintf(os.Stderr, "%s: \twould conflict with %s; adding 'X' prefix.\n",
o.fset.Position(prev.syntax.Pos()), name)
// TODO(adonovan): fix: use a unique prefix
// that never appears in the package!
name = "X" + name
exportNames[obj] = name
}
c.scope[name] = n
}
}
}
// Mark selectables (fields and methods) for export if they
// are ever referenced from outside their defining package.
// TODO(adonovan): fix: must compute consequences (a la gorename).
for _, n := range o.nodes {
for _, obj := range n.uses {
if v, ok := obj.(*types.Var); ok && v.IsField() {
// field
} else if f, ok := obj.(*types.Func); ok && methodRecv(f) != nil {
// method
} else {
continue
}
// obj is a field or method
// inter-cluster reference?
if o.nodesByObj[obj].cluster != n.cluster {
export(obj)
}
}
}
// Inspect referring identifiers within each node.
// Compute import dependencies (existing and new packages).
// Qualify inter-cluster references with the new package name.
for _, n := range o.nodes {
for id, obj := range n.uses {
// existing import dependency?
if pkgName, ok := obj.(*types.PkgName); ok {
n.addImport(pkgName)
continue
}
name := id.Name
if new, ok := exportNames[obj]; ok {
name = new
}
// Cross-package reference to package-level entity?
//
// TODO(adonovan): fix: check the lexical
// structure to see if the name is free. If
// not, uniquify n2.cluster.name. For now,
// globally qualify; later, uniquify it only as
// needed on a per-cluster basis.
if isPackageLevel(obj) {
n2 := o.nodesByObj[obj]
if n2.cluster != n.cluster {
// qualify the identifier
name = n2.cluster.name + "." + name
n.addImport(n2.cluster)
}
}
id.Name = name
}
}
// Modify defining identifiers for exported objects.
for id, obj := range o.info.Defs {
if new, ok := exportNames[obj]; ok {
id.Name = new
}
}
// Split the source files into files in subpackages.
if err := o.split(); err != nil {
return err
}
// Now write the clusters out:
var failed bool
fmt.Fprintf(os.Stderr, "Writing refactored output...\n")
for _, c := range clusters {
dir := filepath.Join(*outdir, c.importPath)
fmt.Fprintf(os.Stderr, "\t%s", dir)
if err := os.MkdirAll(dir, 0755); err != nil {
fmt.Fprintf(os.Stderr, ": %v", err)
failed = true
} else {
// Create an empty .s file in each new package;
// this causes gc to suppress "missing function
// body" errors until link time.
ioutil.WriteFile(filepath.Join(dir, "dummy.s"), nil, 0666)
for base, out := range c.outputFiles {
filename := filepath.Join(dir, base)
if err := out.writeFile(filename); err != nil {
fmt.Fprintf(os.Stderr, ": %v", err)
failed = true
}
}
}
fmt.Fprintln(os.Stderr)
}
if failed {
return fmt.Errorf("there were I/O errors")
}
return nil
}
// split writes the (modified) AST for each node to the output file to
// which it belongs, in lexical order.
//
func (o *organizer) split() error {
// TODO(adonovan): fix: look at other uses too: references to
// interface methods and struct fields.
// Now we pretty-print the modified syntax trees, split the text
// into node-sized chunks (along with preceding
// whitespace/comments), and append each chunk to the relevant
// (split) files belonging to each cluster.
//
// To back, split the text into node-sized chunks and attach the
// text of each one to the appropriate node's text.
//
// We do this one file at a time, splitting the pretty text into
// declarations, with order determined by forEachDecl again, for
// consistency. This way each decl corresponds to o.nodes[i].
//
var i int // node index
for _, f := range o.info.Files {
filename := o.fset.Position(f.Pos()).Filename
filebase := filepath.Base(filename)
// Print each file and parse it back.
var buf bytes.Buffer
if err := format.Node(&buf, o.fset, f); err != nil {
return fmt.Errorf("pretty-printing %s failed: %v", filename, err)
}
fset2 := token.NewFileSet()
f2, err := parser.ParseFile(fset2, filename, &buf, parser.ParseComments)
if err != nil {
return fmt.Errorf("parsing of pretty-printed %s failed: %v", filename, err)
}
text := buf.Bytes()
// All text operations are newline-terminated.
// Record the initial comment that runs from the start
// of the file up (but not including) the package decl.
// Each output file will get a copy of it, plus a
// package decl appropriate to its cluster.
initialComment := text[:int(f2.Package)-fset2.File(f2.Pos()).Base()]
// Skip to beyond the import block.
//
// TODO(adonovan): fix: don't discard comments between
// the package decl and the import decl. (Fortunately
// "runtime" uses few imports.)
pos := f2.Name.End() // after package decl
for _, decl := range f2.Decls {
if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT {
pos = decl.End()
}
}
offset := fset2.Position(pos).Offset // offset of end of previous decl
offset = withNewline(text, offset)
var enterGroupText []byte // current group's opening whitespace and "var ("
// Map parsed pretty decls back to their corresponding nodes.
forEachDecl(f2, func(syntax ast.Node, parent *ast.GenDecl) {
// Find node and cluster corresponding to syntax.
// (Careful: methods have no node of their own,
// so we can't use o.nodes[i].)
n := o.nodes[i]
i++
out := n.cluster.file(filebase)
out.addImportsFor(n)
// first time writing to this file?
if out.head.Len() == 0 {
out.head.Write(initialComment)
// TODO(adonovan): fix: think about the
// leading \n. Is it sound w.r.t. both
// package documentation (which doesn't
// want it) and +build comments (which
// need it)?
fmt.Fprintf(&out.head, "package %s\n\n",
path.Base(n.cluster.importPath))
}
// Handle transitions into/out of group decls:
// var(...), type(...).
if parent == nil {
// syntax is a complete decl
// leaving previous group
if out.groupDecl != nil {
out.body.WriteString(")\n")
out.groupDecl = nil
}
} else {
// syntax is one var or type spec in a group decl
// first spec of group?
if syntax == parent.Specs[0] {
// save preceding whitespace and "var ("
lparen := fset2.Position(parent.Lparen).Offset
lparen = withNewline(text, lparen)
enterGroupText = text[offset:lparen]
offset = lparen
}
// has group changed?
if parent != out.groupDecl {
// leave previous group
if out.groupDecl != nil {
out.body.WriteString(")\n")
}
// enter new group
out.body.Write(enterGroupText)
out.groupDecl = parent
}
}
// The final implicit "leaving group" transition for
// each file is handled by (*cluster).writeFile.
// TODO(adonovan): fix: don't discard comments
// at the end of each file; copy them to all
// output files.
// Emit node syntax.
// Emit in all text since the end of the last decl.
end := fset2.Position(syntax.End()).Offset
end = withNewline(text, end)
out.body.Write(text[offset:end])
offset = end
// last spec of group?
if parent != nil && syntax == parent.Specs[len(parent.Specs)-1] {
// consume input up to ')'
rparen := fset2.Position(parent.Rparen).Offset
rparen = withNewline(text, rparen)
offset = rparen
}
})
}
if i != len(o.nodes) {
panic("internal error")
}
return nil
}
func withNewline(data []byte, i int) int {
for ; i < len(data); i++ {
if data[i] == '\n' {
return i + 1
}
}
return i
}
func (n *node) addImport(imp interface{}) {
if n.imports == nil {
n.imports = make(map[interface{}]bool)
}
n.imports[imp] = true
}
// outputFile holds state for each output file.
type outputFile struct {
head, body bytes.Buffer // head is package decl + cluster imports
imports map[interface{}]bool // union of node.imports
groupDecl ast.Decl // previous group decl, if any
}
func (out *outputFile) addImportsFor(n *node) {
if out.imports == nil {
out.imports = make(map[interface{}]bool)
}
for imp := range n.imports {
out.imports[imp] = true
}
}
func (c *cluster) file(base string) *outputFile {
f := c.outputFiles[base]
if f == nil {
f = new(outputFile)
c.outputFiles[base] = f
}
return f
}
// writeFile writes the outputFile data to the specified file.
func (out *outputFile) writeFile(filename string) error {
// Add necessary imports to head.
if len(out.imports) > 0 {
var importLines []string
for imp := range out.imports {
var name, importPath string
switch imp := imp.(type) {
case *types.PkgName:
name = imp.Name()
importPath = imp.Imported().Path()
case *cluster:
name = imp.name
importPath = imp.importPath
}
var spec string
if name == path.Base(importPath) {
spec = fmt.Sprintf("\t%q\n", importPath)
} else {
spec = fmt.Sprintf("\t%s %q\n", name, importPath)
}
importLines = append(importLines, spec)
}
sort.Strings(importLines)
fmt.Fprintf(&out.head, "import (\n")
for _, imp := range importLines {
out.head.WriteString(imp)
}
fmt.Fprintf(&out.head, ")\n")
}
// Implement final state transition.
if out.groupDecl != nil {
// leaving var or type(...) decl
out.body.WriteString(")\n")
}
// Write formatted head and data to filename.
out.head.Write(out.body.Bytes())
data := out.head.Bytes()
// Run it through gofmt.
data, err := format.Source(data)
if err != nil {
return fmt.Errorf("failed to gofmt %s: %v", filename, err)
}
return ioutil.WriteFile(filename, data, 0666)
}
// exportName returns the corresponding exported name for a non-exported identifier.
func exportedName(name string) string {
// Underscores are used to avoid conflicts with keywords
// (e.g. _func) or built-in identifiers (e.g. _string),
// or to suppress export of uppercase names (e.g. _ESRCH).
// Strip them off.
name = strings.TrimLeft(name, "_")
r, size := utf8.DecodeRuneInString(name)
name = string(unicode.ToUpper(r)) + name[size:] // "foo" -> "Foo"
if !unicode.IsLetter(r) {
name = "X" + name // e.g. "_64bit" -> "X64bit"
// TODO(adonovan): fix: result may yet conflict.
}
return name
}
// -- from refactor/rename --
func isPackageLevel(obj types.Object) bool {
return obj.Pkg().Scope().Lookup(obj.Name()) == obj
}