├── .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 | Version 4 | 5 | License: MIT 6 | 7 | 8 | Twitter: reflog 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 | 48 | 52 | 53 | 54 | 57 | 58 | 59 | 60 |
45 |

type your go code here:

46 | 47 |
49 |

here's your AST:

50 |
 
51 |
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 | --------------------------------------------------------------------------------