licenses

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 24 Imported by: 0

README

licenses

Go library and command for matching license text and scanning repositories against ScanCode's license rule corpus.

The corpus is embedded in the package. Matching needs no network access, cgo, or Python.

Install

Install the repository scanner with Homebrew:

brew tap git-pkgs/git-pkgs
brew trust --tap git-pkgs/git-pkgs
brew install licenses

Or with Go:

go install github.com/git-pkgs/licenses/cmd/licenses@latest

Add the matching library to a Go module:

go get github.com/git-pkgs/licenses

Scan a repository

licenses .
licenses /path/to/repository
licenses -json /path/to/repository > licenses.json
licenses -scope all -max-files 0 /path/to/repository

The command reports detections by file with the matching rule, expression, rule kind, score, coverage, and byte range. Each JSON file record also reports license_text_coverage, the percentage of decoded file bytes covered by the union of license-text and notice matches. JSON is used when output is redirected; terminals get a text report. Skipped files and directories are named with the reason they were skipped.

Reference rules that join text across document blocks are reported as clues outside files such as LICENSE, COPYING, and NOTICE. Soft-wrapped lines, including lines with a shared source-comment prefix, remain detections. Demoted matches stay visible in the file report but do not contribute to the repository expression totals.

The default project scope skips hidden, dependency, build, cache, and test-data directories. Use -scope all for dependency-license scans. An explicit -skip list applies in either scope. Both scopes exclude .git.

Regular text files are limited to 1 MiB and 32 directory levels. Project scope also has a 10,000-file default limit. All scope requires an explicit -max-files value; use -max-files 0 for an unlimited dependency scan. Setting any limit to zero removes that guard.

The scanner accepts UTF-8, UTF-16LE or UTF-16BE with a byte-order mark, and Latin-1. Reported byte ranges refer to the original file. JSON reports use schema version 2. Schema 2 is additive: consumers must ignore unknown fields and accept file records with empty detections when clues are present. With -matched-text, matched contains decoded UTF-8 rather than the original encoded bytes.

Detection and expression records include identification. Its value is identified, partial, or SPDX's NOASSERTION. Partial expressions contain both concrete identifiers and ScanCode placeholder LicenseRef-* values. NOASSERTION detections confirm license-related text without naming another license identifier.

JSON reports include a declared record for each recognized package manifest that declares license values or a license-file path. Each record preserves the raw manifest values and includes a normalized SPDX expression when all values can be normalized. Multiple values are joined with OR; values that cannot be normalized remain available in raw with an empty normalized_expression.

Reported expressions use canonical SPDX identifiers when ScanCode supplies one. Other ScanCode license keys use LicenseRef-scancode-<key>. ScanCode rule IDs remain in each match for traceability.

The three per-file summary counts overlap when one file contains detections in more than one identification state.

Each match reports the method that produced it. hash is a whole-file match against a rule text, exact is a rule token sequence found within the file, and spdx-id is an SPDX-License-Identifier tag line whose expression bytes are not already covered by a rule match. Tag expression grammar is parsed with github.com/git-pkgs/spdx, then bare identifiers are resolved against the embedded ScanCode mapping and normalized to the primary SPDX identifiers in ScanCode's metadata. Valid custom LicenseRef-* values become LicenseRef-scancode-unknown-spdx and report NOASSERTION. Malformed expressions and unknown bare identifiers do not produce an spdx-id match. Tag matches use the rule id spdx-license-identifier.

Exit status 0 means detections were found, 1 is a fatal command error, 2 means the scan was incomplete because of per-file errors or the file limit, and 3 means no conclusive detections were found.

Use the library

matcher, err := licenses.New()
if err != nil {
	return err
}

result, err := matcher.Match(ctx, text)
if err != nil {
	return err
}

for _, detection := range result.Detections {
	fmt.Println(detection.Expression)
}

Scan a file or directory with the same matcher:

options := licenses.DefaultScanOptions()
options.IncludeLegalFiles = true
report, err := licenses.ScanRepository(ctx, matcher, ".", options)
if err != nil {
	return err
}

for _, file := range report.Files {
	fmt.Println(file.Path, file.Roles, file.Text)
}

IncludeLegalFiles retains recognized license and notice files when the corpus does not produce a match. It also includes their complete text decoded to UTF-8.

Matching uses normalized whole-text hashes, exact token sequences, and SPDX-License-Identifier tag lines. It does not use fuzzy or sequence matching, so edits within a license text can prevent a match.

Corpus

The ScanCode commit is pinned in CORPUS_VERSION. Regenerate the embedded index from a clean checkout at that commit:

go run ./cmd/corpusgen \
  -scancode /path/to/scancode-toolkit \
  -version-file CORPUS_VERSION \
  -output internal/corpus/corpus.bin.gz

Conformance

The exact matcher passes 1,535 of 1,786 cases (85.95%) from ScanCode's four active data-driven detection suites. Run the suite against a ScanCode checkout at the commit in CORPUS_VERSION:

SCANCODE_TESTDATA=/path/to/scancode-toolkit/tests/licensedcode/data \
  go test . -run '^TestScanCodeConformanceExact$' -v

Known differences are recorded in the conformance baseline. CI fails if an existing result changes or a new difference appears.

Benchmarks

See docs/benchmarking.md for matcher, repository scanner, and Licensee comparison benchmarks.

License

The Go code is released under the MIT License. ScanCode's license and rule data is licensed under CC-BY-4.0. See NOTICE for attribution and modification details.

Documentation

Overview

Package licenses matches text and scans repositories against the ScanCode license rule corpus. Matching is exact after token normalization, so edits within a license can prevent a match.

Index

Constants

View Source
const (
	// ReportSchemaVersion is the additive JSON report schema version.
	ReportSchemaVersion = reportSchemaVersion
	// DefaultMaxDepth is the default maximum directory depth.
	DefaultMaxDepth = defaultMaxDepth
	// DefaultMaxFiles is the default maximum number of visited files.
	DefaultMaxFiles = defaultMaxFiles
	// DefaultMaxFileSize is the default maximum number of bytes per file.
	DefaultMaxFileSize = defaultMaxFileSize
	// ScannerName identifies this scanner in reports.
	ScannerName = scannerName
	// ScopeAll includes dependency, build, cache, and test-data directories.
	ScopeAll = scopeAll
	// ScopeProject skips dependency, build, cache, and test-data directories.
	ScopeProject = scopeProject
)

Variables

View Source
var ErrTooManyMatches = errors.New("licenses: too many exact-match candidates")

ErrTooManyMatches is returned when an input produces more exact-match candidates than the matcher can safely filter.

Functions

func LegalFileRoles added in v0.6.0

func LegalFileRoles(filePath string) []string

LegalFileRoles classifies a path as a license, notice, both, or neither.

func ValidateScanOptions added in v0.6.0

func ValidateScanOptions(options ScanOptions) error

ValidateScanOptions reports invalid limits or worker counts.

Types

type CorpusInfo

type CorpusInfo struct {
	Version      string // ScanCode version recorded in CORPUS_VERSION.
	RuleCount    int    // Number of license texts and rules in the index.
	SourceCommit string // Full ScanCode Toolkit source commit.
}

CorpusInfo identifies the ScanCode corpus used for a result.

type CorpusRecord added in v0.6.0

type CorpusRecord struct {
	Version      string `json:"version"`
	RuleCount    int    `json:"rule_count"`
	SourceCommit string `json:"source_commit"`
}

CorpusRecord identifies the ScanCode corpus used for a scan.

type DeclaredRecord added in v0.6.0

type DeclaredRecord struct {
	Path                 string   `json:"path"`
	Raw                  []string `json:"raw"`
	LicenseFile          string   `json:"license_file"`
	NormalizedExpression string   `json:"normalized_expression"`
}

DeclaredRecord contains license metadata read from a package manifest.

type Detection

type Detection struct {
	// Expression uses canonical SPDX identifiers where available and
	// LicenseRef-scancode-* identifiers for other ScanCode license keys.
	Expression string
	// Identification is derived from the identifiers in Expression.
	Identification Identification
	// Matches contains the rule matches that state Expression.
	Matches []Match
}

Detection groups matches that state the same license expression.

type DetectionRecord added in v0.6.0

type DetectionRecord struct {
	Expression     string         `json:"expression"`
	Identification Identification `json:"identification"`
	Matches        []MatchRecord  `json:"matches"`
}

DetectionRecord groups matches that report the same license expression.

type ExpressionRecord added in v0.6.0

type ExpressionRecord struct {
	Expression     string         `json:"expression"`
	Identification Identification `json:"identification"`
	Root           bool           `json:"root"`
	Files          int            `json:"files"`
	Matches        int            `json:"matches"`
}

ExpressionRecord summarizes a detected license expression across files.

type FileRecord added in v0.6.0

type FileRecord struct {
	Path                string            `json:"path"`
	Size                int64             `json:"size"`
	SHA256              string            `json:"sha256"`
	Encoding            string            `json:"encoding"`
	Text                string            `json:"text,omitempty"`
	Roles               []string          `json:"roles"`
	LicenseTextCoverage float64           `json:"license_text_coverage"`
	Detections          []DetectionRecord `json:"detections"`
	Clues               []MatchRecord     `json:"clues"`
}

FileRecord contains the license detections and clues reported for one file.

type Identification

type Identification string

Identification states whether a detected expression names concrete licenses.

const (
	// Identified means the expression contains no ScanCode placeholder
	// identifiers.
	Identified Identification = "identified"
	// Partial means the expression contains both non-placeholder and ScanCode
	// placeholder identifiers.
	Partial Identification = "partial"
	// NoAssertion uses SPDX's NOASSERTION term when the expression contains
	// only ScanCode placeholder identifiers.
	NoAssertion Identification = "NOASSERTION"
)

type Kind

type Kind string

Kind identifies the ScanCode category of a matched rule.

const (
	// KindUnknown identifies a rule without an is_license_* category.
	KindUnknown Kind = "unknown"
	// KindText identifies a full license text rule.
	KindText Kind = "text"
	// KindNotice identifies a license notice rule.
	KindNotice Kind = "notice"
	// KindTag identifies a license tag rule.
	KindTag Kind = "tag"
	// KindReference identifies a license reference rule.
	KindReference Kind = "reference"
	// KindIntro identifies a license introduction rule.
	KindIntro Kind = "intro"
	// KindClue identifies a weak license clue rule.
	KindClue Kind = "clue"
)

type Match

type Match struct {
	// RuleID is the ScanCode rule identifier.
	RuleID string
	// LicenseIDs contains the SPDX-compatible identifiers in the expression.
	LicenseIDs []string
	// Kind identifies the ScanCode category of the matched rule.
	Kind Kind
	// Method identifies the exact matching stage that produced the match.
	Method Method
	// Score is the rule's 0-100 relevance, not a similarity score.
	Score float64
	// Coverage is 100 for every exact match.
	Coverage float64
	// Start is the inclusive byte offset into the input.
	Start int
	// End is the exclusive byte offset into the input.
	End int
	// Matched is a copy of input[Start:End] when WithMatchedText is set.
	// It is nil otherwise.
	Matched []byte
}

Match describes one rule match in the input.

type MatchRecord added in v0.6.0

type MatchRecord struct {
	RuleID     string   `json:"rule_id"`
	LicenseIDs []string `json:"license_ids,omitempty"`
	Kind       Kind     `json:"kind"`
	Method     Method   `json:"method"`
	Score      float64  `json:"score"`
	Coverage   float64  `json:"coverage"`
	Start      int      `json:"start"`
	End        int      `json:"end"`
	Matched    string   `json:"matched,omitempty"`
}

MatchRecord describes one rule match in a scanned file.

type Matcher

type Matcher struct {
	// contains filtered or unexported fields
}

Matcher matches byte slices against an immutable embedded corpus.

func New

func New(options ...Option) (*Matcher, error)

New loads the embedded corpus. The decoded corpus is shared by every Matcher in the process.

func (*Matcher) Corpus

func (m *Matcher) Corpus() CorpusInfo

Corpus returns information about the embedded corpus used by m. It returns the zero value for a nil or uninitialized Matcher.

func (*Matcher) Match

func (m *Matcher) Match(ctx context.Context, b []byte) (Result, error)

Match finds exact normalized rule matches in b. It returns ErrTooManyMatches when the input exceeds the exact-match candidate limit; callers can identify it with errors.Is.

type Method

type Method string

Method identifies the matching stage that produced a match.

const (
	// Hash is a normalized whole-text hash match.
	Hash Method = "hash"
	// Exact is an exact token-sequence match within a larger input.
	Exact Method = "exact"
	// SpdxID is a strictly parsed SPDX-License-Identifier tag match.
	SpdxID Method = "spdx-id"
)

type Option

type Option func(*matcherOptions)

Option configures a Matcher.

func WithMatchedText

func WithMatchedText() Option

WithMatchedText includes a copy of each matched input range in Match.Matched.

type Result

type Result struct {
	Detections []Detection
	Clues      []Match
	Corpus     CorpusInfo
}

Result contains conclusive detections and weaker clue matches.

type ScanErrorRecord added in v0.6.0

type ScanErrorRecord struct {
	Path  string `json:"path"`
	Error string `json:"error"`
}

ScanErrorRecord describes an error associated with one path.

type ScanOptions added in v0.6.0

type ScanOptions struct {
	MaxDepth          int             // Maximum directory depth; zero disables the limit.
	MaxFiles          int             // Maximum number of visited files; zero disables the limit.
	MaxFileSize       int64           // Maximum bytes read per file; zero disables the limit.
	Workers           int             // Requested concurrent file scans, capped at 16.
	SkipDirs          map[string]bool // Directory base names to skip.
	NoDefaultSkip     bool            // Include hidden, dependency, build, cache, and test-data directories.
	IncludeLegalFiles bool            // Report legal files even when they contain no matches.
	ScannerVersion    string          // Scanner build version recorded in the report.
}

ScanOptions controls repository traversal and file scanning.

func DefaultScanOptions added in v0.6.0

func DefaultScanOptions() ScanOptions

DefaultScanOptions returns the default traversal limits and worker count.

type ScanReport added in v0.6.0

type ScanReport struct {
	Schema      int                `json:"schema"`
	Root        string             `json:"root"`
	Scope       string             `json:"scope"`
	Scanner     ScannerRecord      `json:"scanner"`
	Corpus      CorpusRecord       `json:"corpus"`
	Summary     ScanSummary        `json:"summary"`
	Declared    []DeclaredRecord   `json:"declared"`
	Expressions []ExpressionRecord `json:"expressions"`
	Files       []FileRecord       `json:"files"`
	Skipped     []SkipRecord       `json:"skipped"`
	Errors      []ScanErrorRecord  `json:"errors"`
}

ScanReport contains the deterministic result of scanning a file or directory.

func ScanRepository added in v0.6.0

func ScanRepository(
	ctx context.Context,
	matcher *Matcher,
	root string,
	options ScanOptions,
) (ScanReport, error)

ScanRepository scans a file or directory with matcher.

type ScanSummary added in v0.6.0

type ScanSummary struct {
	FilesVisited                   int   `json:"files_visited"`
	FilesScanned                   int   `json:"files_scanned"`
	FilesWithDetections            int   `json:"files_with_detections"`
	FilesWithIdentifiedDetections  int   `json:"files_with_identified_detections"`
	FilesWithPartialDetections     int   `json:"files_with_partial_detections"`
	FilesWithNoAssertionDetections int   `json:"files_with_noassertion_detections"`
	FilesWithClues                 int   `json:"files_with_clues"`
	BytesScanned                   int64 `json:"bytes_scanned"`
	DirectoriesSkipped             int   `json:"directories_skipped"`
	FilesSkippedBinary             int   `json:"files_skipped_binary"`
	FilesSkippedSize               int   `json:"files_skipped_size"`
	FilesSkippedOther              int   `json:"files_skipped_other"`
	ErrorCount                     int   `json:"error_count"`
	Truncated                      bool  `json:"truncated"`
}

ScanSummary contains file, byte, skip, and error counts for a scan.

type ScannerRecord added in v0.6.0

type ScannerRecord struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

ScannerRecord identifies the scanner and its build version.

type SkipRecord added in v0.6.0

type SkipRecord struct {
	Path   string `json:"path"`
	Reason string `json:"reason"`
}

SkipRecord describes a path omitted from a scan and the reason it was omitted.

Directories

Path Synopsis
cmd
corpusgen command
Command corpusgen builds the embedded license corpus from a ScanCode checkout.
Command corpusgen builds the embedded license corpus from a ScanCode checkout.
licenses command
Command licenses scans files and repositories for exact ScanCode license rule matches.
Command licenses scans files and repositories for exact ScanCode license rule matches.
internal
aho
Package aho implements a compact Aho-Corasick automaton over integer tokens.
Package aho implements a compact Aho-Corasick automaton over integer tokens.
tokenize
Package tokenize converts license text into normalized integer tokens.
Package tokenize converts license text into normalized integer tokens.

Jump to

Keyboard shortcuts

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