scanner

package module
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: MIT Imports: 17 Imported by: 0

README

scanner

A deliberately lightweight manifest reader: thin per-format parsers turning dependency manifests into one ecosystem-neutral shape: the package's declared identity (name, version) and its declared dependencies with their ranges, manifest fields and local-path signals. No SBOM machinery, no lockfile resolution, no network. The recognised formats are fixed at build time, thirty-five of them across twenty ecosystems, and fence tests hold the reader and the writer to the same list; the shared vocabulary (dependency kinds, the file-name rules) lives in pkg/manifest so this reader and pkg/writer can never drift apart. It only reads; rewriting is the writer's job. This is the library behind dispat compute (deriving a monorepo's dependency graph from its manifests) and the executor's native auto-versioning.

sc := scanner.New()
mans, err := sc.Scan(ctx, "packages/web") // every manifest under the folder
roots, err := sc.ScanRoot(ctx, "packages/web") // only the folder's own manifests

mans, err = scanner.Scan(ctx, "packages/web") // the package-level conveniences
roots, err = scanner.ScanRoot(ctx, "packages/web")

Both methods share one error contract: a manifest that fails to parse is skipped, its error joined into the returned error, and the parsed manifests come back either way, so callers can report the problem and keep the partial result. Reads are capped at 16 MiB per file (ErrManifestTooLarge); output order is deterministic.

Supported manifests

File Ecosystem Reads
package.json npm name, version, the four dependency fields, file:/link: local paths
go.mod gomod module path, direct requires, indirect ones apart in Indirect, relative replace targets as local paths
Cargo.toml cargo name, version, [dependencies]/[dev-dependencies]/[build-dependencies], renames, path keys
pyproject.toml python PEP 621, PEP 735 groups, Poetry, PEP 503 name normalisation
requirements files python PEP 508 lines, continuations, editable local installs (-e ./pkg)
composer.json composer name, version, require/require-dev (platform requirements filtered)
pom.xml maven groupId:artifactId coordinates, scopes onto dependency kinds
*.csproj nuget PackageId/AssemblyName (the file's base name when both are absent), <Version>, PackageReference, ProjectReference as local paths
*.fsproj/*.vbproj nuget the same SDK-style schema, since F# and VB projects share it
*.nuspec nuget id, version, dependencies flat or inside targetFramework groups
Directory.Packages.props nuget Central Package Management: every PackageVersion the repository pins
packages.config nuget the legacy list, with developmentDependency onto devDependencies
pubspec.yaml/.yml pub name, version (its + suffix as the build number), dependencies, dependency_overrides folded onto their declarations
Gemfile rubygems gem declarations, :path local gems, development and test groups onto devDependencies (a group is tracked by line, so a block that never closes scopes every later declaration)
*.gemspec rubygems name, version, add_dependency/add_runtime_dependency/add_development_dependency
Dockerfile, Containerfile docker every FROM, COPY --from and RUN --mount=…,from= image; stage aliases and scratch excluded
compose.yaml docker the image the file builds as its identity, every other service's image as a dependency

A Dockerfile is matched by name rather than by extension, so Dockerfile, Dockerfile.dev, api.Dockerfile and Podman's Containerfile all count. A compose file is matched by name too: compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml and the .override. variant of each, which is the set the Compose specification itself loads. A requirements file is matched by whole words rather than a glob: a .txt whose base name starts or ends with the word requirements counts, so dev-requirements.txt and requirements-test.txt land in devDependencies while requirements-latest.txt stays a plain requirements file and old-requirements-notes.txt is prose, not a manifest.

Docker has no version field, so the reader takes the identity from the images the file names. The rule, in order: the service that declares both a build section and a tagged image is producing that image here, which is as close to "this is my package" as compose gets; failing that, the tagged repository the most services name. Ties go to the lowest service name, because a YAML mapping decodes in no order worth trusting and the answer has to come from the data. A compose file that only wires third-party services together declares no identity at all, which is the honest answer rather than a guess. A Dockerfile never declares one: what it builds is named on the command line, not in the file.

The name a Docker manifest declares is an image repository (ghcr.io/acme/api, not api), so a package usually either states manifestNames or leans on the substring name matching, whose last-segment rule maps the two onto each other.

The mobile platforms are covered too. Four of these declare an identity and a version but no dependencies at all, so they feed auto-versioning rather than the dependency graph. Every Java-world coordinate is spelled group:artifact, which means a version-catalog entry, a build script's literal notation and a pom.xml dependency all name the same package.

File Ecosystem Reads
Info.plist plist CFBundleIdentifier, CFBundleShortVersionString, CFBundleVersion as the build number
project.pbxproj xcode PRODUCT_BUNDLE_IDENTIFIER, MARKETING_VERSION, CURRENT_PROJECT_VERSION, first config wins
Podfile cocoapods pod declarations, :path local pods, a …Tests target's pods onto devDependencies
*.podspec cocoapods name, version, dependency declarations, including subspec and platform-scoped ones
AndroidManifest.xml android package, android:versionName, android:versionCode as the build number
libs.versions.toml gradle [libraries] by Maven coordinate, version.ref resolved through [versions]
build.gradle(.kts) gradle applicationId/namespace, versionName, versionCode, literal coordinates, project(…)

Manifest.BuildNumber carries the monotonic counter these formats keep beside their marketing version (CFBundleVersion, android:versionCode, CURRENT_PROJECT_VERSION, Gradle's versionCode, a pubspec version's + suffix). It is not a semantic version, so no version write ever moves it; the writer's SetBuild is the one entry point that does.

Manifest.Dropped names the entries a manifest declared but the parser could not coerce into a dependency, one line each (service db: not a mapping). They are not errors: the manifest parsed, and the caller decides whether the drops are worth reporting. The shapes a format reads selectively by design are not dropped entries; those are listed under Not read today.

Manifest.Indirect carries the requirements a manifest records as transitive bookkeeping rather than as its own declarations. Only go.mod has the distinction and only its parser fills the field; a requirement in Deps never appears there as well, and an indirect require that a relative replace pins locally counts as a declaration and stays in Deps. Keeping the two apart is what lets a caller reconcile ranges without touching a version the toolchain owns, while still being able to redirect a module reached only transitively, which a Go build needs, since it honours replace in the main module alone.

Helpers shared by the CLI's two consumers: NameIndex (manifest name → owning package, stated names first, then root manifests, then nested ones, with a same-rank collision reported instead of guessed), ResolveLocalDir (declared local path → owning package folder) and SkipDir (the folder names a workspace walk never enters, exported so a caller walking a package for other reasons stays out of the same places).

Owner.Names is how a package with no readable identity joins the index: a Gradle module or a Makefile project declares nothing a parser here can read, so the caller states the names it answers to and they outrank anything a file declares.

From the command line

dispat scanner [folder] is this package with a listing attached, and it needs no dispat config file and no git repository:

dispat scanner packages/web              # every manifest under the folder
dispat scanner packages/web --root-only  # only the folder's own
dispat scanner --log-format json         # one JSON object per manifest
dispat scanner --strict                  # exit 1 if any manifest failed to parse

The full guide is Manifest tools.

Not read today

These gaps are written down so nobody meets them for the first time in production.

Not read: npm workspaces, overrides and resolutions; Cargo [workspace.dependencies], [workspace.members] and target-specific tables; Maven ${property} interpolation, parent-POM resolution, <dependencyManagement> and <modules>; Poetry multi-constraint dependency lists; PEP 735 include-group; Directory.Build.props and NuGet lock files; Bundler's alternative gems.rb spelling and its Gemfile.lock.

A .nuspec packed from a project is a template. NuGet fills in its $id$ and $version$ tokens at pack time, so a token version is kept as written and a token identifier reads as empty. An Xcode $(PRODUCT_BUNDLE_IDENTIFIER) is treated the same way.

Version text is always kept as written, so name matching still carries the graph when the version is indirected. The Acme::VERSION constant nearly every gemspec assigns its version from reads as no version at all, because the number lives in a Ruby source file rather than the manifest.

The mobile formats are read by recognising the statement shapes that declare something. Anything a single file cannot resolve is dropped instead of guessed at, and on a modern Android project that is a great deal.

Not read: version-catalog accessors (implementation libs.retrofit), interpolated versions ("…:$coreVersion", and #{...} in a Podfile), ext properties, and a versionName computed from a properties file.

A Gradle project(':core') reference is recorded by its last path segment with no local path. A project path is relative to the build's root, which one build file does not reveal, so guessing at the folder could land on a real but unrelated package. settings.gradle projectDir remapping is invisible for the same reason.

[plugins] and [bundles] catalog tables are not dependencies. Subspec dependencies are collected but not attributed to their subspec, and .podspec.json is a different grammar in JSON. Only the legacy pre-namespacing attributes are read from an AndroidManifest.xml, so a modern project correctly reads empty there and declares its versions in build.gradle instead.

Apple build-setting references are kept as written where a version is expected, matching the Maven ${property} rule. A $(PRODUCT_BUNDLE_IDENTIFIER)-shaped identifier reads as empty instead: every project spells it identically, and NameIndex would report the shared literal as an ambiguous name. Info.plist is matched by exact name, so the legacy MyApp-Info.plist spelling is not recognised.

Whole ecosystems not read

Some package managers have no reader here at all. Each entry states what reading it would take, so the cost of closing a gap is known before anyone starts.

  • Swift Package Manager: Package.swift is executable Swift rather than a manifest, the same objection that keeps the statement-shape readers above conservative.
  • Helm: Chart.yaml is plain YAML with a version, an appVersion and a dependencies list, the cheapest gap in this list to close.
  • Elixir: mix.exs is executable Elixir, readable only the way the Ruby files are read, by recognising the statement shapes that declare something.
  • Deno: deno.json is plain JSON whose imports map carries versioned specifiers.
  • Conan: conanfile.txt is a flat list; conanfile.py is executable Python and shares Swift PM's objection.

Requirements

Go 1.25 or later.

Licence

MIT. See LICENSE.

Documentation

Overview

Package scanner reads dependency manifests (package.json, go.mod, Cargo.toml, Dockerfiles and compose files among twenty-odd others) into one ecosystem-neutral shape: the package's declared identity (name, version) and its declared dependencies with their ranges and manifest fields. It only reads; rewriting manifests is the writer package's job.

The scanner is deliberately lightweight: a handful of file-name probes and thin per-format parsers, no SBOM machinery. The recognised manifest names are fixed at build time; supporting a new ecosystem means adding a parser to this package.

Example
package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/yohimik/dispat/pkg/scanner"
)

func main() {
	dir, _ := os.MkdirTemp("", "scanner-example-")
	defer os.RemoveAll(dir)
	manifest := []byte(`{
  "name": "@acme/web",
  "version": "1.2.0",
  "dependencies": {"@acme/core": "workspace:*"},
  "devDependencies": {"typescript": "^5.4.0"}
}`)
	_ = os.WriteFile(filepath.Join(dir, "package.json"), manifest, 0o644)

	mans, err := scanner.New().Scan(context.Background(), dir)
	if err != nil {
		fmt.Println("partial scan:", err)
	}
	for _, m := range mans {
		fmt.Printf("%s %s@%s\n", m.Ecosystem, m.Name, m.Version)
		for _, d := range m.Deps {
			fmt.Printf("  %s %s %q\n", d.Kind, d.Name, d.Range)
		}
	}
}
Output:
npm @acme/web@1.2.0
  dependencies @acme/core "workspace:*"
  devDependencies typescript "^5.4.0"
Example (Android)

Example_android reads an Android module's build script and the version catalog it resolves its dependency versions through. Both name libraries by Maven coordinate, so a catalog entry and a pom.xml dependency describe the same package.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/yohimik/dispat/pkg/scanner"
)

func main() {
	dir, _ := os.MkdirTemp("", "scanner-android-")
	defer os.RemoveAll(dir)
	_ = os.WriteFile(filepath.Join(dir, "build.gradle"), []byte(`android {
    defaultConfig {
        applicationId "com.acme.app"
        versionCode 42
        versionName "1.2.3"
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation project(':core')
    testImplementation 'junit:junit:4.13.2'
}`), 0o644)
	_ = os.MkdirAll(filepath.Join(dir, "gradle"), 0o755)
	_ = os.WriteFile(filepath.Join(dir, "gradle", "libs.versions.toml"), []byte(`[versions]
retrofit = "2.9.0"

[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }`), 0o644)

	mans, err := scanner.New().Scan(context.Background(), dir)
	if err != nil {
		fmt.Println("partial scan:", err)
	}
	for _, m := range mans {
		fmt.Printf("%s (%s)\n", m.Path, m.Ecosystem)
		if m.Name != "" {
			fmt.Printf("  %s version=%s build=%s\n", m.Name, m.Version, m.BuildNumber)
		}
		for _, d := range m.Deps {
			fmt.Printf("  %s %s %q\n", d.Kind, d.Name, d.Range)
		}
	}
}
Output:
build.gradle (gradle)
  com.acme.app version=1.2.3 build=42
  dependencies androidx.core:core-ktx "1.12.0"
  dependencies core ""
  devDependencies junit:junit "4.13.2"
gradle/libs.versions.toml (gradle)
  dependencies com.squareup.retrofit2:retrofit "2.9.0"
Example (IOS)

Example_iOS reads an iOS application's manifests: the bundle metadata that carries its identity and version, and the Podfile that carries its dependencies.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/yohimik/dispat/pkg/scanner"
)

func main() {
	dir, _ := os.MkdirTemp("", "scanner-ios-")
	defer os.RemoveAll(dir)
	_ = os.WriteFile(filepath.Join(dir, "Info.plist"), []byte(`<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
  <key>CFBundleIdentifier</key>
  <string>com.acme.app</string>
  <key>CFBundleShortVersionString</key>
  <string>1.2.3</string>
  <key>CFBundleVersion</key>
  <string>42</string>
</dict>
</plist>`), 0o644)
	_ = os.WriteFile(filepath.Join(dir, "Podfile"), []byte(`target 'Acme' do
  pod 'Alamofire', '~> 5.6'
  pod 'Core', :path => '../Core'

  target 'AcmeTests' do
    pod 'Quick', '~> 7.0'
  end
end`), 0o644)

	mans, err := scanner.New().Scan(context.Background(), dir)
	if err != nil {
		fmt.Println("partial scan:", err)
	}
	for _, m := range mans {
		fmt.Printf("%s (%s)\n", m.Path, m.Ecosystem)
		if m.Name != "" {
			fmt.Printf("  %s version=%s build=%s\n", m.Name, m.Version, m.BuildNumber)
		}
		for _, d := range m.Deps {
			fmt.Printf("  %s %s %q", d.Kind, d.Name, d.Range)
			if d.LocalPath != "" {
				fmt.Printf(" -> %s", d.LocalPath)
			}
			fmt.Println()
		}
	}
}
Output:
Info.plist (plist)
  com.acme.app version=1.2.3 build=42
Podfile (cocoapods)
  dependencies Alamofire "~> 5.6"
  dependencies Core "" -> ../Core
  devDependencies Quick "~> 7.0"
Example (Ruby)

Example_ruby reads a Ruby project's manifests: the gemspec that declares the library's own identity and the Gemfile that declares what an application installs. A gemspec assigning its version from a constant reports no version: the number lives in a Ruby source file this package does not evaluate.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/yohimik/dispat/pkg/scanner"
)

func main() {
	dir, _ := os.MkdirTemp("", "scanner-ruby-")
	defer os.RemoveAll(dir)
	_ = os.WriteFile(filepath.Join(dir, "Gemfile"), []byte(`source 'https://rubygems.org'

gem 'rails', '~> 7.0.4'
gem 'local', path: '../local'

group :development, :test do
  gem 'rspec-rails', '~> 6.0'
end`), 0o644)
	_ = os.WriteFile(filepath.Join(dir, "acme.gemspec"), []byte(`Gem::Specification.new do |spec|
  spec.name    = "acme"
  spec.version = "1.2.3"

  spec.add_dependency "rails", "~> 7.0"
  spec.add_development_dependency "rspec", "~> 3.0"
end`), 0o644)

	mans, err := scanner.New().Scan(context.Background(), dir)
	if err != nil {
		fmt.Println("partial scan:", err)
	}
	for _, m := range mans {
		fmt.Printf("%s (%s)\n", m.Path, m.Ecosystem)
		if m.Name != "" {
			fmt.Printf("  %s@%s\n", m.Name, m.Version)
		}
		for _, d := range m.Deps {
			fmt.Printf("  %s %s %q", d.Kind, d.Name, d.Range)
			if d.LocalPath != "" {
				fmt.Printf(" -> %s", d.LocalPath)
			}
			fmt.Println()
		}
	}
}
Output:
Gemfile (rubygems)
  dependencies local "" -> ../local
  dependencies rails "~> 7.0.4"
  devDependencies rspec-rails "~> 6.0"
acme.gemspec (rubygems)
  acme@1.2.3
  dependencies rails "~> 7.0"
  devDependencies rspec "~> 3.0"

Index

Examples

Constants

View Source
const (
	KindDependencies         = manifest.KindDependencies
	KindDevDependencies      = manifest.KindDevDependencies
	KindPeerDependencies     = manifest.KindPeerDependencies
	KindOptionalDependencies = manifest.KindOptionalDependencies
)

Dependency kinds, re-exported from pkg/manifest.

Variables

View Source
var ErrManifestTooLarge = errors.New("scanner: manifest exceeds 16 MiB")

ErrManifestTooLarge marks a manifest skipped for exceeding the read cap; joined into the scan error like any parse failure.

Functions

func NameIndex

func NameIndex(owners []Owner) (names map[string]string, ambiguous []string)

NameIndex maps every manifest name onto the package it belongs to, under one rule shared by every consumer of the mapping: a stated name binds before a declared one, and a root manifest before a nested one (a package's own identity beats a vendored or example manifest deeper inside another package). A name two packages claim at the same rank is ambiguous, returned in ambiguous (sorted) instead of mapped, because deriving relations from it would be guessing.

func ResolveLocalDir

func ResolveLocalDir(dirs map[string]string, pkgDir, manifestRel, local string) string

ResolveLocalDir maps a declared local path (an npm "file:" range, a go.mod relative replace, a Cargo `path` key) onto the package whose folder it points into: dirs indexes cleaned package folders by name, pkgDir is the consuming package's folder, manifestRel the declaring manifest's slash-relative path inside it. The lookup ascends from the exact target, so a path into a package's sub-folder still finds the package. Empty when the path leaves every known package.

func SkipDir

func SkipDir(name string) bool

SkipDir reports a folder name a workspace walk must not enter: the dependency trees, virtual environments and build output listed above, plus every dot-folder. It is exported so a caller walking a package folder for some other reason stays out of exactly the same places rather than keeping a second list that drifts from this one.

It is not the rule Scan follows; SkipWorkspaceDir is. The two differ by the engine output folders, which hold generated copies of real manifests but may still hold a file a caller means to read. A tool replacing literal text uses this one, because a version string under Build/ is still a version string.

func SkipWorkspaceDir added in v1.1.0

func SkipWorkspaceDir(name string) bool

SkipWorkspaceDir reports a folder no search for manifests should enter: everything SkipDir names, plus the folders a game engine generates. It is the rule Scan follows.

Types

type DeclaredDep

type DeclaredDep struct {
	// Name as the manifest declares it: "@acme/core", "github.com/acme/x",
	// a crate name. For a renamed Cargo dependency this is the real package
	// name (the `package` key), not the alias.
	Name string
	// Range is the declared version text, verbatim: "^1.2.0", "workspace:*",
	// "v1.2.3". Empty when the manifest declares no version (e.g. a Cargo
	// path-only dependency).
	Range string
	// Kind is the manifest field the declaration sits in.
	Kind Kind
	// LocalPath is the declared filesystem path when the dependency points into
	// the same repository (an npm "file:"/"link:" range, a go.mod replace to a
	// relative path, a Cargo `path` key) relative to the manifest's folder. Empty
	// otherwise. It is the strongest workspace-edge signal: it survives name
	// mismatches between folder and manifest.
	LocalPath string
}

DeclaredDep is one dependency declaration inside a manifest.

type Ecosystem

type Ecosystem string

Ecosystem names the package manager or platform family a manifest belongs to. Several formats can share one: every NuGet list is "nuget", and a Podfile and a podspec are both "cocoapods".

const (
	EcosystemNpm       Ecosystem = "npm"       // package.json
	EcosystemGoMod     Ecosystem = "gomod"     // go.mod
	EcosystemCargo     Ecosystem = "cargo"     // Cargo.toml
	EcosystemPython    Ecosystem = "python"    // pyproject.toml (PEP 621 and Poetry)
	EcosystemComposer  Ecosystem = "composer"  // composer.json
	EcosystemMaven     Ecosystem = "maven"     // pom.xml
	EcosystemNuGet     Ecosystem = "nuget"     // *.csproj, *.nuspec, packages.config
	EcosystemPub       Ecosystem = "pub"       // pubspec.yaml
	EcosystemPlist     Ecosystem = "plist"     // Info.plist
	EcosystemCocoaPods Ecosystem = "cocoapods" // Podfile, *.podspec
	EcosystemXcode     Ecosystem = "xcode"     // project.pbxproj
	EcosystemAndroid   Ecosystem = "android"   // AndroidManifest.xml
	EcosystemGradle    Ecosystem = "gradle"    // libs.versions.toml, build.gradle(.kts)
	EcosystemRubyGems  Ecosystem = "rubygems"  // Gemfile, *.gemspec
	EcosystemDocker    Ecosystem = "docker"    // Dockerfile, compose.yaml

	// The game engines, each named after the engine rather than a package
	// manager, because that is what resolves their manifests.
	EcosystemUnity  Ecosystem = "unity"  // Packages/manifest.json, ProjectSettings.asset
	EcosystemGodot  Ecosystem = "godot"  // project.godot, plugin.cfg, export_presets.cfg
	EcosystemUnreal Ecosystem = "unreal" // *.uproject, *.uplugin, Config/Default*.ini
	EcosystemDefold Ecosystem = "defold" // game.project
	EcosystemO3DE   Ecosystem = "o3de"   // project.json, gem.json
)

Ecosystems the built-in parsers recognise. The names are spelled after the manifest format rather than the platform that ships it: Info.plist is Apple bundle metadata on macOS and tvOS as much as on iOS, and a Gradle build script is not exclusively Android.

func EcosystemOf

func EcosystemOf(f manifest.Format) Ecosystem

EcosystemOf reports the ecosystem a format's manifests belong to.

type Kind

type Kind = manifest.Kind

Kind is the manifest dependency field a declaration came from: the shared pkg/manifest vocabulary, aliased so the reader and the writer can never disagree on what a kind is called.

type Manifest

type Manifest struct {
	// Path of the manifest file relative to the scanned folder, using slashes.
	Path string
	// Ecosystem the manifest belongs to: one of the Ecosystem* constants.
	Ecosystem Ecosystem
	// Name is the package's declared name; empty when the ecosystem has no
	// name field or the manifest omits it.
	Name string
	// Version is the package's declared own version; empty when absent
	// (go.mod has none by design).
	Version string
	// BuildNumber is the monotonic build counter the mobile formats carry beside
	// their marketing version, CFBundleVersion, android:versionCode,
	// CURRENT_PROJECT_VERSION. It is not a semantic version, so no version
	// write ever moves it; the writer's SetBuild is the dedicated write.
	// Empty for every format without one.
	BuildNumber string
	// Deps are the manifest's declared dependencies, sorted by field then
	// name for deterministic output.
	Deps []DeclaredDep
	// Indirect are the requirements the manifest records as transitive
	// bookkeeping rather than as its own declarations, sorted the same way.
	// Only go.mod has the distinction, and only its parser fills this; every
	// other format leaves it nil.
	//
	// They are kept apart from Deps because they are not something the package
	// asked for, so reconciling their ranges would rewrite a number the
	// toolchain owns. What they are good for is redirection: only a main
	// module's replace directives govern a Go build, so a module reached
	// transitively still has to be pointed at a local folder from here. A
	// requirement listed in Deps never appears here as well.
	Indirect []DeclaredDep
	// Dropped are the entries the manifest declared but the parser could not
	// coerce into a dependency, one line each ("service db: not a mapping"),
	// sorted for deterministic output. They are not errors: the manifest
	// parsed, and the caller decides whether the drops are worth reporting.
	// The shapes a format reads selectively by design (a Gradle line built by
	// code, a platform-specific Poetry constraint list) are not dropped
	// entries; those live in each reader's documented limits.
	Dropped []string
	// Root reports that the manifest sits directly in the scanned folder
	// rather than in a sub-folder.
	Root bool
}

Manifest is one parsed manifest file.

func Scan

func Scan(ctx context.Context, dir string) ([]Manifest, error)

Scan is the package-level convenience over New().Scan.

func ScanRoot

func ScanRoot(ctx context.Context, dir string) ([]Manifest, error)

ScanRoot is the package-level convenience over New().ScanRoot.

func (Manifest) AtPackageRoot added in v1.1.1

func (m Manifest) AtPackageRoot() bool

AtPackageRoot reports that the manifest is the scanned folder's own rather than one belonging to something nested inside it.

Root answers that for every format whose location its author chose. The path-qualified formats are the exception: their folder is part of the format's name, so a Unity project keeps its settings at ProjectSettings/ProjectSettings.asset and an Unreal project keeps its version under Config/ because the engine says so, not because somebody filed them away there. Such a manifest is nested and still the scanned folder's own. A copy deeper in the tree is not, and stays excluded.

type Owner

type Owner struct {
	Package string
	// Names are the manifest names the package is known by regardless of what
	// its files say. They exist for the packages whose manifests declare no
	// name a workspace can learn: a Gradle module, a bare Makefile project, a
	// folder whose manifest this package cannot parse. A stated name outranks
	// a declared one, since it is the operator saying so.
	Names []string
	// Manifests are the package's parsed manifests.
	Manifests []Manifest
}

Owner is one package's identity as NameIndex sees it: the names its configuration states outright, and the names its manifests declare.

type Scanner

type Scanner interface {
	// Scan returns every recognised manifest under dir in deterministic
	// (path-sorted) order, descending into sub-folders but skipping
	// dependency and build-output folders (node_modules, vendor, dist, ...,
	// and every dot-folder).
	Scan(ctx context.Context, dir string) ([]Manifest, error)
	// ScanRoot parses only the manifests sitting directly in dir (the files that
	// declare the folder's own identity) without descending anywhere.
	ScanRoot(ctx context.Context, dir string) ([]Manifest, error)
}

Scanner turns a folder into its parsed manifests. Both methods share one error contract: a manifest that fails to parse is skipped, its error joined into the returned error, and the successfully parsed manifests are returned either way, so callers may report the error and keep the partial result. A folder Scan cannot read is stepped over on the same terms, so one unreadable sub-tree costs its own manifests and no others.

func New

func New() Scanner

New returns the filesystem-backed Scanner.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL