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
147 lines
3.6 KiB
Go
147 lines
3.6 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
)
|
|
|
|
// JSON has no symbol semantics, but we still want CLAUDE.md /
|
|
// package.json / tsconfig keys to show up in the graph so they can
|
|
// be referenced and searched. Extract top-level keys of the outermost
|
|
// object as variable nodes. Nested keys are intentionally skipped.
|
|
type JSONExtractor struct{}
|
|
|
|
func NewJSONExtractor() *JSONExtractor { return &JSONExtractor{} }
|
|
|
|
func (e *JSONExtractor) Language() string { return "json" }
|
|
func (e *JSONExtractor) Extensions() []string { return []string{".json", ".json5", ".jsonc"} }
|
|
|
|
func (e *JSONExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
|
|
lines := strings.Split(string(src), "\n")
|
|
result := &parser.ExtractionResult{}
|
|
|
|
fileNode := &graph.Node{
|
|
ID: filePath, Kind: graph.KindFile, Name: filePath,
|
|
FilePath: filePath, StartLine: 1, EndLine: len(lines),
|
|
Language: "json",
|
|
}
|
|
result.Nodes = append(result.Nodes, fileNode)
|
|
|
|
seen := make(map[string]bool)
|
|
add := func(name string, start int) {
|
|
if name == "" {
|
|
return
|
|
}
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindVariable, Name: name,
|
|
FilePath: filePath, StartLine: start, EndLine: start,
|
|
Language: "json",
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileNode.ID, To: id, Kind: graph.EdgeDefines,
|
|
FilePath: filePath, Line: start,
|
|
})
|
|
}
|
|
|
|
// Manual scan: track brace/bracket depth while skipping strings and
|
|
// comments (JSON5/JSONC). Emit each string key that sits at depth == 1
|
|
// inside an object (not an array) and is followed by a `:`.
|
|
//
|
|
// Depth convention: incremented on `{` / `[`, decremented on `}` / `]`.
|
|
// A key is "top-level" when the innermost open container is the
|
|
// outermost `{` — i.e. depth transitions 0 -> 1 on `{`, and we accept
|
|
// keys at depth == 1 while the innermost container is an object.
|
|
var stack []byte // tracks nested container chars: '{' or '['
|
|
line := 1
|
|
i := 0
|
|
n := len(src)
|
|
for i < n {
|
|
c := src[i]
|
|
switch {
|
|
case c == '\n':
|
|
line++
|
|
i++
|
|
case c == '/' && i+1 < n && src[i+1] == '/':
|
|
// Line comment (JSON5/JSONC).
|
|
for i < n && src[i] != '\n' {
|
|
i++
|
|
}
|
|
case c == '/' && i+1 < n && src[i+1] == '*':
|
|
i += 2
|
|
for i+1 < n && (src[i] != '*' || src[i+1] != '/') {
|
|
if src[i] == '\n' {
|
|
line++
|
|
}
|
|
i++
|
|
}
|
|
if i+1 < n {
|
|
i += 2
|
|
}
|
|
case c == '{' || c == '[':
|
|
stack = append(stack, c)
|
|
i++
|
|
case c == '}' || c == ']':
|
|
if len(stack) > 0 {
|
|
stack = stack[:len(stack)-1]
|
|
}
|
|
i++
|
|
case c == '"':
|
|
// String literal — capture the contents.
|
|
keyStart := i + 1
|
|
j := i + 1
|
|
for j < n {
|
|
if src[j] == '\\' && j+1 < n {
|
|
if src[j+1] == '\n' {
|
|
line++
|
|
}
|
|
j += 2
|
|
continue
|
|
}
|
|
if src[j] == '"' {
|
|
break
|
|
}
|
|
if src[j] == '\n' {
|
|
line++
|
|
}
|
|
j++
|
|
}
|
|
if j >= n {
|
|
i = n
|
|
break
|
|
}
|
|
key := string(src[keyStart:j])
|
|
// Move past closing quote.
|
|
i = j + 1
|
|
// Look ahead past whitespace/comments for `:` — if present at
|
|
// depth 1 inside an object, it's a top-level key.
|
|
k := i
|
|
for k < n {
|
|
if src[k] == ' ' || src[k] == '\t' || src[k] == '\r' || src[k] == '\n' {
|
|
k++
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
if k < n && src[k] == ':' {
|
|
// At depth 1 means stack length is exactly 1 and top is '{'.
|
|
if len(stack) == 1 && stack[0] == '{' {
|
|
add(key, line)
|
|
}
|
|
}
|
|
default:
|
|
i++
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
var _ parser.Extractor = (*JSONExtractor)(nil)
|