Skip to content

Commit 6c03495

Browse files
committed
feat: generate ts typings from golang code
1 parent c3bced8 commit 6c03495

File tree

6 files changed

+1193
-0
lines changed

6 files changed

+1193
-0
lines changed

jsonrpc.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,24 @@ func rpcKeyboardReportMulti(ctx context.Context, macro []map[string]any) (usbgad
11451145
return last, nil
11461146
}
11471147

1148+
// JSONRPCHandler is a struct that contains the name of the handler and the type of the handler
1149+
type JSONRPCHandler struct {
1150+
Params []string
1151+
Type reflect.Type
1152+
}
1153+
1154+
// GetJSONRPCHandlers returns a map of the JSON RPC handlers
1155+
func GetJSONRPCHandlers() map[string]JSONRPCHandler {
1156+
handlers := make(map[string]JSONRPCHandler)
1157+
for name, handler := range rpcHandlers {
1158+
function := reflect.ValueOf(handler.Func).Type()
1159+
1160+
handlers[name] = JSONRPCHandler{Params: handler.Params, Type: function}
1161+
}
1162+
1163+
return handlers
1164+
}
1165+
11481166
var rpcHandlers = map[string]RPCHandler{
11491167
"ping": {Func: rpcPing},
11501168
"reboot": {Func: rpcReboot, Params: []string{"force"}},
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
package main
2+
3+
import (
4+
"go/ast"
5+
"go/parser"
6+
"go/token"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/rs/zerolog/log"
12+
)
13+
14+
// getStringAliasInfoWithReflection uses reflection to automatically detect constants
15+
// This approach tries to find constants by examining the actual values
16+
func getStringAliasInfoWithReflection(searchPath string) []StringAliasInfo {
17+
log.Debug().Str("searchPath", searchPath).Msg("Detecting string aliases and constants in single pass")
18+
19+
// Detect both string aliases and their constants in a single file system walk
20+
result := detectStringAliasesWithConstants(searchPath)
21+
22+
// If reflection didn't work, throw an error
23+
if len(result) == 0 {
24+
log.Fatal().Msg("No string aliases with constants could be detected. Make sure the types are defined with constants in Go files.")
25+
}
26+
27+
log.Debug().Int("detected", len(result)).Msg("String alias detection completed")
28+
return result
29+
}
30+
31+
// detectStringAliasesWithConstants detects both string aliases and their constants in a single file system walk
32+
func detectStringAliasesWithConstants(searchPath string) []StringAliasInfo {
33+
var result []StringAliasInfo
34+
35+
// Walk the specified directory to find Go files
36+
err := filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
37+
if err != nil {
38+
return err
39+
}
40+
41+
// Skip directories and non-Go files
42+
if info.IsDir() || !strings.HasSuffix(path, ".go") {
43+
return nil
44+
}
45+
46+
// Skip test files and our own tool files
47+
if strings.Contains(path, "_test.go") || strings.Contains(path, "scripts/jsonrpc_typings") {
48+
return nil
49+
}
50+
51+
// Parse the file to find both string aliases and their constants
52+
aliases := findStringAliasesWithConstantsInFile(path)
53+
result = append(result, aliases...)
54+
55+
return nil
56+
})
57+
58+
if err != nil {
59+
log.Fatal().Err(err).Msg("Error walking directory for string alias detection")
60+
}
61+
62+
// Remove duplicates based on type name
63+
uniqueAliases := make([]StringAliasInfo, 0)
64+
seen := make(map[string]bool)
65+
for _, alias := range result {
66+
if !seen[alias.Name] {
67+
seen[alias.Name] = true
68+
uniqueAliases = append(uniqueAliases, alias)
69+
}
70+
}
71+
72+
return uniqueAliases
73+
}
74+
75+
// findStringAliasesWithConstantsInFile finds both string aliases and their constants in a single Go file
76+
func findStringAliasesWithConstantsInFile(filePath string) []StringAliasInfo {
77+
var result []StringAliasInfo
78+
79+
// Parse the Go file
80+
fset := token.NewFileSet()
81+
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
82+
if err != nil {
83+
log.Debug().Err(err).Str("file", filePath).Msg("Failed to parse file")
84+
return result
85+
}
86+
87+
// First pass: collect all string alias type names
88+
stringAliases := make(map[string]bool)
89+
ast.Inspect(node, func(n ast.Node) bool {
90+
genDecl, ok := n.(*ast.GenDecl)
91+
if !ok || genDecl.Tok != token.TYPE {
92+
return true
93+
}
94+
95+
for _, spec := range genDecl.Specs {
96+
typeSpec, ok := spec.(*ast.TypeSpec)
97+
if !ok {
98+
continue
99+
}
100+
101+
// Check if this is a string alias (type Name string)
102+
if ident, ok := typeSpec.Type.(*ast.Ident); ok && ident.Name == "string" {
103+
stringAliases[typeSpec.Name.Name] = true
104+
}
105+
}
106+
107+
return true
108+
})
109+
110+
// Second pass: find constants for the string aliases we found
111+
ast.Inspect(node, func(n ast.Node) bool {
112+
genDecl, ok := n.(*ast.GenDecl)
113+
if !ok || genDecl.Tok != token.CONST {
114+
return true
115+
}
116+
117+
// Process each constant specification in the declaration
118+
for _, spec := range genDecl.Specs {
119+
valueSpec, ok := spec.(*ast.ValueSpec)
120+
if !ok {
121+
continue
122+
}
123+
124+
// Check if this constant is typed with one of our string aliases
125+
if valueSpec.Type == nil {
126+
continue
127+
}
128+
129+
ident, ok := valueSpec.Type.(*ast.Ident)
130+
if !ok {
131+
continue
132+
}
133+
typeName := ident.Name
134+
135+
// Check if this type is one of our string aliases
136+
if _, ok := stringAliases[typeName]; !ok {
137+
continue
138+
}
139+
140+
// Extract string literal values
141+
for _, value := range valueSpec.Values {
142+
basicLit, ok := value.(*ast.BasicLit)
143+
if !ok || basicLit.Kind != token.STRING {
144+
continue
145+
}
146+
147+
// Remove quotes from string literal
148+
constantValue := strings.Trim(basicLit.Value, "\"")
149+
150+
// Find or create the StringAliasInfo for this type
151+
var aliasInfo *StringAliasInfo
152+
for i := range result {
153+
if result[i].Name == typeName {
154+
aliasInfo = &result[i]
155+
break
156+
}
157+
}
158+
159+
if aliasInfo == nil {
160+
result = append(result, StringAliasInfo{
161+
Name: typeName,
162+
Constants: []string{},
163+
})
164+
aliasInfo = &result[len(result)-1]
165+
}
166+
167+
aliasInfo.Constants = append(aliasInfo.Constants, constantValue)
168+
}
169+
}
170+
171+
return true
172+
})
173+
174+
return result
175+
}
176+
177+
// batchDetectConstantsForTypes efficiently detects constants for multiple types in a single file system walk
178+
func batchDetectConstantsForTypes(typeNames []string, searchPath string) map[string][]string {
179+
result := make(map[string][]string)
180+
181+
// Initialize result map
182+
for _, typeName := range typeNames {
183+
result[typeName] = []string{}
184+
}
185+
186+
// Walk the specified directory to find Go files
187+
err := filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
188+
if err != nil {
189+
return err
190+
}
191+
192+
// Skip directories and non-Go files
193+
if info.IsDir() || !strings.HasSuffix(path, ".go") {
194+
return nil
195+
}
196+
197+
// Skip test files and our own tool files
198+
if strings.Contains(path, "_test.go") || strings.Contains(path, "scripts/jsonrpc_typings") {
199+
return nil
200+
}
201+
202+
// Check if this file contains any of the types we're looking for
203+
fileContainsAnyType := false
204+
for _, typeName := range typeNames {
205+
if fileContainsType(path, typeName) {
206+
fileContainsAnyType = true
207+
break
208+
}
209+
}
210+
211+
if !fileContainsAnyType {
212+
return nil
213+
}
214+
215+
log.Debug().Str("file", path).Strs("types", typeNames).Msg("Parsing file for constants")
216+
217+
// Parse constants for all types from this file
218+
fileConstants := batchParseConstantsFromFile(path, typeNames)
219+
220+
// Merge results
221+
for typeName, constants := range fileConstants {
222+
if len(constants) > 0 {
223+
result[typeName] = constants
224+
log.Debug().Str("type", typeName).Strs("constants", constants).Str("file", path).Msg("Found constants")
225+
}
226+
}
227+
228+
return nil
229+
})
230+
231+
if err != nil {
232+
log.Fatal().Err(err).Msg("Error searching for constants")
233+
}
234+
235+
return result
236+
}
237+
238+
// batchParseConstantsFromFile parses constants for multiple types from a single Go file
239+
func batchParseConstantsFromFile(filePath string, typeNames []string) map[string][]string {
240+
result := make(map[string][]string)
241+
242+
// Initialize result map
243+
for _, typeName := range typeNames {
244+
result[typeName] = []string{}
245+
}
246+
247+
// Parse the Go file
248+
fset := token.NewFileSet()
249+
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
250+
if err != nil {
251+
log.Debug().Err(err).Str("file", filePath).Msg("Failed to parse file")
252+
return result
253+
}
254+
255+
// Walk the AST to find constant declarations
256+
ast.Inspect(node, func(n ast.Node) bool {
257+
// Look for GenDecl nodes (const declarations)
258+
genDecl, ok := n.(*ast.GenDecl)
259+
if !ok || genDecl.Tok != token.CONST {
260+
return true
261+
}
262+
263+
// Process each constant specification in the declaration
264+
for _, spec := range genDecl.Specs {
265+
valueSpec, ok := spec.(*ast.ValueSpec)
266+
if !ok {
267+
continue
268+
}
269+
270+
// Check if this constant is typed with one of our target types
271+
if valueSpec.Type != nil {
272+
if ident, ok := valueSpec.Type.(*ast.Ident); ok {
273+
typeName := ident.Name
274+
275+
// Check if this type is one we're looking for
276+
if contains(typeNames, typeName) {
277+
// Extract string literal values
278+
for _, value := range valueSpec.Values {
279+
if basicLit, ok := value.(*ast.BasicLit); ok && basicLit.Kind == token.STRING {
280+
// Remove quotes from string literal
281+
constantValue := strings.Trim(basicLit.Value, "\"")
282+
result[typeName] = append(result[typeName], constantValue)
283+
}
284+
}
285+
}
286+
}
287+
}
288+
}
289+
290+
return true
291+
})
292+
293+
return result
294+
}
295+
296+
// contains checks if a slice contains a string
297+
func contains(slice []string, item string) bool {
298+
for _, s := range slice {
299+
if s == item {
300+
return true
301+
}
302+
}
303+
return false
304+
}
305+
306+
// fileContainsType checks if a Go file contains a type definition for the given type name
307+
func fileContainsType(filePath, typeName string) bool {
308+
fset := token.NewFileSet()
309+
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
310+
if err != nil {
311+
return false
312+
}
313+
314+
// Walk the AST to find type definitions
315+
found := false
316+
ast.Inspect(node, func(n ast.Node) bool {
317+
switch x := n.(type) {
318+
case *ast.GenDecl:
319+
if x.Tok == token.TYPE {
320+
for _, spec := range x.Specs {
321+
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
322+
if typeSpec.Name.Name == typeName {
323+
found = true
324+
return false // Stop searching
325+
}
326+
}
327+
}
328+
}
329+
}
330+
return !found // Continue searching if not found yet
331+
})
332+
333+
return found
334+
}

0 commit comments

Comments
 (0)