jsenzyme.go raw
1 package enzyme
2
3 import (
4 "bufio"
5 "io"
6 "regexp"
7 "strings"
8
9 "git.mleku.dev/mleku/dendrite/pkg/axiom"
10 )
11
12 // JSSource is an enzyme that decomposes JavaScript/TypeScript/Svelte source
13 // into typed AST-like elements using pattern recognition.
14 //
15 // It does not do full parsing — it recognizes structural markers: imports,
16 // exports, classes, types, interfaces, functions, methods, fields, and
17 // string literals. This gives the lattice the vocabulary of a JS/TS codebase
18 // without requiring a Node.js runtime.
19 type JSSource struct{}
20
21 // CanDigest returns true if the sample looks like JS/TS/Svelte source.
22 func (JSSource) CanDigest(sample []byte) bool {
23 s := string(sample)
24 return strings.Contains(s, "import ") ||
25 strings.Contains(s, "export ") ||
26 strings.Contains(s, "function ") ||
27 strings.Contains(s, "<script")
28 }
29
30 var (
31 // Import patterns.
32 reImportFrom = regexp.MustCompile(`import\s+(?:\{[^}]*\}|[^{;]+)\s+from\s+['"]([^'"]+)['"]`)
33 reImportBare = regexp.MustCompile(`import\s+['"]([^'"]+)['"]`)
34
35 // Export/declaration patterns.
36 reExportClass = regexp.MustCompile(`(?:export\s+)?class\s+(\w+)`)
37 reExportType = regexp.MustCompile(`(?:export\s+)?type\s+(\w+)\s*[=<{]`)
38 reExportInterface = regexp.MustCompile(`(?:export\s+)?interface\s+(\w+)`)
39 reExportFunction = regexp.MustCompile(`(?:export\s+)?(?:async\s+)?function\s+(\w+)`)
40 reExportConst = regexp.MustCompile(`(?:export\s+)?(?:const|let|var)\s+(\w+)`)
41 reEnum = regexp.MustCompile(`(?:export\s+)?enum\s+(\w+)`)
42
43 // Class members.
44 reMethod = regexp.MustCompile(`^\s+(?:(?:public|private|protected|static|async|readonly)\s+)*(\w+)\s*\(`)
45 reGetter = regexp.MustCompile(`^\s+(?:(?:public|private|protected|static)\s+)*get\s+(\w+)\s*\(`)
46 reSetter = regexp.MustCompile(`^\s+(?:(?:public|private|protected|static)\s+)*set\s+(\w+)\s*\(`)
47 reField = regexp.MustCompile(`^\s+(?:(?:public|private|protected|static|readonly)\s+)*(\w+)\s*[?!]?\s*:\s*`)
48 reArrowFn = regexp.MustCompile(`(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(`)
49 reArrowFn2 = regexp.MustCompile(`(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\w+\s*=>`)
50
51 // String literals — single and double quoted.
52 reSingleString = regexp.MustCompile(`'((?:[^'\\]|\\.){3,})'`)
53 reDoubleString = regexp.MustCompile(`"((?:[^"\\]|\\.){3,})"`)
54 reTemplateStr = regexp.MustCompile("(`[^`]{3,}`)")
55
56 // Svelte component tags.
57 reSvelteComponent = regexp.MustCompile(`<([A-Z]\w+)`)
58
59 // JSDoc/comment tags.
60 reComment = regexp.MustCompile(`^\s*(?://|/\*|\*)`)
61 )
62
63 // Digest scans JS/TS/Svelte source and emits typed elements.
64 func (JSSource) Digest(r io.Reader) <-chan axiom.Element {
65 ch := make(chan axiom.Element, 128)
66
67 go func() {
68 defer close(ch)
69
70 scanner := bufio.NewScanner(r)
71 scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
72 inClass := false
73 braceDepth := 0
74 classDepth := 0
75
76 for scanner.Scan() {
77 line := scanner.Text()
78
79 // Track brace depth for class scope detection.
80 for _, c := range line {
81 if c == '{' {
82 braceDepth++
83 } else if c == '}' {
84 braceDepth--
85 if inClass && braceDepth < classDepth {
86 inClass = false
87 }
88 }
89 }
90
91 // Skip pure comments — emit as comment elements.
92 if reComment.MatchString(line) {
93 trimmed := strings.TrimSpace(line)
94 trimmed = strings.TrimLeft(trimmed, "/* ")
95 if len(trimmed) > 3 {
96 ch <- newHexElement("comment", trimmed)
97 }
98 continue
99 }
100
101 // Imports.
102 if m := reImportFrom.FindStringSubmatch(line); m != nil {
103 ch <- newHexElement("import", m[1])
104 // Also extract imported identifiers.
105 if idx := strings.Index(line, "{"); idx >= 0 {
106 if end := strings.Index(line[idx:], "}"); end >= 0 {
107 names := line[idx+1 : idx+end]
108 for _, name := range strings.Split(names, ",") {
109 name = strings.TrimSpace(name)
110 if as := strings.Index(name, " as "); as >= 0 {
111 name = strings.TrimSpace(name[as+4:])
112 }
113 if name != "" {
114 ch <- newHexElement("ident", name)
115 }
116 }
117 }
118 }
119 continue
120 }
121 if m := reImportBare.FindStringSubmatch(line); m != nil {
122 ch <- newHexElement("import", m[1])
123 continue
124 }
125
126 // Class declarations.
127 if m := reExportClass.FindStringSubmatch(line); m != nil {
128 ch <- newHexElement("type", m[1])
129 ch <- newHexElement("struct", "")
130 inClass = true
131 classDepth = braceDepth
132 continue
133 }
134
135 // Type aliases.
136 if m := reExportType.FindStringSubmatch(line); m != nil {
137 ch <- newHexElement("type", m[1])
138 continue
139 }
140
141 // Interfaces.
142 if m := reExportInterface.FindStringSubmatch(line); m != nil {
143 ch <- newHexElement("type", m[1])
144 ch <- newHexElement("interface", "")
145 continue
146 }
147
148 // Enums.
149 if m := reEnum.FindStringSubmatch(line); m != nil {
150 ch <- newHexElement("type", m[1])
151 continue
152 }
153
154 // Functions (top-level or exported).
155 if m := reExportFunction.FindStringSubmatch(line); m != nil {
156 ch <- newHexElement("func", m[1])
157 continue
158 }
159
160 // Arrow functions assigned to const/let/var.
161 if m := reArrowFn.FindStringSubmatch(line); m != nil {
162 if !inClass {
163 ch <- newHexElement("func", m[1])
164 }
165 continue
166 }
167 if m := reArrowFn2.FindStringSubmatch(line); m != nil {
168 if !inClass {
169 ch <- newHexElement("func", m[1])
170 }
171 continue
172 }
173
174 // Exported constants (that aren't arrow functions).
175 if m := reExportConst.FindStringSubmatch(line); m != nil {
176 if !inClass {
177 ch <- newHexElement("ident", m[1])
178 }
179 continue
180 }
181
182 // Class members.
183 if inClass {
184 // Getters.
185 if m := reGetter.FindStringSubmatch(line); m != nil {
186 ch <- newHexElement("method", m[1])
187 continue
188 }
189 // Setters.
190 if m := reSetter.FindStringSubmatch(line); m != nil {
191 ch <- newHexElement("method", m[1])
192 continue
193 }
194 // Methods.
195 if m := reMethod.FindStringSubmatch(line); m != nil {
196 name := m[1]
197 // Skip keywords that look like methods.
198 if name != "if" && name != "for" && name != "while" &&
199 name != "switch" && name != "return" && name != "constructor" &&
200 name != "new" && name != "throw" && name != "catch" {
201 ch <- newHexElement("method", name)
202 }
203 continue
204 }
205 // Fields.
206 if m := reField.FindStringSubmatch(line); m != nil {
207 name := m[1]
208 if name != "return" && name != "const" && name != "let" {
209 ch <- newHexElement("field", name)
210 }
211 continue
212 }
213 }
214
215 // Svelte component references.
216 if matches := reSvelteComponent.FindAllStringSubmatch(line, -1); matches != nil {
217 for _, m := range matches {
218 ch <- newHexElement("ident", m[1])
219 }
220 }
221
222 // String literals.
223 for _, re := range []*regexp.Regexp{reSingleString, reDoubleString, reTemplateStr} {
224 if matches := re.FindAllStringSubmatch(line, -1); matches != nil {
225 for _, m := range matches {
226 ch <- newHexElement("literal", m[0])
227 }
228 }
229 }
230 }
231 }()
232
233 return ch
234 }
235