devrites 3.2.5 → 3.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/docs/cli.md +0 -1
  4. package/docs/command-map.md +1 -1
  5. package/docs/orchestration.md +1 -2
  6. package/engine/commands.go +1 -1
  7. package/engine/hooks_agent_dispatch.go +98 -33
  8. package/engine/internal/lib/reconcile.go +1 -1
  9. package/engine/internal/lib/reconcile_test.go +4 -12
  10. package/engine/internal/lib/util.go +2 -2
  11. package/engine/main.go +0 -3
  12. package/engine/root_routing_test.go +0 -1
  13. package/engine/tests/hook_test.go +235 -0
  14. package/pack/.claude/skills/rite-build/reference/forge.md +2 -2
  15. package/pack/.claude/skills/rite-build/reference/phase-contract.md +11 -20
  16. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +2 -5
  17. package/pack/.claude/skills/rite-prove/reference/acceptance-proof.md +0 -1
  18. package/pack/.claude/skills/rite-seal/reference/final-evidence.md +0 -1
  19. package/pack/generated/claude/skills/rite-build/reference/forge.md +2 -2
  20. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +11 -20
  21. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +2 -5
  22. package/pack/generated/claude/skills/rite-prove/reference/acceptance-proof.md +0 -1
  23. package/pack/generated/claude/skills/rite-seal/reference/final-evidence.md +0 -1
  24. package/pack/generated/codex/skills/rite-build/reference/forge.md +2 -2
  25. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +11 -20
  26. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +2 -5
  27. package/pack/generated/codex/skills/rite-prove/reference/acceptance-proof.md +0 -1
  28. package/pack/generated/codex/skills/rite-seal/reference/final-evidence.md +0 -1
  29. package/package.json +1 -1
  30. package/scripts/build-release-tarball.sh +1 -0
  31. package/scripts/validate.sh +0 -1
  32. package/engine/internal/lib/packageexistence.go +0 -968
  33. package/engine/internal/lib/packageexistence_test.go +0 -506
  34. package/engine/testdata/golden/TestParityPackageExistence/declared.golden +0 -2
  35. package/engine/testdata/golden/TestParityPackageExistence/default-import-subpath.golden +0 -2
  36. package/engine/tests/parity_packageexistence_test.go +0 -64
@@ -1,968 +0,0 @@
1
- package lib
2
-
3
- import (
4
- "encoding/json"
5
- "fmt"
6
- "go/parser"
7
- "go/token"
8
- "io"
9
- "os"
10
- "path/filepath"
11
- "regexp"
12
- "sort"
13
- "strconv"
14
- "strings"
15
- )
16
-
17
- // Import patterns are language-specific: JS/TS uses quoted specifiers, Go uses
18
- // the standard parser, and Python/Rust use their import declaration syntax.
19
- var (
20
- quotedModule = regexp.MustCompile(`\b(?:from|import|require)[[:space:]]*(?:\([[:space:]]*)?['"]([^'"]+)['"]`)
21
- cssImport = regexp.MustCompile(`(?i)@import[[:space:]]+(?:url\([[:space:]]*)?(?:['"]([^'"]+)['"]|([^[:space:]);]+))`)
22
- remoteImport = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9+.-]*:`)
23
- pythonImport = regexp.MustCompile(`^\+?[[:space:]]*import[[:space:]]+([A-Za-z0-9_./-]+)`)
24
- pythonFrom = regexp.MustCompile(`^\+?[[:space:]]*from[[:space:]]+([A-Za-z0-9_./-]+)[[:space:]]+import\b`)
25
- rustUse = regexp.MustCompile(`^[[:space:]]*(?:pub(?:\([^)]*\))?[[:space:]]+)?use[[:space:]]+(?:::)?(?:r#)?([A-Za-z_][A-Za-z0-9_]*)(?:::|[[:space:]]+as\b|[[:space:]]*;)`)
26
- rustExtern = regexp.MustCompile(`^[[:space:]]*(?:pub[[:space:]]+)?extern[[:space:]]+crate[[:space:]]+(?:r#)?([A-Za-z_][A-Za-z0-9_]*)\b`)
27
- requirement = regexp.MustCompile(`^[[:space:]]*([A-Za-z0-9][A-Za-z0-9._-]*)[[:space:]]*(?:\[[^]]*\])?[[:space:]]*(?:@|[<>=!~;]|$)`)
28
- quotedValue = regexp.MustCompile(`["']([^"']+)["']`)
29
- scopedPackage = regexp.MustCompile(`^(@[^/]+/[^/]+).*`) // @scope/name/... -> @scope/name
30
- firstSegment = regexp.MustCompile(`^([^@][^/]+)/.*`) // name/sub/... -> name
31
- localPath = regexp.MustCompile(`^(\.|/|[A-Z]:)`) // relative / absolute / drive path
32
- // stdlib and framework-builtin prefixes that need no manifest entry.
33
- builtinPackage = regexp.MustCompile(`^(os|sys|re|json|math|time|datetime|typing|collections|itertools|functools|pathlib|subprocess|logging|fmt|errors|context|strings|strconv|net|http|io|bufio|std|core|alloc|crate|self|super|react|node:|fs|path|util|crypto|events|stream|child_process)$`)
34
- )
35
-
36
- // manifestFiles are the dependency manifests a declared package can appear in.
37
- var manifestFiles = []string{"package.json", "requirements.txt", "pyproject.toml", "Pipfile", "go.mod", "Cargo.toml"}
38
-
39
- type importedModule struct {
40
- sourcePath string
41
- specifier string
42
- }
43
-
44
- type packageResolution struct {
45
- name string
46
- resolved bool
47
- manifestFound bool
48
- ignored bool
49
- }
50
-
51
- type packageResolver struct {
52
- extensions string
53
- extract func(string, string, []string) []string
54
- resolve func(string, string, string) packageResolution
55
- }
56
-
57
- var packageResolvers = []packageResolver{
58
- {extensions: " .js .jsx .mjs .cjs .ts .tsx .mts .cts .svelte .vue ", extract: quotedModulesFromAddedLines, resolve: resolveJavaScriptImport},
59
- {extensions: " .css ", extract: cssModulesFromAddedLines, resolve: resolveCSSImport},
60
- {extensions: " .go ", extract: goModulesFromAddedLinesForSource, resolve: resolveGoImport},
61
- {extensions: " .py .pyi ", extract: pythonModulesFromAddedLines, resolve: resolvePythonImport},
62
- {extensions: " .rs ", extract: rustModulesFromAddedLines, resolve: resolveRustImport},
63
- }
64
-
65
- var fallbackPackageResolver = packageResolver{extract: quotedModulesFromAddedLines, resolve: resolveGenericImport}
66
-
67
- // PackageExistence checks that every new third-party import in the slice diff is
68
- // declared in a project manifest. It works offline and does not query a package
69
- // registry. The workspace is <root>/features/<slug>.
70
- //
71
- // 0 clean, nothing to check, or skipped (not a git repo / no manifest)
72
- // 2 no active workspace or missing/corrupt baseline state
73
- // 3 an imported package is not declared in any manifest
74
- func PackageExistence(root string, args []string, stdout, stderr io.Writer) int {
75
- slug := slugOrActive(root, args)
76
- if slug == "" {
77
- fmt.Fprintln(stderr, "package-existence: no active workspace.")
78
- return 2
79
- }
80
- d := featureDir(root, slug)
81
- if !isDir(d) {
82
- fmt.Fprintf(stderr, "package-existence: no workspace at %s.\n", d)
83
- return 2
84
- }
85
-
86
- cwd, _ := os.Getwd()
87
- gitRoot := gitToplevel(cwd)
88
- if gitRoot == "" {
89
- fmt.Fprintln(stderr, "package-existence: not a git repo: skipped.")
90
- return 0
91
- }
92
- trees, err := captureSliceTreeRange(gitRoot, root, d)
93
- if err != nil {
94
- fmt.Fprintf(stderr, "package-existence: cannot capture slice range: %v\n", err)
95
- return 2
96
- }
97
- defer trees.cleanup()
98
-
99
- var rootManifests []string
100
- for _, m := range manifestFiles {
101
- p := filepath.Join(gitRoot, m)
102
- if isFile(p) {
103
- rootManifests = append(rootManifests, p)
104
- }
105
- }
106
-
107
- imports, err := extractImportedPackages(gitRoot, trees.base, trees.current, trees.env)
108
- if err != nil {
109
- fmt.Fprintf(stderr, "package-existence: cannot inspect slice imports: %v\n", err)
110
- return 2
111
- }
112
- hasManifest := len(rootManifests) > 0
113
- findings := map[string]struct{}{}
114
- for _, imported := range imports {
115
- result := resolverForSource(imported.sourcePath).resolve(gitRoot, imported.sourcePath, imported.specifier)
116
- if result.ignored {
117
- continue
118
- }
119
- if result.manifestFound {
120
- hasManifest = true
121
- }
122
- if result.resolved {
123
- continue
124
- }
125
- findings[result.name] = struct{}{}
126
- }
127
-
128
- if !hasManifest {
129
- fmt.Fprintln(stderr, "package-existence: no recognized manifest: skipped.")
130
- return 0
131
- }
132
-
133
- if len(findings) > 0 {
134
- packages := make([]string, 0, len(findings))
135
- for pkg := range findings {
136
- packages = append(packages, pkg)
137
- }
138
- sort.Strings(packages)
139
- fmt.Fprintf(stderr, "package-existence: %d imported package(s) are NOT declared in a manifest: verify each exists and add it via the package manager:\n", len(packages))
140
- for _, pkg := range packages {
141
- fmt.Fprintf(stderr, " - %s: imported but not declared in any manifest\n", pkg)
142
- }
143
- fmt.Fprintln(stderr, "Check each package name in its registry before trusting it. Undeclared imports may be misspelled, typo-squatted, or nonexistent.")
144
- return 3
145
- }
146
- fmt.Fprintln(stdout, "package-existence: OK: every new third-party import is declared in a manifest.")
147
- return 0
148
- }
149
-
150
- // extractImportedPackages retains the source file and raw specifier for each
151
- // import introduced between two immutable trees. CSS is parsed from complete
152
- // baseline/current files so block-comment state from unchanged lines is
153
- // preserved; other languages retain the established added-line extraction.
154
- func extractImportedPackages(gitRoot, baseTree, currentTree string, env []string) ([]importedModule, error) {
155
- paths, err := changedTreePaths(gitRoot, env, baseTree, currentTree)
156
- if err != nil {
157
- return nil, err
158
- }
159
-
160
- var imports []importedModule
161
- for _, sourcePath := range paths {
162
- currentSource, currentExists, err := treeFileContent(gitRoot, env, currentTree, sourcePath)
163
- if err != nil {
164
- return nil, err
165
- }
166
- if !currentExists {
167
- continue
168
- }
169
-
170
- var specifiers []string
171
- if strings.EqualFold(filepath.Ext(sourcePath), ".css") {
172
- baselineSource, _, err := treeFileContent(gitRoot, env, baseTree, sourcePath)
173
- if err != nil {
174
- return nil, err
175
- }
176
- specifiers = addedCSSModules(baselineSource, currentSource)
177
- } else {
178
- diff, err := reconcileGitOutput(
179
- gitRoot,
180
- env,
181
- "diff", "--unified=0", "--no-renames", baseTree, currentTree, "--", sourcePath,
182
- )
183
- if err != nil {
184
- return nil, err
185
- }
186
- var addedLines []string
187
- for _, line := range splitLinesNoTrailing(diff) {
188
- if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
189
- addedLines = append(addedLines, strings.TrimPrefix(line, "+"))
190
- }
191
- }
192
- specifiers = importedModulesFromAddedLines(gitRoot, sourcePath, addedLines)
193
- }
194
- for _, specifier := range specifiers {
195
- imports = append(imports, importedModule{sourcePath: sourcePath, specifier: specifier})
196
- }
197
- }
198
- return imports, nil
199
- }
200
-
201
- func importedModulesFromAddedLines(gitRoot, sourcePath string, lines []string) []string {
202
- return resolverForSource(sourcePath).extract(gitRoot, sourcePath, lines)
203
- }
204
-
205
- func resolverForSource(sourcePath string) packageResolver {
206
- extension := " " + strings.ToLower(filepath.Ext(sourcePath)) + " "
207
- for _, resolver := range packageResolvers {
208
- if strings.Contains(resolver.extensions, extension) {
209
- return resolver
210
- }
211
- }
212
- return fallbackPackageResolver
213
- }
214
-
215
- func resolveJavaScriptImport(gitRoot, sourcePath, specifier string) packageResolution {
216
- pkg := normalizeImportedPackage(specifier)
217
- if pkg == "" || builtinPackage.MatchString(pkg) {
218
- return packageResolution{ignored: true}
219
- }
220
- manifest := nearestFile(gitRoot, sourcePath, "package.json")
221
- return packageResolution{
222
- name: pkg,
223
- resolved: importMatchesPathAlias(gitRoot, sourcePath, specifier) || manifest != "" && packageJSONDeclares(pkg, manifest),
224
- manifestFound: manifest != "",
225
- }
226
- }
227
-
228
- func resolveCSSImport(gitRoot, sourcePath, specifier string) packageResolution {
229
- specifier = strings.TrimSpace(specifier)
230
- if remoteImport.MatchString(specifier) || strings.HasPrefix(specifier, "//") {
231
- return packageResolution{ignored: true}
232
- }
233
- if cssSourceRelativePathExists(gitRoot, sourcePath, specifier) {
234
- return packageResolution{resolved: true}
235
- }
236
- return resolveJavaScriptImport(gitRoot, sourcePath, specifier)
237
- }
238
-
239
- func resolveGoImport(gitRoot, sourcePath, specifier string) packageResolution {
240
- if isGoStandardLibrary(specifier) {
241
- return packageResolution{ignored: true}
242
- }
243
- resolved, manifestFound := goImportResolved(gitRoot, sourcePath, specifier)
244
- return packageResolution{name: specifier, resolved: resolved, manifestFound: manifestFound}
245
- }
246
-
247
- func resolvePythonImport(gitRoot, sourcePath, specifier string) packageResolution {
248
- module := topLevelPythonModule(specifier)
249
- if module == "" || isPythonStandardLibrary(module) {
250
- return packageResolution{ignored: true}
251
- }
252
- resolved, manifestFound := pythonImportResolved(gitRoot, sourcePath, normalizeDistributionName(module))
253
- return packageResolution{name: module, resolved: resolved, manifestFound: manifestFound}
254
- }
255
-
256
- func resolveRustImport(gitRoot, sourcePath, specifier string) packageResolution {
257
- crate := normalizeRustCrate(specifier)
258
- if crate == "" || isRustBuiltin(crate) {
259
- return packageResolution{ignored: true}
260
- }
261
- resolved, manifestFound := rustImportResolved(gitRoot, sourcePath, crate)
262
- return packageResolution{name: crate, resolved: resolved, manifestFound: manifestFound}
263
- }
264
-
265
- // resolveGenericImport sends quoted imports from unregistered source types
266
- // through the shared pipeline. Without ecosystem rules, a nearby manifest is
267
- // recorded but cannot prove that the import is declared.
268
- func resolveGenericImport(gitRoot, sourcePath, specifier string) packageResolution {
269
- pkg := normalizeImportedPackage(specifier)
270
- if pkg == "" {
271
- return packageResolution{ignored: true}
272
- }
273
- return packageResolution{
274
- name: pkg,
275
- manifestFound: len(nearestFiles(gitRoot, sourcePath, manifestFiles)) > 0,
276
- }
277
- }
278
-
279
- func quotedModulesFromAddedLines(_, _ string, lines []string) []string {
280
- var modules []string
281
- for _, line := range lines {
282
- modules = append(modules, importedModulesFromLine(line)...)
283
- }
284
- return modules
285
- }
286
-
287
- func cssModulesFromAddedLines(_, _ string, lines []string) []string {
288
- return cssModulesFromSource([]byte(strings.Join(lines, "\n")))
289
- }
290
-
291
- func addedCSSModules(baseline, current []byte) []string {
292
- baselineCounts := map[string]int{}
293
- for _, module := range cssModulesFromSource(baseline) {
294
- baselineCounts[module]++
295
- }
296
- var added []string
297
- for _, module := range cssModulesFromSource(current) {
298
- if baselineCounts[module] > 0 {
299
- baselineCounts[module]--
300
- continue
301
- }
302
- added = append(added, module)
303
- }
304
- return added
305
- }
306
-
307
- func cssModulesFromSource(source []byte) []string {
308
- activeSource := stripCSSComments(string(source))
309
- var modules []string
310
- for _, match := range cssImport.FindAllStringSubmatch(activeSource, -1) {
311
- if len(match) < 3 {
312
- continue
313
- }
314
- module := match[1]
315
- if module == "" {
316
- module = match[2]
317
- }
318
- modules = append(modules, module)
319
- }
320
- return modules
321
- }
322
-
323
- func stripCSSComments(source string) string {
324
- var active strings.Builder
325
- active.Grow(len(source))
326
- inComment := false
327
- for i := 0; i < len(source); {
328
- switch {
329
- case !inComment && i+1 < len(source) && source[i] == '/' && source[i+1] == '*':
330
- inComment = true
331
- active.WriteString(" ")
332
- i += 2
333
- case inComment && i+1 < len(source) && source[i] == '*' && source[i+1] == '/':
334
- inComment = false
335
- active.WriteString(" ")
336
- i += 2
337
- case inComment:
338
- if source[i] == '\n' {
339
- active.WriteByte('\n')
340
- } else {
341
- active.WriteByte(' ')
342
- }
343
- i++
344
- default:
345
- active.WriteByte(source[i])
346
- i++
347
- }
348
- }
349
- return active.String()
350
- }
351
-
352
- func cssSourceRelativePathExists(gitRoot, sourcePath, specifier string) bool {
353
- localSpecifier := specifier
354
- if index := strings.IndexAny(localSpecifier, "?#"); index >= 0 {
355
- localSpecifier = localSpecifier[:index]
356
- }
357
- if localSpecifier == "" {
358
- return false
359
- }
360
- candidate := filepath.Join(
361
- gitRoot,
362
- filepath.Dir(filepath.FromSlash(sourcePath)),
363
- filepath.FromSlash(localSpecifier),
364
- )
365
- info, err := os.Stat(candidate)
366
- return err == nil && !info.IsDir()
367
- }
368
-
369
- func goModulesFromAddedLinesForSource(gitRoot, sourcePath string, lines []string) []string {
370
- return goModulesFromAddedLines(filepath.Join(gitRoot, sourcePath), lines)
371
- }
372
-
373
- func pythonModulesFromAddedLines(_, _ string, lines []string) []string {
374
- var modules []string
375
- for _, line := range lines {
376
- modules = append(modules, pythonModulesFromLine(line)...)
377
- }
378
- return modules
379
- }
380
-
381
- func rustModulesFromAddedLines(_, _ string, lines []string) []string {
382
- var modules []string
383
- for _, line := range lines {
384
- modules = append(modules, rustModulesFromLine(line)...)
385
- }
386
- return modules
387
- }
388
-
389
- func goModulesFromAddedLines(sourcePath string, lines []string) []string {
390
- file, _ := parser.ParseFile(token.NewFileSet(), sourcePath, nil, parser.ImportsOnly|parser.AllErrors)
391
- if file == nil {
392
- var modules []string
393
- for _, line := range lines {
394
- modules = append(modules, importedModulesFromLine(line)...)
395
- }
396
- return modules
397
- }
398
- var modules []string
399
- for _, spec := range file.Imports {
400
- module, err := strconv.Unquote(spec.Path.Value)
401
- if err != nil || !addedLinesContainImport(lines, spec.Path.Value, module) {
402
- continue
403
- }
404
- modules = append(modules, module)
405
- }
406
- return modules
407
- }
408
-
409
- func addedLinesContainImport(lines []string, quoted, module string) bool {
410
- for _, line := range lines {
411
- if strings.Contains(line, quoted) || strings.Contains(line, "`"+module+"`") {
412
- return true
413
- }
414
- }
415
- return false
416
- }
417
-
418
- func pythonModulesFromLine(line string) []string {
419
- line = strings.TrimSpace(stripConfigComment(line, '#'))
420
- if line == "" {
421
- return nil
422
- }
423
- var modules []string
424
- for statement := range strings.SplitSeq(line, ";") {
425
- statement = strings.TrimSpace(statement)
426
- if match := pythonFrom.FindStringSubmatch(statement); len(match) > 1 {
427
- if module := topLevelPythonModule(match[1]); module != "" {
428
- modules = append(modules, module)
429
- }
430
- continue
431
- }
432
- if !strings.HasPrefix(statement, "import ") {
433
- continue
434
- }
435
- for imported := range strings.SplitSeq(strings.TrimPrefix(statement, "import "), ",") {
436
- fields := strings.Fields(strings.TrimSpace(imported))
437
- if len(fields) == 0 {
438
- continue
439
- }
440
- if module := topLevelPythonModule(fields[0]); module != "" {
441
- modules = append(modules, module)
442
- }
443
- }
444
- }
445
- return modules
446
- }
447
-
448
- func topLevelPythonModule(module string) string {
449
- module = strings.TrimSpace(module)
450
- if module == "" || strings.HasPrefix(module, ".") {
451
- return ""
452
- }
453
- return strings.SplitN(module, ".", 2)[0]
454
- }
455
-
456
- func rustModulesFromLine(line string) []string {
457
- line = stripConfigComment(line, '/')
458
- if match := rustUse.FindStringSubmatch(line); len(match) > 1 {
459
- return []string{match[1]}
460
- }
461
- if match := rustExtern.FindStringSubmatch(line); len(match) > 1 {
462
- return []string{match[1]}
463
- }
464
- return nil
465
- }
466
-
467
- func isGoStandardLibrary(module string) bool {
468
- return strings.Contains(" C archive/tar archive/zip bufio bytes cmp compress/bzip2 compress/flate compress/gzip compress/lzw compress/zlib container/heap container/list container/ring context crypto crypto/aes crypto/cipher crypto/des crypto/dsa crypto/ecdh crypto/ecdsa crypto/ed25519 crypto/elliptic crypto/fips140 crypto/hkdf crypto/hmac crypto/hpke crypto/md5 crypto/mlkem crypto/mlkem/mlkemtest crypto/pbkdf2 crypto/rand crypto/rc4 crypto/rsa crypto/sha1 crypto/sha256 crypto/sha3 crypto/sha512 crypto/subtle crypto/tls crypto/x509 crypto/x509/pkix database/sql database/sql/driver debug/buildinfo debug/dwarf debug/elf debug/gosym debug/macho debug/pe debug/plan9obj embed encoding encoding/ascii85 encoding/asn1 encoding/base32 encoding/base64 encoding/binary encoding/csv encoding/gob encoding/hex encoding/json encoding/pem encoding/xml errors expvar flag fmt go/ast go/build go/build/constraint go/constant go/doc go/doc/comment go/format go/importer go/parser go/printer go/scanner go/token go/types go/version hash hash/adler32 hash/crc32 hash/crc64 hash/fnv hash/maphash html html/template image image/color image/color/palette image/draw image/gif image/jpeg image/png index/suffixarray io io/fs io/ioutil iter log log/slog log/syslog maps math math/big math/bits math/cmplx math/rand math/rand/v2 mime mime/multipart mime/quotedprintable net net/http net/http/cgi net/http/cookiejar net/http/fcgi net/http/httptest net/http/httptrace net/http/httputil net/http/pprof net/mail net/netip net/rpc net/rpc/jsonrpc net/smtp net/textproto net/url os os/exec os/signal os/user path path/filepath plugin reflect regexp regexp/syntax runtime runtime/cgo runtime/coverage runtime/debug runtime/metrics runtime/pprof runtime/race runtime/trace slices sort strconv strings structs sync sync/atomic syscall testing testing/cryptotest testing/fstest testing/iotest testing/quick testing/slogtest testing/synctest text/scanner text/tabwriter text/template text/template/parse time time/tzdata unicode unicode/utf16 unicode/utf8 unique unsafe weak ", " "+module+" ")
469
- }
470
-
471
- func goImportResolved(gitRoot, sourcePath, module string) (bool, bool) {
472
- manifest := nearestFile(gitRoot, sourcePath, "go.mod")
473
- if manifest == "" {
474
- return false, false
475
- }
476
- data, err := os.ReadFile(manifest)
477
- if err != nil {
478
- return false, true
479
- }
480
- localModule, required, ok := parseGoMod(string(data))
481
- if !ok {
482
- return false, true
483
- }
484
- if modulePathContains(localModule, module) {
485
- return true, true
486
- }
487
- for _, dependency := range required {
488
- if modulePathContains(dependency, module) {
489
- return true, true
490
- }
491
- }
492
- return false, true
493
- }
494
-
495
- func parseGoMod(data string) (string, []string, bool) {
496
- var module string
497
- var required []string
498
- inRequireBlock := false
499
- for rawLine := range strings.SplitSeq(data, "\n") {
500
- line := strings.TrimSpace(strings.SplitN(rawLine, "//", 2)[0])
501
- if line == "" {
502
- continue
503
- }
504
- if inRequireBlock {
505
- if line == ")" {
506
- inRequireBlock = false
507
- continue
508
- }
509
- if fields := strings.Fields(line); len(fields) >= 2 {
510
- required = append(required, strings.Trim(fields[0], `"`))
511
- }
512
- continue
513
- }
514
- fields := strings.Fields(line)
515
- if len(fields) >= 2 && fields[0] == "module" {
516
- module = strings.Trim(fields[1], `"`)
517
- }
518
- if len(fields) >= 2 && fields[0] == "require" {
519
- if fields[1] == "(" {
520
- inRequireBlock = true
521
- } else if len(fields) >= 3 {
522
- required = append(required, strings.Trim(fields[1], `"`))
523
- }
524
- }
525
- }
526
- return module, required, module != "" && !inRequireBlock
527
- }
528
-
529
- func modulePathContains(module, imported string) bool {
530
- return module != "" && (imported == module || strings.HasPrefix(imported, module+"/"))
531
- }
532
-
533
- func normalizeDistributionName(name string) string {
534
- name = strings.ToLower(strings.TrimSpace(name))
535
- var normalized strings.Builder
536
- separator := false
537
- for _, char := range name {
538
- if char == '-' || char == '_' || char == '.' {
539
- separator = true
540
- continue
541
- }
542
- if separator && normalized.Len() > 0 {
543
- normalized.WriteByte('-')
544
- }
545
- separator = false
546
- normalized.WriteRune(char)
547
- }
548
- return normalized.String()
549
- }
550
-
551
- func isPythonStandardLibrary(module string) bool {
552
- return strings.Contains(" __future__ abc aifc argparse array ast asynchat asyncio asyncore atexit audioop base64 bdb binascii bisect builtins bz2 calendar cgi cgitb chunk cmath cmd code codecs codeop collections colorsys compileall concurrent configparser contextlib contextvars copy copyreg crypt csv ctypes curses dataclasses datetime dbm decimal difflib dis doctest email encodings ensurepip enum errno faulthandler fcntl filecmp fileinput fnmatch fractions ftplib functools gc getopt getpass gettext glob graphlib grp gzip hashlib heapq hmac html http idlelib imaplib imghdr importlib inspect io ipaddress itertools json keyword linecache locale logging lzma mailbox mailcap marshal math mimetypes mmap modulefinder multiprocessing netrc nis nntplib numbers operator optparse os pathlib pdb pickle pickletools pipes pkgutil platform plistlib poplib posix pprint profile pstats pty pwd py_compile pyclbr pydoc queue quopri random re readline reprlib resource rlcompleter runpy sched secrets select selectors shelve shlex shutil signal site smtpd smtplib sndhdr socket socketserver sqlite3 ssl stat statistics string stringprep struct subprocess sunau symtable sys sysconfig tabnanny tarfile telnetlib tempfile termios textwrap threading time timeit tkinter token tokenize tomllib trace traceback tracemalloc tty turtle turtledemo types typing unicodedata unittest urllib uu uuid venv warnings wave weakref webbrowser winreg winsound wsgiref xdrlib xml xmlrpc zipapp zipfile zipimport zlib zoneinfo ", " "+module+" ")
553
- }
554
-
555
- func pythonImportResolved(gitRoot, sourcePath, module string) (bool, bool) {
556
- manifests := nearestFiles(gitRoot, sourcePath, []string{"requirements.txt", "pyproject.toml", "Pipfile"})
557
- if len(manifests) == 0 {
558
- return false, false
559
- }
560
- declared := map[string]struct{}{}
561
- for _, manifest := range manifests {
562
- data, err := os.ReadFile(manifest)
563
- if err != nil {
564
- continue
565
- }
566
- var packages map[string]struct{}
567
- switch filepath.Base(manifest) {
568
- case "requirements.txt":
569
- packages = requirementPackages(string(data))
570
- case "pyproject.toml":
571
- packages, _ = pyprojectPackages(string(data))
572
- case "Pipfile":
573
- packages = pipfilePackages(string(data))
574
- }
575
- for pkg := range packages {
576
- declared[pkg] = struct{}{}
577
- }
578
- }
579
- _, ok := declared[module]
580
- return ok, true
581
- }
582
-
583
- func requirementPackages(data string) map[string]struct{} {
584
- packages := map[string]struct{}{}
585
- for rawLine := range strings.SplitSeq(data, "\n") {
586
- addRequirementPackage(packages, stripConfigComment(rawLine, '#'))
587
- }
588
- return packages
589
- }
590
-
591
- func addRequirementPackage(packages map[string]struct{}, value string) {
592
- value = strings.TrimSpace(value)
593
- if value == "" || strings.HasPrefix(value, "-") {
594
- return
595
- }
596
- if match := requirement.FindStringSubmatch(value); len(match) > 1 {
597
- packages[normalizeDistributionName(match[1])] = struct{}{}
598
- }
599
- }
600
-
601
- func pyprojectPackages(data string) (map[string]struct{}, bool) {
602
- packages := map[string]struct{}{}
603
- section := ""
604
- arrayOpen := false
605
- for rawLine := range strings.SplitSeq(data, "\n") {
606
- line := strings.TrimSpace(stripConfigComment(rawLine, '#'))
607
- if line == "" {
608
- continue
609
- }
610
- if arrayOpen {
611
- addQuotedRequirements(packages, line)
612
- if containsUnquotedByte(line, ']') {
613
- arrayOpen = false
614
- }
615
- continue
616
- }
617
- if strings.HasPrefix(line, "[") {
618
- if !strings.HasSuffix(line, "]") {
619
- return nil, false
620
- }
621
- section = strings.TrimSpace(strings.Trim(line, "[]"))
622
- continue
623
- }
624
- key, value, ok := tomlAssignment(line)
625
- if !ok {
626
- continue
627
- }
628
- if section == "tool.poetry.dependencies" || strings.HasPrefix(section, "tool.poetry.group.") && strings.HasSuffix(section, ".dependencies") {
629
- if normalizeDistributionName(key) != "python" {
630
- packages[normalizeDistributionName(key)] = struct{}{}
631
- }
632
- continue
633
- }
634
- arrayDependency := section == "project" && key == "dependencies" || section == "project.optional-dependencies" || section == "dependency-groups"
635
- if !arrayDependency {
636
- continue
637
- }
638
- addQuotedRequirements(packages, value)
639
- arrayOpen = containsUnquotedByte(value, '[') && !containsUnquotedByte(value, ']')
640
- }
641
- if arrayOpen {
642
- return nil, false
643
- }
644
- return packages, true
645
- }
646
-
647
- func addQuotedRequirements(packages map[string]struct{}, value string) {
648
- for _, match := range quotedValue.FindAllStringSubmatch(value, -1) {
649
- if len(match) > 1 {
650
- addRequirementPackage(packages, match[1])
651
- }
652
- }
653
- }
654
-
655
- func pipfilePackages(data string) map[string]struct{} {
656
- packages := map[string]struct{}{}
657
- section := ""
658
- for rawLine := range strings.SplitSeq(data, "\n") {
659
- line := strings.TrimSpace(stripConfigComment(rawLine, '#'))
660
- if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
661
- section = strings.TrimSpace(strings.Trim(line, "[]"))
662
- continue
663
- }
664
- if section != "packages" && section != "dev-packages" {
665
- continue
666
- }
667
- if key, _, ok := tomlAssignment(line); ok {
668
- packages[normalizeDistributionName(key)] = struct{}{}
669
- }
670
- }
671
- return packages
672
- }
673
-
674
- func tomlAssignment(line string) (string, string, bool) {
675
- parts := strings.SplitN(line, "=", 2)
676
- if len(parts) != 2 {
677
- return "", "", false
678
- }
679
- key := strings.Trim(strings.TrimSpace(parts[0]), `"'`)
680
- return key, strings.TrimSpace(parts[1]), key != ""
681
- }
682
-
683
- func stripConfigComment(line string, marker byte) string {
684
- quote := byte(0)
685
- escaped := false
686
- for index := 0; index < len(line); index++ {
687
- char := line[index]
688
- if escaped {
689
- escaped = false
690
- continue
691
- }
692
- if char == '\\' && quote == '"' {
693
- escaped = true
694
- continue
695
- }
696
- if char == '\'' || char == '"' {
697
- if quote == 0 {
698
- quote = char
699
- } else if quote == char {
700
- quote = 0
701
- }
702
- continue
703
- }
704
- if quote == 0 && char == marker {
705
- if marker != '/' || index+1 < len(line) && line[index+1] == '/' {
706
- return line[:index]
707
- }
708
- }
709
- }
710
- return line
711
- }
712
-
713
- func containsUnquotedByte(value string, target byte) bool {
714
- quote := byte(0)
715
- escaped := false
716
- for index := 0; index < len(value); index++ {
717
- char := value[index]
718
- if escaped {
719
- escaped = false
720
- continue
721
- }
722
- if char == '\\' && quote == '"' {
723
- escaped = true
724
- continue
725
- }
726
- if char == '\'' || char == '"' {
727
- if quote == 0 {
728
- quote = char
729
- } else if quote == char {
730
- quote = 0
731
- }
732
- continue
733
- }
734
- if quote == 0 && char == target {
735
- return true
736
- }
737
- }
738
- return false
739
- }
740
-
741
- func normalizeRustCrate(crate string) string {
742
- crate = strings.TrimPrefix(strings.TrimSpace(crate), "r#")
743
- crate = strings.SplitN(crate, "::", 2)[0]
744
- return strings.ReplaceAll(crate, "-", "_")
745
- }
746
-
747
- func isRustBuiltin(crate string) bool {
748
- switch crate {
749
- case "std", "core", "alloc", "crate", "self", "super":
750
- return true
751
- default:
752
- return false
753
- }
754
- }
755
-
756
- func rustImportResolved(gitRoot, sourcePath, crate string) (bool, bool) {
757
- manifest := nearestFile(gitRoot, sourcePath, "Cargo.toml")
758
- if manifest == "" {
759
- return false, false
760
- }
761
- data, err := os.ReadFile(manifest)
762
- if err != nil {
763
- return false, true
764
- }
765
- localCrate, dependencies := cargoPackages(string(data))
766
- if crate == localCrate {
767
- return true, true
768
- }
769
- _, ok := dependencies[crate]
770
- return ok, true
771
- }
772
-
773
- func cargoPackages(data string) (string, map[string]struct{}) {
774
- dependencies := map[string]struct{}{}
775
- section := ""
776
- localCrate := ""
777
- for rawLine := range strings.SplitSeq(data, "\n") {
778
- line := strings.TrimSpace(stripConfigComment(rawLine, '#'))
779
- if line == "" {
780
- continue
781
- }
782
- if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
783
- section = strings.TrimSpace(strings.Trim(line, "[]"))
784
- if dependency := cargoDependencyTable(section); dependency != "" {
785
- dependencies[dependency] = struct{}{}
786
- }
787
- continue
788
- }
789
- key, value, ok := tomlAssignment(line)
790
- if !ok {
791
- continue
792
- }
793
- if section == "package" && key == "name" {
794
- localCrate = normalizeRustCrate(strings.Trim(value, `"'`))
795
- continue
796
- }
797
- if isCargoDependencySection(section) && value != "" {
798
- dependencies[normalizeRustCrate(key)] = struct{}{}
799
- }
800
- }
801
- return localCrate, dependencies
802
- }
803
-
804
- func isCargoDependencySection(section string) bool {
805
- return section == "dependencies" || section == "dev-dependencies" || section == "build-dependencies" ||
806
- strings.HasPrefix(section, "target.") && (strings.HasSuffix(section, ".dependencies") || strings.HasSuffix(section, ".dev-dependencies") || strings.HasSuffix(section, ".build-dependencies"))
807
- }
808
-
809
- func cargoDependencyTable(section string) string {
810
- for _, prefix := range []string{"dependencies.", "dev-dependencies.", "build-dependencies."} {
811
- if dependency, ok := strings.CutPrefix(section, prefix); ok {
812
- return normalizeRustCrate(strings.Trim(dependency, `"'`))
813
- }
814
- }
815
- if strings.HasPrefix(section, "target.") {
816
- for _, marker := range []string{".dependencies.", ".dev-dependencies.", ".build-dependencies."} {
817
- if _, dependency, ok := strings.Cut(section, marker); ok {
818
- return normalizeRustCrate(strings.Trim(dependency, `"'`))
819
- }
820
- }
821
- }
822
- return ""
823
- }
824
-
825
- func nearestFiles(gitRoot, sourcePath string, names []string) []string {
826
- dir := filepath.Dir(filepath.Join(gitRoot, sourcePath))
827
- for {
828
- var manifests []string
829
- for _, name := range names {
830
- candidate := filepath.Join(dir, name)
831
- if isFile(candidate) {
832
- manifests = append(manifests, candidate)
833
- }
834
- }
835
- if len(manifests) > 0 || dir == gitRoot {
836
- return manifests
837
- }
838
- dir = filepath.Dir(dir)
839
- }
840
- }
841
-
842
- func importedModulesFromLine(line string) []string {
843
- var mods []string
844
- for _, match := range quotedModule.FindAllStringSubmatch(line, -1) {
845
- if len(match) > 1 {
846
- mods = append(mods, match[1])
847
- }
848
- }
849
- if len(mods) > 0 {
850
- return mods
851
- }
852
- if match := pythonFrom.FindStringSubmatch(line); len(match) > 1 {
853
- return []string{match[1]}
854
- }
855
- if match := pythonImport.FindStringSubmatch(line); len(match) > 1 {
856
- return []string{match[1]}
857
- }
858
- return nil
859
- }
860
-
861
- func normalizeImportedPackage(name string) string {
862
- name = scopedPackage.ReplaceAllString(name, "$1")
863
- name = firstSegment.ReplaceAllString(name, "$1")
864
- if localPath.MatchString(name) {
865
- return ""
866
- }
867
- return name
868
- }
869
-
870
- func nearestFile(gitRoot, sourcePath, name string) string {
871
- dir := filepath.Dir(filepath.Join(gitRoot, sourcePath))
872
- for {
873
- candidate := filepath.Join(dir, name)
874
- if isFile(candidate) {
875
- return candidate
876
- }
877
- if dir == gitRoot {
878
- return ""
879
- }
880
- dir = filepath.Dir(dir)
881
- }
882
- }
883
-
884
- func packageJSONDeclares(pkg, manifest string) bool {
885
- data, err := os.ReadFile(manifest)
886
- if err != nil {
887
- return false
888
- }
889
- var packageJSON struct {
890
- Dependencies map[string]string `json:"dependencies"`
891
- DevDependencies map[string]string `json:"devDependencies"`
892
- PeerDependencies map[string]string `json:"peerDependencies"`
893
- OptionalDependencies map[string]string `json:"optionalDependencies"`
894
- }
895
- if err := json.Unmarshal(data, &packageJSON); err != nil {
896
- return false
897
- }
898
- for _, dependencies := range []map[string]string{
899
- packageJSON.Dependencies,
900
- packageJSON.DevDependencies,
901
- packageJSON.PeerDependencies,
902
- packageJSON.OptionalDependencies,
903
- } {
904
- if _, ok := dependencies[pkg]; ok {
905
- return true
906
- }
907
- }
908
- return false
909
- }
910
-
911
- func importMatchesPathAlias(gitRoot, sourcePath, specifier string) bool {
912
- dir := filepath.Dir(filepath.Join(gitRoot, sourcePath))
913
- for {
914
- foundConfig := false
915
- config := filepath.Join(dir, "tsconfig.json")
916
- if !isFile(config) {
917
- config = filepath.Join(dir, "jsconfig.json")
918
- }
919
- for _, config := range []string{config, filepath.Join(dir, ".svelte-kit", "tsconfig.json")} {
920
- if isFile(config) {
921
- foundConfig = true
922
- if configMatchesPathAlias(config, specifier) {
923
- return true
924
- }
925
- }
926
- }
927
- if foundConfig {
928
- return false
929
- }
930
- if dir == gitRoot {
931
- return false
932
- }
933
- dir = filepath.Dir(dir)
934
- }
935
- }
936
-
937
- func configMatchesPathAlias(configPath, specifier string) bool {
938
- data, err := os.ReadFile(configPath)
939
- if err != nil {
940
- return false
941
- }
942
- var config struct {
943
- CompilerOptions struct {
944
- Paths map[string][]string `json:"paths"`
945
- } `json:"compilerOptions"`
946
- }
947
- if err := json.Unmarshal(data, &config); err != nil {
948
- return false
949
- }
950
- for alias, targets := range config.CompilerOptions.Paths {
951
- if len(targets) > 0 && matchesAlias(specifier, alias) {
952
- return true
953
- }
954
- }
955
- return false
956
- }
957
-
958
- func matchesAlias(specifier, alias string) bool {
959
- if strings.Count(alias, "*") == 0 {
960
- return specifier == alias
961
- }
962
- if strings.Count(alias, "*") != 1 {
963
- return false
964
- }
965
- parts := strings.SplitN(alias, "*", 2)
966
- return len(specifier) >= len(parts[0])+len(parts[1]) &&
967
- strings.HasPrefix(specifier, parts[0]) && strings.HasSuffix(specifier, parts[1])
968
- }