a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
134 lines
4.0 KiB
Go
134 lines
4.0 KiB
Go
package contracts
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// WebSocketExtractor detects WebSocket event emit (provider) and listen (consumer) patterns.
|
|
type WebSocketExtractor struct{}
|
|
|
|
var (
|
|
// Emit patterns (providers).
|
|
wsEmitPatterns = []*regexp.Regexp{
|
|
// socket.emit("event"
|
|
regexp.MustCompile(`\.emit\(\s*"([^"]+)"`),
|
|
// ws.send(JSON.stringify({type: "event"
|
|
regexp.MustCompile(`\.send\(\s*JSON\.stringify\(\s*\{\s*type:\s*"([^"]+)"`),
|
|
// conn.WriteJSON(map{"type": "event"
|
|
regexp.MustCompile(`WriteJSON\([^)]*"type":\s*"([^"]+)"`),
|
|
}
|
|
|
|
// Listen patterns (consumers).
|
|
wsListenPatterns = []*regexp.Regexp{
|
|
// socket.on("event"
|
|
regexp.MustCompile(`\.on\(\s*"([^"]+)"`),
|
|
// ws.addEventListener("message"
|
|
regexp.MustCompile(`\.addEventListener\(\s*"([^"]+)"`),
|
|
}
|
|
)
|
|
|
|
func (e *WebSocketExtractor) SupportedLanguages() []string {
|
|
return []string{"go", "typescript", "javascript", "python"}
|
|
}
|
|
|
|
// wsPrefilterMarkers covers every emit/listen regex:
|
|
// - `.emit(` → socket.io emit
|
|
// - `JSON.stringify` + `type:` → raw WS send with typed payload
|
|
// - `WriteJSON` + `"type":` → Go gorilla/websocket idiom
|
|
// - `.on(` / `.addEventListener(` → listeners
|
|
//
|
|
// `.on(` is intentionally loose (matches any event listener); we
|
|
// accept false positives from non-WS code so we don't miss legit
|
|
// WS handlers on raw `ws.on("message", ...)`.
|
|
var wsPrefilterMarkers = [][]byte{
|
|
[]byte(".emit("),
|
|
[]byte(".on("),
|
|
[]byte(".addEventListener("),
|
|
[]byte("WriteJSON"),
|
|
[]byte("JSON.stringify"),
|
|
[]byte("@SubscribeMessage("), // NestJS WebSocketGateway message handler
|
|
}
|
|
|
|
// wsSubscribeMessageRe matches a NestJS @SubscribeMessage('event') handler.
|
|
var wsSubscribeMessageRe = regexp.MustCompile(`@SubscribeMessage\(\s*["']([^"']+)["']`)
|
|
|
|
func (e *WebSocketExtractor) Extract(filePath string, src []byte, nodes []*graph.Node, edges []*graph.Edge) []Contract {
|
|
if !srcHasAnyMarker(src, wsPrefilterMarkers) {
|
|
return nil
|
|
}
|
|
|
|
var contracts []Contract
|
|
text := string(src)
|
|
lines := strings.Split(text, "\n")
|
|
|
|
fileNodes := filterFileNodes(filePath, nodes)
|
|
sort.Slice(fileNodes, func(i, j int) bool {
|
|
return fileNodes[i].StartLine < fileNodes[j].StartLine
|
|
})
|
|
|
|
for _, re := range wsEmitPatterns {
|
|
for _, m := range re.FindAllStringSubmatchIndex(text, -1) {
|
|
event := text[m[2]:m[3]]
|
|
ln := lineNumber(lines, m[0])
|
|
contracts = append(contracts, Contract{
|
|
ID: fmt.Sprintf("ws::%s", event),
|
|
Type: ContractWS,
|
|
Role: RoleProvider,
|
|
SymbolID: findEnclosingSymbol(fileNodes, ln),
|
|
FilePath: filePath,
|
|
Line: ln,
|
|
Meta: map[string]any{"event": event},
|
|
Confidence: 0.85,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, re := range wsListenPatterns {
|
|
for _, m := range re.FindAllStringSubmatchIndex(text, -1) {
|
|
event := text[m[2]:m[3]]
|
|
ln := lineNumber(lines, m[0])
|
|
contracts = append(contracts, Contract{
|
|
ID: fmt.Sprintf("ws::%s", event),
|
|
Type: ContractWS,
|
|
Role: RoleConsumer,
|
|
SymbolID: findEnclosingSymbol(fileNodes, ln),
|
|
FilePath: filePath,
|
|
Line: ln,
|
|
Meta: map[string]any{"event": event},
|
|
Confidence: 0.8,
|
|
})
|
|
}
|
|
}
|
|
|
|
// NestJS WebSocketGateway message handlers: @SubscribeMessage('event') is
|
|
// a server-side handler for an inbound message — a provider of that event.
|
|
for _, m := range wsSubscribeMessageRe.FindAllStringSubmatchIndex(text, -1) {
|
|
event := text[m[2]:m[3]]
|
|
ln := lineNumber(lines, m[0])
|
|
handlerName, _ := nestHandlerAfter(lines, ln-1)
|
|
symbolID := findEnclosingSymbol(fileNodes, ln)
|
|
if handlerName != "" {
|
|
if hID := findFunctionByName(fileNodes, handlerName); hID != "" {
|
|
symbolID = hID
|
|
}
|
|
}
|
|
contracts = append(contracts, Contract{
|
|
ID: fmt.Sprintf("ws::%s", event),
|
|
Type: ContractWS,
|
|
Role: RoleProvider,
|
|
SymbolID: symbolID,
|
|
FilePath: filePath,
|
|
Line: ln,
|
|
Meta: map[string]any{"event": event, "framework": "nestjs"},
|
|
Confidence: 0.9,
|
|
})
|
|
}
|
|
|
|
return contracts
|
|
}
|