├── .github
└── workflows
│ └── ci.yml
├── README.md
├── docs
├── _config.yml
├── index.html
├── main.wasm
└── wasm_exec.js
├── go.mod
├── main.go
└── main_test.go
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | # Sequence of patterns matched against refs/tags
4 | tags:
5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
6 |
7 | name: Make a release
8 |
9 | jobs:
10 | build:
11 | name: Make a release
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v1
16 | - name: Set up Go 1.11
17 | uses: actions/setup-go@v1
18 | with:
19 | go-version: 1.11
20 | - name: Test
21 | run: go test -v .
22 | - name: Build
23 | uses: sosedoff/actions/golang-build@master
24 | with:
25 | args: "linux/amd64 darwin/amd64 windows/amd64"
26 | - name: Create Release
27 | id: create_release
28 | uses: actions/create-release@v1.0.0
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 | with:
32 | tag_name: ${{ github.ref }}
33 | release_name: Release ${{ github.ref }}
34 | draft: false
35 | prerelease: false
36 | - name: Upload Release Asset (Windows)
37 | id: upload-release-asset
38 | uses: actions/upload-release-asset@v1.0.1
39 | env:
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | with:
42 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
43 | asset_path: .release/go2ast_windows_amd64.zip
44 | asset_name: go2ast_windows_amd64.zip
45 | asset_content_type: application/zip
46 | - name: Upload Release Asset (Linux)
47 | id: upload-release-asset-linux
48 | uses: actions/upload-release-asset@v1.0.1
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 | with:
52 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
53 | asset_path: .release/go2ast_linux_amd64.zip
54 | asset_name: go2ast_linux_amd64.zip
55 | asset_content_type: application/zip
56 | - name: Upload Release Asset (Darwin)
57 | id: upload-release-asset-darwin
58 | uses: actions/upload-release-asset@v1.0.1
59 | env:
60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 | with:
62 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
63 | asset_path: .release/go2ast_darwin_amd64.zip
64 | asset_name: go2ast_darwin_amd64.zip
65 | asset_content_type: application/zip
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Welcome to go2ast 👋
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | > Transform Go code into it's AST
13 |
14 | ## Usage
15 |
16 | ```sh
17 | echo "a := 1" | go run main.go
18 | ```
19 |
20 | ## Example output
21 | ```go
22 | []ast.Stmt {
23 | &ast.AssignStmt {
24 | Lhs: []ast.Expr {
25 | &ast.Ident {
26 | Name: "a",
27 | },
28 | },
29 | Tok: :=,
30 | Rhs: []ast.Expr {
31 | &ast.BasicLit {
32 | ValuePos: 30,
33 | Kind: INT,
34 | Value: "1",
35 | },
36 | },
37 | },
38 | }
39 |
40 | ```
41 |
42 | ## WASM Demo
43 |
44 | Try this live: https://reflog.github.io/go2ast/
45 |
46 | ## Author
47 |
48 | 👤 **Eli Yukelzon**
49 |
50 | * Twitter: [@reflog](https://twitter.com/reflog)
51 | * Github: [@reflog](https://github.com/reflog)
52 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Go2AST
5 |
6 |
36 |
37 |
38 |
39 | Go 2 AST
40 |
41 |
42 |
43 |
44 |
45 | type your go code here:
46 |
47 | |
48 |
49 | here's your AST:
50 |
51 | |
52 |
53 |
54 | |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/docs/main.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reflog/go2ast/2f7be153c7e919cd64c9ddc3d1466d3234a4d29e/docs/main.wasm
--------------------------------------------------------------------------------
/docs/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | (() => {
6 | // Map multiple JavaScript environments to a single common API,
7 | // preferring web standards over Node.js API.
8 | //
9 | // Environments considered:
10 | // - Browsers
11 | // - Node.js
12 | // - Electron
13 | // - Parcel
14 |
15 | if (typeof global !== "undefined") {
16 | // global already exists
17 | } else if (typeof window !== "undefined") {
18 | window.global = window;
19 | } else if (typeof self !== "undefined") {
20 | self.global = self;
21 | } else {
22 | throw new Error("cannot export Go (neither global, window nor self is defined)");
23 | }
24 |
25 | if (!global.require && typeof require !== "undefined") {
26 | global.require = require;
27 | }
28 |
29 | if (!global.fs && global.require) {
30 | global.fs = require("fs");
31 | }
32 |
33 | if (!global.fs) {
34 | let outputBuf = "";
35 | global.fs = {
36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
37 | writeSync(fd, buf) {
38 | outputBuf += decoder.decode(buf);
39 | const nl = outputBuf.lastIndexOf("\n");
40 | if (nl != -1) {
41 | console.log(outputBuf.substr(0, nl));
42 | outputBuf = outputBuf.substr(nl + 1);
43 | }
44 | return buf.length;
45 | },
46 | write(fd, buf, offset, length, position, callback) {
47 | if (offset !== 0 || length !== buf.length || position !== null) {
48 | throw new Error("not implemented");
49 | }
50 | const n = this.writeSync(fd, buf);
51 | callback(null, n);
52 | },
53 | open(path, flags, mode, callback) {
54 | const err = new Error("not implemented");
55 | err.code = "ENOSYS";
56 | callback(err);
57 | },
58 | read(fd, buffer, offset, length, position, callback) {
59 | const err = new Error("not implemented");
60 | err.code = "ENOSYS";
61 | callback(err);
62 | },
63 | fsync(fd, callback) {
64 | callback(null);
65 | },
66 | };
67 | }
68 |
69 | if (!global.crypto) {
70 | const nodeCrypto = require("crypto");
71 | global.crypto = {
72 | getRandomValues(b) {
73 | nodeCrypto.randomFillSync(b);
74 | },
75 | };
76 | }
77 |
78 | if (!global.performance) {
79 | global.performance = {
80 | now() {
81 | const [sec, nsec] = process.hrtime();
82 | return sec * 1000 + nsec / 1000000;
83 | },
84 | };
85 | }
86 |
87 | if (!global.TextEncoder) {
88 | global.TextEncoder = require("util").TextEncoder;
89 | }
90 |
91 | if (!global.TextDecoder) {
92 | global.TextDecoder = require("util").TextDecoder;
93 | }
94 |
95 | // End of polyfills for common API.
96 |
97 | const encoder = new TextEncoder("utf-8");
98 | const decoder = new TextDecoder("utf-8");
99 |
100 | global.Go = class {
101 | constructor() {
102 | this.argv = ["js"];
103 | this.env = {};
104 | this.exit = (code) => {
105 | if (code !== 0) {
106 | console.warn("exit code:", code);
107 | }
108 | };
109 | this._exitPromise = new Promise((resolve) => {
110 | this._resolveExitPromise = resolve;
111 | });
112 | this._pendingEvent = null;
113 | this._scheduledTimeouts = new Map();
114 | this._nextCallbackTimeoutID = 1;
115 |
116 | const mem = () => {
117 | // The buffer may change when requesting more memory.
118 | return new DataView(this._inst.exports.mem.buffer);
119 | }
120 |
121 | const setInt64 = (addr, v) => {
122 | mem().setUint32(addr + 0, v, true);
123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
124 | }
125 |
126 | const getInt64 = (addr) => {
127 | const low = mem().getUint32(addr + 0, true);
128 | const high = mem().getInt32(addr + 4, true);
129 | return low + high * 4294967296;
130 | }
131 |
132 | const loadValue = (addr) => {
133 | const f = mem().getFloat64(addr, true);
134 | if (f === 0) {
135 | return undefined;
136 | }
137 | if (!isNaN(f)) {
138 | return f;
139 | }
140 |
141 | const id = mem().getUint32(addr, true);
142 | return this._values[id];
143 | }
144 |
145 | const storeValue = (addr, v) => {
146 | const nanHead = 0x7FF80000;
147 |
148 | if (typeof v === "number") {
149 | if (isNaN(v)) {
150 | mem().setUint32(addr + 4, nanHead, true);
151 | mem().setUint32(addr, 0, true);
152 | return;
153 | }
154 | if (v === 0) {
155 | mem().setUint32(addr + 4, nanHead, true);
156 | mem().setUint32(addr, 1, true);
157 | return;
158 | }
159 | mem().setFloat64(addr, v, true);
160 | return;
161 | }
162 |
163 | switch (v) {
164 | case undefined:
165 | mem().setFloat64(addr, 0, true);
166 | return;
167 | case null:
168 | mem().setUint32(addr + 4, nanHead, true);
169 | mem().setUint32(addr, 2, true);
170 | return;
171 | case true:
172 | mem().setUint32(addr + 4, nanHead, true);
173 | mem().setUint32(addr, 3, true);
174 | return;
175 | case false:
176 | mem().setUint32(addr + 4, nanHead, true);
177 | mem().setUint32(addr, 4, true);
178 | return;
179 | }
180 |
181 | let ref = this._refs.get(v);
182 | if (ref === undefined) {
183 | ref = this._values.length;
184 | this._values.push(v);
185 | this._refs.set(v, ref);
186 | }
187 | let typeFlag = 0;
188 | switch (typeof v) {
189 | case "string":
190 | typeFlag = 1;
191 | break;
192 | case "symbol":
193 | typeFlag = 2;
194 | break;
195 | case "function":
196 | typeFlag = 3;
197 | break;
198 | }
199 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
200 | mem().setUint32(addr, ref, true);
201 | }
202 |
203 | const loadSlice = (addr) => {
204 | const array = getInt64(addr + 0);
205 | const len = getInt64(addr + 8);
206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
207 | }
208 |
209 | const loadSliceOfValues = (addr) => {
210 | const array = getInt64(addr + 0);
211 | const len = getInt64(addr + 8);
212 | const a = new Array(len);
213 | for (let i = 0; i < len; i++) {
214 | a[i] = loadValue(array + i * 8);
215 | }
216 | return a;
217 | }
218 |
219 | const loadString = (addr) => {
220 | const saddr = getInt64(addr + 0);
221 | const len = getInt64(addr + 8);
222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
223 | }
224 |
225 | const timeOrigin = Date.now() - performance.now();
226 | this.importObject = {
227 | go: {
228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
231 | // This changes the SP, thus we have to update the SP used by the imported function.
232 |
233 | // func wasmExit(code int32)
234 | "runtime.wasmExit": (sp) => {
235 | const code = mem().getInt32(sp + 8, true);
236 | this.exited = true;
237 | delete this._inst;
238 | delete this._values;
239 | delete this._refs;
240 | this.exit(code);
241 | },
242 |
243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
244 | "runtime.wasmWrite": (sp) => {
245 | const fd = getInt64(sp + 8);
246 | const p = getInt64(sp + 16);
247 | const n = mem().getInt32(sp + 24, true);
248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
249 | },
250 |
251 | // func nanotime() int64
252 | "runtime.nanotime": (sp) => {
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | const msec = (new Date).getTime();
259 | setInt64(sp + 8, msec / 1000);
260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
261 | },
262 |
263 | // func scheduleTimeoutEvent(delay int64) int32
264 | "runtime.scheduleTimeoutEvent": (sp) => {
265 | const id = this._nextCallbackTimeoutID;
266 | this._nextCallbackTimeoutID++;
267 | this._scheduledTimeouts.set(id, setTimeout(
268 | () => {
269 | this._resume();
270 | while (this._scheduledTimeouts.has(id)) {
271 | // for some reason Go failed to register the timeout event, log and try again
272 | // (temporary workaround for https://github.com/golang/go/issues/28975)
273 | console.warn("scheduleTimeoutEvent: missed timeout event");
274 | this._resume();
275 | }
276 | },
277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
278 | ));
279 | mem().setInt32(sp + 16, id, true);
280 | },
281 |
282 | // func clearTimeoutEvent(id int32)
283 | "runtime.clearTimeoutEvent": (sp) => {
284 | const id = mem().getInt32(sp + 8, true);
285 | clearTimeout(this._scheduledTimeouts.get(id));
286 | this._scheduledTimeouts.delete(id);
287 | },
288 |
289 | // func getRandomData(r []byte)
290 | "runtime.getRandomData": (sp) => {
291 | crypto.getRandomValues(loadSlice(sp + 8));
292 | },
293 |
294 | // func stringVal(value string) ref
295 | "syscall/js.stringVal": (sp) => {
296 | storeValue(sp + 24, loadString(sp + 8));
297 | },
298 |
299 | // func valueGet(v ref, p string) ref
300 | "syscall/js.valueGet": (sp) => {
301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
302 | sp = this._inst.exports.getsp(); // see comment above
303 | storeValue(sp + 32, result);
304 | },
305 |
306 | // func valueSet(v ref, p string, x ref)
307 | "syscall/js.valueSet": (sp) => {
308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
309 | },
310 |
311 | // func valueIndex(v ref, i int) ref
312 | "syscall/js.valueIndex": (sp) => {
313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
314 | },
315 |
316 | // valueSetIndex(v ref, i int, x ref)
317 | "syscall/js.valueSetIndex": (sp) => {
318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
319 | },
320 |
321 | // func valueCall(v ref, m string, args []ref) (ref, bool)
322 | "syscall/js.valueCall": (sp) => {
323 | try {
324 | const v = loadValue(sp + 8);
325 | const m = Reflect.get(v, loadString(sp + 16));
326 | const args = loadSliceOfValues(sp + 32);
327 | const result = Reflect.apply(m, v, args);
328 | sp = this._inst.exports.getsp(); // see comment above
329 | storeValue(sp + 56, result);
330 | mem().setUint8(sp + 64, 1);
331 | } catch (err) {
332 | storeValue(sp + 56, err);
333 | mem().setUint8(sp + 64, 0);
334 | }
335 | },
336 |
337 | // func valueInvoke(v ref, args []ref) (ref, bool)
338 | "syscall/js.valueInvoke": (sp) => {
339 | try {
340 | const v = loadValue(sp + 8);
341 | const args = loadSliceOfValues(sp + 16);
342 | const result = Reflect.apply(v, undefined, args);
343 | sp = this._inst.exports.getsp(); // see comment above
344 | storeValue(sp + 40, result);
345 | mem().setUint8(sp + 48, 1);
346 | } catch (err) {
347 | storeValue(sp + 40, err);
348 | mem().setUint8(sp + 48, 0);
349 | }
350 | },
351 |
352 | // func valueNew(v ref, args []ref) (ref, bool)
353 | "syscall/js.valueNew": (sp) => {
354 | try {
355 | const v = loadValue(sp + 8);
356 | const args = loadSliceOfValues(sp + 16);
357 | const result = Reflect.construct(v, args);
358 | sp = this._inst.exports.getsp(); // see comment above
359 | storeValue(sp + 40, result);
360 | mem().setUint8(sp + 48, 1);
361 | } catch (err) {
362 | storeValue(sp + 40, err);
363 | mem().setUint8(sp + 48, 0);
364 | }
365 | },
366 |
367 | // func valueLength(v ref) int
368 | "syscall/js.valueLength": (sp) => {
369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
370 | },
371 |
372 | // valuePrepareString(v ref) (ref, int)
373 | "syscall/js.valuePrepareString": (sp) => {
374 | const str = encoder.encode(String(loadValue(sp + 8)));
375 | storeValue(sp + 16, str);
376 | setInt64(sp + 24, str.length);
377 | },
378 |
379 | // valueLoadString(v ref, b []byte)
380 | "syscall/js.valueLoadString": (sp) => {
381 | const str = loadValue(sp + 8);
382 | loadSlice(sp + 16).set(str);
383 | },
384 |
385 | // func valueInstanceOf(v ref, t ref) bool
386 | "syscall/js.valueInstanceOf": (sp) => {
387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
388 | },
389 |
390 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
391 | "syscall/js.copyBytesToGo": (sp) => {
392 | const dst = loadSlice(sp + 8);
393 | const src = loadValue(sp + 32);
394 | if (!(src instanceof Uint8Array)) {
395 | mem().setUint8(sp + 48, 0);
396 | return;
397 | }
398 | const toCopy = src.subarray(0, dst.length);
399 | dst.set(toCopy);
400 | setInt64(sp + 40, toCopy.length);
401 | mem().setUint8(sp + 48, 1);
402 | },
403 |
404 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
405 | "syscall/js.copyBytesToJS": (sp) => {
406 | const dst = loadValue(sp + 8);
407 | const src = loadSlice(sp + 16);
408 | if (!(dst instanceof Uint8Array)) {
409 | mem().setUint8(sp + 48, 0);
410 | return;
411 | }
412 | const toCopy = src.subarray(0, dst.length);
413 | dst.set(toCopy);
414 | setInt64(sp + 40, toCopy.length);
415 | mem().setUint8(sp + 48, 1);
416 | },
417 |
418 | "debug": (value) => {
419 | console.log(value);
420 | },
421 | }
422 | };
423 | }
424 |
425 | async run(instance) {
426 | this._inst = instance;
427 | this._values = [ // TODO: garbage collection
428 | NaN,
429 | 0,
430 | null,
431 | true,
432 | false,
433 | global,
434 | this,
435 | ];
436 | this._refs = new Map();
437 | this.exited = false;
438 |
439 | const mem = new DataView(this._inst.exports.mem.buffer)
440 |
441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
442 | let offset = 4096;
443 |
444 | const strPtr = (str) => {
445 | const ptr = offset;
446 | const bytes = encoder.encode(str + "\0");
447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
448 | offset += bytes.length;
449 | if (offset % 8 !== 0) {
450 | offset += 8 - (offset % 8);
451 | }
452 | return ptr;
453 | };
454 |
455 | const argc = this.argv.length;
456 |
457 | const argvPtrs = [];
458 | this.argv.forEach((arg) => {
459 | argvPtrs.push(strPtr(arg));
460 | });
461 |
462 | const keys = Object.keys(this.env).sort();
463 | argvPtrs.push(keys.length);
464 | keys.forEach((key) => {
465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
466 | });
467 |
468 | const argv = offset;
469 | argvPtrs.forEach((ptr) => {
470 | mem.setUint32(offset, ptr, true);
471 | mem.setUint32(offset + 4, 0, true);
472 | offset += 8;
473 | });
474 |
475 | this._inst.exports.run(argc, argv);
476 | if (this.exited) {
477 | this._resolveExitPromise();
478 | }
479 | await this._exitPromise;
480 | }
481 |
482 | _resume() {
483 | if (this.exited) {
484 | throw new Error("Go program has already exited");
485 | }
486 | this._inst.exports.resume();
487 | if (this.exited) {
488 | this._resolveExitPromise();
489 | }
490 | }
491 |
492 | _makeFuncWrapper(id) {
493 | const go = this;
494 | return function () {
495 | const event = { id: id, this: this, args: arguments };
496 | go._pendingEvent = event;
497 | go._resume();
498 | return event.result;
499 | };
500 | }
501 | }
502 |
503 | if (
504 | global.require &&
505 | global.require.main === module &&
506 | global.process &&
507 | global.process.versions &&
508 | !global.process.versions.electron
509 | ) {
510 | if (process.argv.length < 3) {
511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
512 | process.exit(1);
513 | }
514 |
515 | const go = new Go();
516 | go.argv = process.argv.slice(2);
517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
518 | go.exit = process.exit;
519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending
521 | if (code === 0 && !go.exited) {
522 | // deadlock, make Go print error and stack traces
523 | go._pendingEvent = { id: 0 };
524 | go._resume();
525 | }
526 | });
527 | return go.run(result.instance);
528 | }).catch((err) => {
529 | console.error(err);
530 | process.exit(1);
531 | });
532 | }
533 | })();
534 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/reflog/go2ast
2 |
3 | go 1.12
4 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "go/ast"
7 | "go/parser"
8 | "go/token"
9 | "io"
10 | "os"
11 | "reflect"
12 | )
13 |
14 | // A FieldFilter may be provided to Fprint to control the output.
15 | type FieldFilter func(name string, value reflect.Value) bool
16 |
17 | // NotNilFilter returns true for field values that are not nil;
18 | // it returns false otherwise.
19 | func NotNilFilter(_ string, v reflect.Value) bool {
20 | switch v.Kind() {
21 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
22 | return !v.IsNil()
23 | }
24 | return true
25 | }
26 |
27 | // NotNilFilter returns true for field values that are not nil;
28 | // it returns false otherwise.
29 | func NotBannedFilter(name string, v reflect.Value) bool {
30 | filtered := []string{"Obj", "Rbrace", "Lbrace", "NamePos", "Rparen", "LParen", "EndPos", "TokPos", "Decl", "Opening", "Closing", "Imports", "Unresolved"}
31 | for _, v := range filtered {
32 | if v == name {
33 | return false
34 | }
35 | }
36 | switch v.Kind() {
37 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
38 | return !v.IsNil()
39 | }
40 | return true
41 | }
42 |
43 | // Fprint prints the (sub-)tree starting at AST node x to w.
44 | // If fset != nil, position information is interpreted relative
45 | // to that file set. Otherwise positions are printed as integer
46 | // values (file set specific offsets).
47 | //
48 | // A non-nil FieldFilter f may be provided to control the output:
49 | // struct fields for which f(fieldname, fieldvalue) is true are
50 | // printed; all others are filtered from the output. Unexported
51 | // struct fields are never printed.
52 | func Fprint(w io.Writer, fset *token.FileSet, x interface{}, f FieldFilter) error {
53 | return fprint(w, fset, x, f)
54 | }
55 |
56 | func fprint(w io.Writer, fset *token.FileSet, x interface{}, f FieldFilter) (err error) {
57 | // setup printer
58 | p := printer{
59 | output: w,
60 | fset: fset,
61 | filter: f,
62 | ptrmap: make(map[interface{}]int),
63 | last: '\n', // force printing of line number on first line
64 | }
65 |
66 | // install error handler
67 | defer func() {
68 | if e := recover(); e != nil {
69 | err = e.(localError).err // re-panics if it's not a localError
70 | }
71 | }()
72 |
73 | // print x
74 | if x == nil {
75 | p.printf("nil\n")
76 | return
77 | }
78 | p.print(reflect.ValueOf(x))
79 | p.printf("\n")
80 |
81 | return
82 | }
83 |
84 | type printer struct {
85 | output io.Writer
86 | fset *token.FileSet
87 | filter FieldFilter
88 | ptrmap map[interface{}]int // *T -> line number
89 | indent int // current indentation level
90 | last byte // the last byte processed by Write
91 | line int // current line number
92 | }
93 |
94 | var indent = []byte("\t")
95 |
96 | func (p *printer) Write(data []byte) (n int, err error) {
97 | var m int
98 | for i, b := range data {
99 | // invariant: data[0:n] has been written
100 | if b == '\n' {
101 | m, err = p.output.Write(data[n : i+1])
102 | n += m
103 | if err != nil {
104 | return
105 | }
106 | p.line++
107 | } else if p.last == '\n' {
108 | for j := p.indent; j > 0; j-- {
109 | _, err = p.output.Write(indent)
110 | if err != nil {
111 | return
112 | }
113 | }
114 | }
115 | p.last = b
116 | }
117 | if len(data) > n {
118 | m, err = p.output.Write(data[n:])
119 | n += m
120 | }
121 | return
122 | }
123 |
124 | // localError wraps locally caught errors so we can distinguish
125 | // them from genuine panics which we don't want to return as errors.
126 | type localError struct {
127 | err error
128 | }
129 |
130 | // printf is a convenience wrapper that takes care of print errors.
131 | func (p *printer) printf(format string, args ...interface{}) {
132 | if _, err := fmt.Fprintf(p, format, args...); err != nil {
133 | panic(localError{err})
134 | }
135 | }
136 |
137 | // Implementation note: Print is written for AST nodes but could be
138 | // used to print arbitrary data structures; such a version should
139 | // probably be in a different package.
140 | //
141 | // Note: This code detects (some) cycles created via pointers but
142 | // not cycles that are created via slices or maps containing the
143 | // same slice or map. Code for general data structures probably
144 | // should catch those as well.
145 |
146 | func (p *printer) print(x reflect.Value) {
147 | if !NotNilFilter("", x) {
148 | p.printf("nil")
149 | return
150 | }
151 | switch x.Kind() {
152 | case reflect.Interface:
153 | p.print(x.Elem())
154 |
155 | case reflect.Map:
156 | p.printf("%s {", x.Type())
157 | if x.Len() > 0 {
158 | p.indent++
159 | p.printf("\n")
160 | for _, key := range x.MapKeys() {
161 | p.print(key)
162 | p.printf(": ")
163 | p.print(x.MapIndex(key))
164 | p.printf("\n")
165 | }
166 | p.indent--
167 | }
168 | p.printf("}")
169 |
170 | case reflect.Ptr:
171 | // type-checked ASTs may contain cycles - use ptrmap
172 | // to keep track of objects that have been printed
173 | // already and print the respective line number instead
174 | ptr := x.Interface()
175 |
176 | if _, exists := p.ptrmap[ptr]; exists {
177 | // p.printf("(obj @ %d)", line)
178 | } else {
179 | p.printf("&")
180 | p.ptrmap[ptr] = p.line
181 | p.print(x.Elem())
182 | }
183 |
184 | case reflect.Array:
185 | p.printf("%s {", x.Type())
186 | if x.Len() > 0 {
187 | p.indent++
188 | p.printf("\n")
189 | for i, n := 0, x.Len(); i < n; i++ {
190 | p.print(x.Index(i))
191 | p.printf(",\n")
192 | }
193 | p.indent--
194 | }
195 | p.printf("}")
196 |
197 | case reflect.Slice:
198 | if s, ok := x.Interface().([]byte); ok {
199 | p.printf("%#q", s)
200 | return
201 | }
202 | p.printf("%s {", x.Type())
203 | if x.Len() > 0 {
204 | p.indent++
205 | p.printf("\n")
206 | for i, n := 0, x.Len(); i < n; i++ {
207 | p.print(x.Index(i))
208 | p.printf(",\n")
209 | }
210 | p.indent--
211 | }
212 | p.printf("}")
213 |
214 | case reflect.Struct:
215 | t := x.Type()
216 | p.printf("%s {", t)
217 | p.indent++
218 | first := true
219 | for i, n := 0, t.NumField(); i < n; i++ {
220 | // exclude non-exported fields because their
221 | // values cannot be accessed via reflection
222 | if name := t.Field(i).Name; t.Field(i).PkgPath == "" {
223 | value := x.Field(i)
224 | if p.filter == nil || p.filter(name, value) {
225 | if first {
226 | p.printf("\n")
227 | first = false
228 | }
229 | p.printf("%s: ", name)
230 | p.print(value)
231 | p.printf(",\n")
232 | }
233 | }
234 | }
235 | p.indent--
236 | p.printf("}")
237 |
238 | default:
239 | v := x.Interface()
240 | switch v := v.(type) {
241 | case string:
242 | // print strings in quotes
243 | p.printf("%q", v)
244 | return
245 | }
246 | // default
247 | p.printf("%v", v)
248 | }
249 | }
250 |
251 | func generateAST(src string, writer io.Writer) error {
252 | fset := token.NewFileSet()
253 | f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
254 |
255 | if err != nil {
256 | return err
257 | }
258 | if fd, ok := f.Decls[0].(*ast.FuncDecl); ok {
259 | return Fprint(writer, fset, fd.Body.List, NotBannedFilter)
260 | }
261 | return nil
262 | }
263 |
264 | func wrapInPackage(src string) string {
265 | return "package main\nfunc main(){\n" + src + "\n}"
266 | }
267 |
268 | func main() {
269 |
270 | scanner := bufio.NewScanner(os.Stdin)
271 | src := ""
272 |
273 | for scanner.Scan() {
274 | src += scanner.Text()
275 | }
276 |
277 | if err := generateAST(wrapInPackage(src), os.Stdout); err != nil {
278 | fmt.Println(err)
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func Test_GenerateAST(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | src string
13 | result string
14 | wantErr bool
15 | }{
16 | {name: "broken", src: " ~~~ ", wantErr: true},
17 | {name: "empty source", src: "", result: "nil"},
18 | {name: "plain test", src: "test", result: `
19 | []ast.Stmt {
20 | &ast.ExprStmt {
21 | X: &ast.Ident {
22 | Name: "test",
23 | },
24 | },
25 | }`},
26 | {name: "advanced", src: `
27 | span, ctx := tracing.StartSpanWithParentByContext(c.App.Context, "api4:bot:createBot")
28 | c.App.Context = ctx
29 | defer span.Finish()
30 | `,
31 | result: `
32 | []ast.Stmt {
33 | &ast.AssignStmt {
34 | Lhs: []ast.Expr {
35 | &ast.Ident {
36 | Name: "span",
37 | },
38 | &ast.Ident {
39 | Name: "ctx",
40 | },
41 | },
42 | Tok: :=,
43 | Rhs: []ast.Expr {
44 | &ast.CallExpr {
45 | Fun: &ast.SelectorExpr {
46 | X: &ast.Ident {
47 | Name: "tracing",
48 | },
49 | Sel: &ast.Ident {
50 | Name: "StartSpanWithParentByContext",
51 | },
52 | },
53 | Lparen: 79,
54 | Args: []ast.Expr {
55 | &ast.SelectorExpr {
56 | X: &ast.SelectorExpr {
57 | X: &ast.Ident {
58 | Name: "c",
59 | },
60 | Sel: &ast.Ident {
61 | Name: "App",
62 | },
63 | },
64 | Sel: &ast.Ident {
65 | Name: "Context",
66 | },
67 | },
68 | &ast.BasicLit {
69 | ValuePos: 95,
70 | Kind: STRING,
71 | Value: "\"api4:bot:createBot\"",
72 | },
73 | },
74 | Ellipsis: 0,
75 | },
76 | },
77 | },
78 | &ast.AssignStmt {
79 | Lhs: []ast.Expr {
80 | &ast.SelectorExpr {
81 | X: &ast.SelectorExpr {
82 | X: &ast.Ident {
83 | Name: "c",
84 | },
85 | Sel: &ast.Ident {
86 | Name: "App",
87 | },
88 | },
89 | Sel: &ast.Ident {
90 | Name: "Context",
91 | },
92 | },
93 | },
94 | Tok: =,
95 | Rhs: []ast.Expr {
96 | &ast.Ident {
97 | Name: "ctx",
98 | },
99 | },
100 | },
101 | &ast.DeferStmt {
102 | Defer: 141,
103 | Call: &ast.CallExpr {
104 | Fun: &ast.SelectorExpr {
105 | X: &ast.Ident {
106 | Name: "span",
107 | },
108 | Sel: &ast.Ident {
109 | Name: "Finish",
110 | },
111 | },
112 | Lparen: 158,
113 | Ellipsis: 0,
114 | },
115 | },
116 | }
117 | `,
118 | },
119 | }
120 | for _, tt := range tests {
121 | t.Run(tt.name, func(t *testing.T) {
122 | writer := &bytes.Buffer{}
123 | if err := generateAST(wrapInPackage(tt.src), writer); (err != nil) != tt.wantErr {
124 | t.Errorf("generateAST() error = %v, wantErr %v", err, tt.wantErr)
125 | return
126 | }
127 | if gotWriter := writer.String(); strings.TrimSpace(gotWriter) != strings.TrimSpace(tt.result) {
128 | t.Errorf("generateAST() = {%v}, want {%v}", strings.TrimSpace(gotWriter), strings.TrimSpace(tt.result))
129 | }
130 | })
131 | }
132 | }
133 |
--------------------------------------------------------------------------------