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