Initial commit
This commit is contained in:
Executable
BIN
Binary file not shown.
@@ -0,0 +1,194 @@
|
||||
// # Build from project root:
|
||||
// go build -o tools/parse_go_ast tools/parse_go_ast.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GoDecl struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // "func", "method", "type", "struct", "interface", "var", "package"
|
||||
Receiver string `json:"receiver,omitempty"` // e.g., "*User"
|
||||
FullName string `json:"full_name,omitempty"`
|
||||
Fields []string `json:"fields,omitempty"` // for structs
|
||||
Methods []string `json:"methods,omitempty"` // for interfaces
|
||||
DocComment string `json:"doc_comment,omitempty"`
|
||||
StartLine int `json:"start_line"`
|
||||
EndLine int `json:"end_line"`
|
||||
}
|
||||
|
||||
func astTypeToString(expr ast.Expr) string {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.StarExpr:
|
||||
return "*" + astTypeToString(t.X)
|
||||
case *ast.SelectorExpr:
|
||||
return astTypeToString(t.X) + "." + t.Sel.Name
|
||||
case *ast.ArrayType:
|
||||
return "[]" + astTypeToString(t.Elt)
|
||||
case *ast.MapType:
|
||||
return "map[" + astTypeToString(t.Key) + "]" + astTypeToString(t.Value)
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func extractDocComment(comments []*ast.CommentGroup, pos token.Pos, fset *token.FileSet) string {
|
||||
if len(comments) == 0 {
|
||||
return ""
|
||||
}
|
||||
line := fset.Position(pos).Line
|
||||
for i := len(comments) - 1; i >= 0; i-- {
|
||||
cg := comments[i]
|
||||
cgLine := fset.Position(cg.End()).Line
|
||||
if cgLine < line && line-cgLine <= 5 {
|
||||
return strings.TrimSpace(cg.Text())
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: parse_go_ast <file.go>")
|
||||
os.Exit(1)
|
||||
}
|
||||
filename := os.Args[1]
|
||||
src, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("ERR: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
fileNode, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
fmt.Printf("ERR: %v\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
decls := []GoDecl{}
|
||||
|
||||
// Package comment (first comment group before package decl)
|
||||
pkgComment := ""
|
||||
for _, cg := range fileNode.Comments {
|
||||
if cg.Pos() < fileNode.Package {
|
||||
pkgComment = strings.TrimSpace(cg.Text())
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if pkgComment != "" {
|
||||
decls = append(decls, GoDecl{
|
||||
Name: "package",
|
||||
Type: "package",
|
||||
DocComment: pkgComment,
|
||||
StartLine: 1,
|
||||
EndLine: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// Process declarations
|
||||
for _, d := range fileNode.Decls {
|
||||
switch d := d.(type) {
|
||||
case *ast.FuncDecl:
|
||||
name := d.Name.Name
|
||||
recvType := ""
|
||||
if d.Recv != nil && len(d.Recv.List) > 0 {
|
||||
recvType = astTypeToString(d.Recv.List[0].Type)
|
||||
}
|
||||
typ := "func"
|
||||
fullName := name
|
||||
if recvType != "" {
|
||||
typ = "method"
|
||||
fullName = fmt.Sprintf("(%s).%s", recvType, name)
|
||||
}
|
||||
doc := extractDocComment(fileNode.Comments, d.Pos(), fset)
|
||||
decls = append(decls, GoDecl{
|
||||
Name: name,
|
||||
Type: typ,
|
||||
Receiver: recvType,
|
||||
FullName: fullName,
|
||||
DocComment: doc,
|
||||
StartLine: fset.Position(d.Pos()).Line,
|
||||
EndLine: fset.Position(d.End()).Line,
|
||||
})
|
||||
|
||||
case *ast.GenDecl:
|
||||
for _, spec := range d.Specs {
|
||||
switch s := spec.(type) {
|
||||
case *ast.TypeSpec:
|
||||
typName := s.Name.Name
|
||||
declType := "type"
|
||||
var fields []string
|
||||
var methods []string
|
||||
|
||||
switch t := s.Type.(type) {
|
||||
case *ast.StructType:
|
||||
declType = "struct"
|
||||
for _, f := range t.Fields.List {
|
||||
for _, n := range f.Names {
|
||||
field := n.Name
|
||||
if f.Tag != nil {
|
||||
field += " " + f.Tag.Value
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
if f.Names == nil {
|
||||
// Embedded field
|
||||
fields = append(fields, astTypeToString(f.Type))
|
||||
}
|
||||
}
|
||||
case *ast.InterfaceType:
|
||||
declType = "interface"
|
||||
for _, m := range t.Methods.List {
|
||||
if len(m.Names) > 0 {
|
||||
methods = append(methods, m.Names[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc := extractDocComment(fileNode.Comments, s.Pos(), fset)
|
||||
decls = append(decls, GoDecl{
|
||||
Name: typName,
|
||||
Type: declType,
|
||||
Fields: fields,
|
||||
Methods: methods,
|
||||
DocComment: doc,
|
||||
StartLine: fset.Position(d.Pos()).Line,
|
||||
EndLine: fset.Position(d.End()).Line,
|
||||
})
|
||||
|
||||
case *ast.ValueSpec:
|
||||
for _, name := range s.Names {
|
||||
doc := extractDocComment(fileNode.Comments, name.Pos(), fset)
|
||||
// Use the individual identifier's position, not the declaration's position
|
||||
startLine := fset.Position(name.Pos()).Line
|
||||
endLine := fset.Position(name.End()).Line
|
||||
|
||||
decls = append(decls, GoDecl{
|
||||
Name: name.Name,
|
||||
Type: "var",
|
||||
DocComment: doc,
|
||||
StartLine: startLine,
|
||||
EndLine: endLine,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out, _ := json.Marshal(decls)
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
Executable
+177
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
// Usage: node parse_ts.js file.ts
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { parse } = require('@typescript-eslint/typescript-estree');
|
||||
|
||||
function getLoc(node) {
|
||||
if (node.loc) {
|
||||
return { start: node.loc.start.line, end: node.loc.end.line };
|
||||
}
|
||||
return { start: 1, end: 1 };
|
||||
}
|
||||
|
||||
function extractLeadingComment(node, sourceCode) {
|
||||
if (!node.range || !sourceCode) return "";
|
||||
const startIdx = node.range[0];
|
||||
let commentEnd = startIdx;
|
||||
// Look backward for comments
|
||||
let i = startIdx - 1;
|
||||
let commentLines = [];
|
||||
let inBlock = false;
|
||||
|
||||
while (i >= 0) {
|
||||
const char = sourceCode[i];
|
||||
if (char === '\n') break;
|
||||
i--;
|
||||
}
|
||||
const lineStart = i + 1;
|
||||
const lineAbove = sourceCode.slice(lineStart, startIdx).trim();
|
||||
|
||||
// Check for // comment on same line before node
|
||||
if (lineAbove.startsWith('//')) {
|
||||
return lineAbove.substring(2).trim();
|
||||
}
|
||||
|
||||
// Look further up for multi-line or JSDoc
|
||||
const lines = sourceCode.substring(0, lineStart).split('\n');
|
||||
for (let j = lines.length - 1; j >= Math.max(0, lines.length - 5); j--) {
|
||||
const line = lines[j].trim();
|
||||
if (line.startsWith('//')) {
|
||||
commentLines.unshift(line.substring(2).trim());
|
||||
} else if (line.endsWith('*/')) {
|
||||
inBlock = true;
|
||||
commentLines.unshift(line.slice(0, -2).trim());
|
||||
} else if (inBlock) {
|
||||
if (line.startsWith('/*') || line.startsWith('/**')) {
|
||||
commentLines.unshift(line.slice(2).trim());
|
||||
break;
|
||||
} else {
|
||||
commentLines.unshift(line);
|
||||
}
|
||||
} else if (line === '') {
|
||||
if (commentLines.length > 0) continue;
|
||||
else break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return commentLines.join('\n').trim();
|
||||
}
|
||||
|
||||
function extractDeclarations(ast, sourceCode) {
|
||||
const decls = [];
|
||||
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach(n => visit(n));
|
||||
return;
|
||||
}
|
||||
|
||||
let docComment = extractLeadingComment(node, sourceCode);
|
||||
|
||||
if (node.type === 'FunctionDeclaration' && node.id?.name) {
|
||||
const loc = getLoc(node);
|
||||
decls.push({
|
||||
name: node.id.name,
|
||||
type: 'function',
|
||||
doc_comment: docComment,
|
||||
start_line: loc.start,
|
||||
end_line: loc.end
|
||||
});
|
||||
}
|
||||
else if (
|
||||
node.type === 'VariableDeclarator' &&
|
||||
node.id?.type === 'Identifier' &&
|
||||
node.init?.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
const loc = getLoc(node);
|
||||
decls.push({
|
||||
name: node.id.name,
|
||||
type: 'function',
|
||||
doc_comment: docComment,
|
||||
start_line: loc.start,
|
||||
end_line: loc.end
|
||||
});
|
||||
}
|
||||
else if (node.type === 'ClassDeclaration' && node.id?.name) {
|
||||
const loc = getLoc(node);
|
||||
decls.push({
|
||||
name: node.id.name,
|
||||
type: 'class',
|
||||
doc_comment: docComment,
|
||||
start_line: loc.start,
|
||||
end_line: loc.end
|
||||
});
|
||||
}
|
||||
else if (node.type === 'TSInterfaceDeclaration' && node.id?.name) {
|
||||
const loc = getLoc(node);
|
||||
decls.push({
|
||||
name: node.id.name,
|
||||
type: 'interface',
|
||||
doc_comment: docComment,
|
||||
start_line: loc.start,
|
||||
end_line: loc.end
|
||||
});
|
||||
}
|
||||
else if (node.type === 'TSTypeAliasDeclaration' && node.id?.name) {
|
||||
const loc = getLoc(node);
|
||||
decls.push({
|
||||
name: node.id.name,
|
||||
type: 'type',
|
||||
doc_comment: docComment,
|
||||
start_line: loc.start,
|
||||
end_line: loc.end
|
||||
});
|
||||
}
|
||||
else if (
|
||||
node.type === 'VariableDeclarator' &&
|
||||
node.id?.type === 'Identifier'
|
||||
) {
|
||||
const parent = node.parent;
|
||||
if (
|
||||
parent?.type === 'VariableDeclaration' &&
|
||||
['const', 'let'].includes(parent.kind)
|
||||
) {
|
||||
const loc = getLoc(node);
|
||||
decls.push({
|
||||
name: node.id.name,
|
||||
type: 'variable',
|
||||
doc_comment: docComment,
|
||||
start_line: loc.start,
|
||||
end_line: loc.end
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Object.values(node).forEach(visit);
|
||||
}
|
||||
|
||||
visit(ast);
|
||||
return decls;
|
||||
}
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.error('Usage: node parse_ts.js <file.ts>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const filePath = path.resolve(process.argv[2]);
|
||||
|
||||
try {
|
||||
const code = fs.readFileSync(filePath, 'utf8');
|
||||
const ast = parse(code, {
|
||||
sourceType: 'module',
|
||||
loc: true,
|
||||
range: true,
|
||||
comment: false // we extract manually for simplicity
|
||||
});
|
||||
const decls = extractDeclarations(ast, code);
|
||||
console.log(JSON.stringify(decls, null, 2));
|
||||
} catch (e) {
|
||||
console.error(`ERR: ${e.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
Reference in New Issue
Block a user