1// Copyright (c) 2016 Paul Jolly <paul@myitcv.org.uk>, all rights reserved.
2// Use of this document is governed by a license found in the LICENSE document.
3
4package main
5
6import (
7	"bytes"
8	"go/ast"
9	"go/build"
10	"go/parser"
11	"go/printer"
12	"go/token"
13	"os/exec"
14	"path"
15	"strings"
16
17	"myitcv.io/gogenerate"
18)
19
20const (
21	reactPkg      = "myitcv.io/react"
22	compDefName   = "ComponentDef"
23	compDefSuffix = "Def"
24
25	stateTypeSuffix     = "State"
26	propsTypeSuffix     = "Props"
27	propsTypeTmplPrefix = "_"
28
29	getInitialState           = "GetInitialState"
30	componentWillReceiveProps = "ComponentWillReceiveProps"
31	equals                    = "Equals"
32)
33
34type typeFile struct {
35	ts   *ast.TypeSpec
36	file *ast.File
37}
38
39type funcFile struct {
40	fn   *ast.FuncDecl
41	file *ast.File
42}
43
44var fset = token.NewFileSet()
45
46func astNodeString(i interface{}) string {
47	b := bytes.NewBuffer(nil)
48	err := printer.Fprint(b, fset, i)
49	if err != nil {
50		fatalf("failed to astNodeString %v: %v", i, err)
51	}
52
53	return b.String()
54}
55
56func goFmtBuf(b *bytes.Buffer) (*bytes.Buffer, error) {
57	out := bytes.NewBuffer(nil)
58	cmd := exec.Command("gofmt", "-s")
59	cmd.Stdin = b
60	cmd.Stdout = out
61
62	err := cmd.Run()
63
64	return out, err
65}
66
67type gen struct {
68	pkg        string
69	pkgImpPath string
70
71	isReactCore bool
72
73	propsTmpls    map[string]typeFile
74	components    map[string]typeFile
75	types         map[string]typeFile
76	pointMeths    map[string][]funcFile
77	nonPointMeths map[string][]funcFile
78}
79
80func dogen(dir, license string) {
81
82	bpkg, err := build.ImportDir(dir, 0)
83	if err != nil {
84		fatalf("unable to import pkg in dir %v: %v", dir, err)
85	}
86
87	isReactCore := bpkg.ImportPath == reactPkg
88
89	pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments)
90	if err != nil {
91		fatalf("unable to parse %v: %v", dir, err)
92	}
93
94	// we intentionally walk all packages, i.e. the package in the current directory
95	// and any x-test package that may also be present
96	for pn, pkg := range pkgs {
97		g := &gen{
98			pkg:        pn,
99			pkgImpPath: bpkg.ImportPath,
100
101			isReactCore: isReactCore,
102
103			propsTmpls:    make(map[string]typeFile),
104			components:    make(map[string]typeFile),
105			types:         make(map[string]typeFile),
106			pointMeths:    make(map[string][]funcFile),
107			nonPointMeths: make(map[string][]funcFile),
108		}
109
110		for fn, file := range pkg.Files {
111
112			if gogenerate.FileGeneratedBy(fn, "reactGen") {
113				continue
114			}
115
116			foundImp := false
117			impName := ""
118
119			for _, i := range file.Imports {
120				p := strings.Trim(i.Path.Value, "\"")
121
122				if p == reactPkg {
123					foundImp = true
124
125					if i.Name != nil {
126						impName = i.Name.Name
127					} else {
128						impName = path.Base(reactPkg)
129					}
130
131					break
132				}
133			}
134
135			if !foundImp && !isReactCore {
136				continue
137			}
138
139			for _, d := range file.Decls {
140				switch d := d.(type) {
141				case *ast.FuncDecl:
142					if d.Recv == nil {
143						continue
144					}
145
146					f := d.Recv.List[0]
147
148					switch v := f.Type.(type) {
149					case *ast.StarExpr:
150						id, ok := v.X.(*ast.Ident)
151						if !ok {
152							continue
153						}
154						g.pointMeths[id.Name] = append(g.pointMeths[id.Name], funcFile{d, file})
155					case *ast.Ident:
156						g.nonPointMeths[v.Name] = append(g.nonPointMeths[v.Name], funcFile{d, file})
157					}
158
159				case *ast.GenDecl:
160					if d.Tok != token.TYPE {
161						continue
162					}
163
164					for _, ts := range d.Specs {
165						ts := ts.(*ast.TypeSpec)
166
167						st, ok := ts.Type.(*ast.StructType)
168						if !ok || st.Fields == nil {
169							continue
170						}
171
172						if n := ts.Name.Name; strings.HasPrefix(n, propsTypeTmplPrefix) &&
173							strings.HasSuffix(n, propsTypeSuffix) {
174
175							if ts.Doc == nil {
176								ts.Doc = d.Doc
177							}
178
179							g.propsTmpls[n] = typeFile{
180								ts:   ts,
181								file: file,
182							}
183
184							continue
185						}
186
187						foundAnon := false
188
189						for _, f := range st.Fields.List {
190							if f.Names != nil {
191								// it must be anonymous
192								continue
193							}
194
195							se, ok := f.Type.(*ast.SelectorExpr)
196							if !ok {
197								continue
198							}
199
200							if se.Sel.Name != compDefName {
201								continue
202							}
203
204							id, ok := se.X.(*ast.Ident)
205							if !ok {
206								continue
207							}
208
209							if id.Name != impName {
210								continue
211							}
212
213							foundAnon = true
214						}
215
216						if foundAnon && strings.HasSuffix(ts.Name.Name, compDefSuffix) {
217							g.components[ts.Name.Name] = typeFile{ts, file}
218						} else {
219							g.types[ts.Name.Name] = typeFile{ts, file}
220						}
221					}
222				}
223			}
224		}
225
226		// at this point we have the components and their methods
227		for cd := range g.components {
228			g.genComp(cd)
229		}
230	}
231}
232