Add relationships for ALPM packages (arch linux) (#2851)

* add alpm relationships

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* tweak reader linter rule to check for reader impl

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update JSON schema with alpm dependency information

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2024-05-07 13:29:46 -04:00 committed by GitHub
parent e7b6284039
commit ada8f009d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 3078 additions and 266 deletions

View File

@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.7"
JSONSchemaVersion = "16.0.8"
)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.7/document",
"$id": "anchore.io/schema/syft/json/16.0.8/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -46,6 +46,18 @@
"$ref": "#/$defs/AlpmFileRecord"
},
"type": "array"
},
"provides": {
"items": {
"type": "string"
},
"type": "array"
},
"depends": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object",

View File

@ -27,6 +27,8 @@ type AlpmDBEntry struct {
Reason int `mapstructure:"reason" json:"reason"`
Files []AlpmFileRecord `mapstructure:"files" json:"files"`
Backup []AlpmFileRecord `mapstructure:"backup" json:"backup"`
Provides []string `mapstructure:"provides" json:"provides,omitempty"`
Depends []string `mapstructure:"depends" json:"depends,omitempty"`
}
type AlpmFileRecord struct {

View File

@ -4,12 +4,96 @@ Package arch provides a concrete Cataloger implementations for packages relating
package arch
import (
"context"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
type cataloger struct {
*generic.Cataloger
}
// NewDBCataloger returns a new cataloger object initialized for arch linux pacman database flat-file stores.
func NewDBCataloger() pkg.Cataloger {
return generic.NewCataloger("alpm-db-cataloger").
WithParserByGlobs(parseAlpmDB, pkg.AlpmDBGlob)
return cataloger{
Cataloger: generic.NewCataloger("alpm-db-cataloger").
WithParserByGlobs(parseAlpmDB, pkg.AlpmDBGlob),
}
}
func (c cataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
pkgs, rels, err := c.Cataloger.Catalog(ctx, resolver)
if err != nil {
return nil, nil, err
}
rels = append(rels, associateRelationships(pkgs)...)
return pkgs, rels, nil
}
// associateRelationships will create relationships between packages based on the "Depends" and "Provides"
// fields for installed packages. If there is an installed package that has a dependency that is (somehow) not installed,
// then that relationship (between the installed and uninstalled package) will NOT be created.
func associateRelationships(pkgs []pkg.Package) (relationships []artifact.Relationship) {
// map["provides" + "package"] -> packages that provide that package
lookup := make(map[string][]pkg.Package)
// read providers and add lookup keys as needed
for _, p := range pkgs {
meta, ok := p.Metadata.(pkg.AlpmDBEntry)
if !ok {
log.Warnf("cataloger failed to extract alpm 'provides' metadata for package %+v", p.Name)
continue
}
// allow for lookup by package name
lookup[p.Name] = append(lookup[p.Name], p)
for _, provides := range meta.Provides {
// allow for lookup by exact specification
lookup[provides] = append(lookup[provides], p)
// allow for lookup by library name only
k := stripVersionSpecifier(provides)
lookup[k] = append(lookup[k], p)
}
}
// read "Depends" and match with provider keys
for _, p := range pkgs {
meta, ok := p.Metadata.(pkg.AlpmDBEntry)
if !ok {
log.Warnf("cataloger failed to extract alpm 'dependency' metadata for package %+v", p.Name)
continue
}
for _, dep := range meta.Depends {
for _, depPkg := range lookup[dep] {
relationships = append(relationships, artifact.Relationship{
From: depPkg,
To: p,
Type: artifact.DependencyOfRelationship,
})
}
}
}
return relationships
}
func stripVersionSpecifier(s string) string {
// examples:
// gcc-libs --> gcc-libs
// libtree-sitter.so=0-64 --> libtree-sitter.so
items := strings.Split(s, "=")
if len(items) == 0 {
return s
}
return strings.TrimSpace(items[0])
}

View File

@ -12,20 +12,117 @@ import (
)
func TestAlpmCataloger(t *testing.T) {
dbLocation := file.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/desc")
expectedPkgs := []pkg.Package{
gmpDbLocation := file.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/desc")
treeSitterDbLocation := file.NewLocation("var/lib/pacman/local/tree-sitter-0.22.6-1/desc")
emacsDbLocation := file.NewLocation("var/lib/pacman/local/emacs-29.3-3/desc")
fuzzyDbLocation := file.NewLocation("var/lib/pacman/local/fuzzy-1.2-3/desc")
madeupDbLocation := file.NewLocation("var/lib/pacman/local/madeup-20.30-4/desc")
treeSitterPkg := pkg.Package{
Name: "tree-sitter",
Version: "0.22.6-1",
Type: pkg.AlpmPkg,
FoundBy: "alpm-db-cataloger",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", treeSitterDbLocation),
),
Locations: file.NewLocationSet(treeSitterDbLocation),
Metadata: pkg.AlpmDBEntry{
BasePackage: "tree-sitter",
Package: "tree-sitter",
Version: "0.22.6-1",
Description: "Incremental parsing library",
Architecture: "x86_64",
Size: 223539,
Packager: "Daniel M. Capella <polyzen@archlinux.org>",
URL: "https://github.com/tree-sitter/tree-sitter",
Validation: "pgp",
Reason: 1,
Files: []pkg.AlpmFileRecord{},
Backup: []pkg.AlpmFileRecord{},
Provides: []string{"libtree-sitter.so=0-64"},
},
}
emacsPkg := pkg.Package{
Name: "emacs",
Version: "29.3-3",
Type: pkg.AlpmPkg,
FoundBy: "alpm-db-cataloger",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("GPL3", emacsDbLocation),
),
Locations: file.NewLocationSet(emacsDbLocation),
Metadata: pkg.AlpmDBEntry{
BasePackage: "emacs",
Package: "emacs",
Version: "29.3-3",
Description: "The extensible, customizable, self-documenting real-time display editor",
Architecture: "x86_64",
Size: 126427862,
Packager: "Frederik Schwan <freswa@archlinux.org>",
URL: "https://www.gnu.org/software/emacs/emacs.html",
Validation: "pgp",
Files: []pkg.AlpmFileRecord{},
Backup: []pkg.AlpmFileRecord{},
Depends: []string{"libtree-sitter.so=0-64"},
},
}
fuzzyPkg := pkg.Package{
Name: "fuzzy",
Version: "1.2-3",
Type: pkg.AlpmPkg,
FoundBy: "alpm-db-cataloger",
Locations: file.NewLocationSet(
fuzzyDbLocation,
file.NewLocation("var/lib/pacman/local/fuzzy-1.2-3/files"),
),
Metadata: pkg.AlpmDBEntry{
Package: "fuzzy",
Version: "1.2-3",
Files: []pkg.AlpmFileRecord{},
Backup: []pkg.AlpmFileRecord{
{
Path: "/etc/fuzzy.conf",
Digests: []file.Digest{
{Algorithm: "md5", Value: "79fce043df7dfc676ae5ecb903762d8b"},
},
},
},
Depends: []string{"tree-sitter"},
},
}
madeupPkg := pkg.Package{
Name: "madeup",
Version: "20.30-4",
Type: pkg.AlpmPkg,
FoundBy: "alpm-db-cataloger",
Locations: file.NewLocationSet(madeupDbLocation),
Metadata: pkg.AlpmDBEntry{
Package: "madeup",
Version: "20.30-4",
Files: []pkg.AlpmFileRecord{},
Backup: []pkg.AlpmFileRecord{},
Depends: []string{"libtree-sitter.so"},
},
}
gmpPkg := pkg.Package{
Name: "gmp",
Version: "6.2.1-2",
Type: pkg.AlpmPkg,
FoundBy: "alpm-db-cataloger",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("LGPL3", dbLocation),
pkg.NewLicenseFromLocations("GPL", dbLocation),
pkg.NewLicenseFromLocations("LGPL3", gmpDbLocation),
pkg.NewLicenseFromLocations("GPL", gmpDbLocation),
),
Locations: file.NewLocationSet(
gmpDbLocation,
file.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/files"),
file.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/mtree"),
),
Locations: file.NewLocationSet(dbLocation),
CPEs: nil,
PURL: "",
Metadata: pkg.AlpmDBEntry{
BasePackage: "gmp",
Package: "gmp",
@ -37,6 +134,7 @@ func TestAlpmCataloger(t *testing.T) {
URL: "https://gmplib.org/",
Validation: "pgp",
Reason: 1,
Depends: []string{"gcc-libs", "sh", "libtree-sitter.so=1-64"},
Files: []pkg.AlpmFileRecord{
{
Path: "/usr",
@ -167,14 +265,36 @@ func TestAlpmCataloger(t *testing.T) {
},
Backup: []pkg.AlpmFileRecord{},
},
}
expectedPkgs := []pkg.Package{
treeSitterPkg,
emacsPkg,
fuzzyPkg,
madeupPkg,
gmpPkg,
}
expectedRelationships := []artifact.Relationship{
{ // exact spec lookup
From: treeSitterPkg,
To: emacsPkg,
Type: artifact.DependencyOfRelationship,
},
{ // package name lookup
From: treeSitterPkg,
To: fuzzyPkg,
Type: artifact.DependencyOfRelationship,
},
{ // library name lookup
From: treeSitterPkg,
To: madeupPkg,
Type: artifact.DependencyOfRelationship,
},
}
// TODO: relationships are not under test yet
var expectedRelationships []artifact.Relationship
pkgtest.NewCatalogTester().
FromDirectory(t, "test-fixtures/gmp-fixture").
FromDirectory(t, "test-fixtures/installed").
WithCompareOptions(cmpopts.IgnoreFields(pkg.AlpmFileRecord{}, "Time")).
Expects(expectedPkgs, expectedRelationships).
TestCataloger(t, NewDBCataloger())

View File

@ -9,13 +9,16 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func newPackage(m *parsedData, release *linux.Release, dbLocation file.Location) pkg.Package {
func newPackage(m *parsedData, release *linux.Release, dbLocation file.Location, otherLocations ...file.Location) pkg.Package {
licenseCandidates := strings.Split(m.Licenses, "\n")
locs := file.NewLocationSet(dbLocation)
locs.Add(otherLocations...)
p := pkg.Package{
Name: m.Package,
Version: m.Version,
Locations: file.NewLocationSet(dbLocation),
Locations: locs,
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbLocation.WithoutAnnotations(), licenseCandidates...)...),
Type: pkg.AlpmPkg,
PURL: packageURL(m, release),

View File

@ -6,6 +6,7 @@ import (
"context"
"fmt"
"io"
"path"
"path/filepath"
"strconv"
"strings"
@ -15,6 +16,7 @@ import (
"github.com/vbatts/go-mtree"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@ -44,33 +46,26 @@ func parseAlpmDB(_ context.Context, resolver file.Resolver, env *generic.Environ
return nil, nil, err
}
base := filepath.Dir(reader.RealPath)
r, err := getFileReader(filepath.Join(base, "mtree"), resolver)
if err != nil {
return nil, nil, err
if data == nil {
return nil, nil, nil
}
pkgFiles, err := parseMtree(r)
if err != nil {
return nil, nil, err
}
base := path.Dir(reader.RealPath)
// replace the files found the pacman database with the files from the mtree These contain more metadata and
// thus more useful.
// TODO: probably want to use MTREE and PKGINFO here
data.Files = pkgFiles
files, fileLoc := fetchPkgFiles(base, resolver)
backups, backupLoc := fetchBackupFiles(base, resolver)
// We only really do this to get any backup database entries from the files database
files := filepath.Join(base, "files")
_, err = getFileReader(files, resolver)
if err != nil {
return nil, nil, err
var locs []file.Location
if fileLoc != nil {
locs = append(locs, fileLoc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
data.Files = files
}
filesMetadata, err := parseAlpmDBEntry(reader)
if err != nil {
return nil, nil, err
} else if filesMetadata != nil {
data.Backup = filesMetadata.Backup
if backupLoc != nil {
locs = append(locs, backupLoc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
data.Backup = backups
}
if data.Package == "" {
@ -82,10 +77,67 @@ func parseAlpmDB(_ context.Context, resolver file.Resolver, env *generic.Environ
data,
env.LinuxRelease,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
locs...,
),
}, nil, nil
}
func fetchPkgFiles(base string, resolver file.Resolver) ([]pkg.AlpmFileRecord, *file.Location) {
// TODO: probably want to use MTREE and PKGINFO here
target := path.Join(base, "mtree")
loc, err := getLocation(target, resolver)
if err != nil {
log.WithFields("error", err, "path", target).Trace("failed to find mtree file")
return []pkg.AlpmFileRecord{}, nil
}
if loc == nil {
return []pkg.AlpmFileRecord{}, nil
}
reader, err := resolver.FileContentsByLocation(*loc)
if err != nil {
return []pkg.AlpmFileRecord{}, nil
}
defer internal.CloseAndLogError(reader, loc.RealPath)
pkgFiles, err := parseMtree(reader)
if err != nil {
log.WithFields("error", err, "path", target).Trace("failed to parse mtree file")
return []pkg.AlpmFileRecord{}, nil
}
return pkgFiles, loc
}
func fetchBackupFiles(base string, resolver file.Resolver) ([]pkg.AlpmFileRecord, *file.Location) {
// We only really do this to get any backup database entries from the files database
target := filepath.Join(base, "files")
loc, err := getLocation(target, resolver)
if err != nil {
log.WithFields("error", err, "path", target).Trace("failed to find alpm files")
return []pkg.AlpmFileRecord{}, nil
}
if loc == nil {
return []pkg.AlpmFileRecord{}, nil
}
reader, err := resolver.FileContentsByLocation(*loc)
if err != nil {
return []pkg.AlpmFileRecord{}, nil
}
defer internal.CloseAndLogError(reader, loc.RealPath)
filesMetadata, err := parseAlpmDBEntry(reader)
if err != nil {
return []pkg.AlpmFileRecord{}, nil
}
if filesMetadata != nil {
return filesMetadata.Backup, loc
}
return []pkg.AlpmFileRecord{}, loc
}
func parseAlpmDBEntry(reader io.Reader) (*parsedData, error) {
scanner := newScanner(reader)
metadata, err := parseDatabase(scanner)
@ -119,7 +171,7 @@ func newScanner(reader io.Reader) *bufio.Scanner {
return scanner
}
func getFileReader(path string, resolver file.Resolver) (io.Reader, error) {
func getLocation(path string, resolver file.Resolver) (*file.Location, error) {
locs, err := resolver.FilesByPath(path)
if err != nil {
return nil, err
@ -128,13 +180,11 @@ func getFileReader(path string, resolver file.Resolver) (io.Reader, error) {
if len(locs) == 0 {
return nil, fmt.Errorf("could not find file: %s", path)
}
// TODO: Should we maybe check if we found the file
dbContentReader, err := resolver.FileContentsByLocation(locs[0])
if err != nil {
return nil, err
if len(locs) > 1 {
log.WithFields("path", path).Trace("multiple files found for path, using first path")
}
defer internal.CloseAndLogError(dbContentReader, locs[0].RealPath)
return dbContentReader, nil
return &locs[0], nil
}
func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
@ -157,9 +207,9 @@ func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
case "files":
var files []map[string]string
for _, f := range strings.Split(value, "\n") {
path := fmt.Sprintf("/%s", f)
if ok := ignoredFiles[path]; !ok {
files = append(files, map[string]string{"path": path})
p := fmt.Sprintf("/%s", f)
if ok := ignoredFiles[p]; !ok {
files = append(files, map[string]string{"path": p})
}
}
pkgFields[key] = files
@ -167,10 +217,10 @@ func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
var backup []map[string]interface{}
for _, f := range strings.Split(value, "\n") {
fields := strings.SplitN(f, "\t", 2)
path := fmt.Sprintf("/%s", fields[0])
if ok := ignoredFiles[path]; !ok {
p := fmt.Sprintf("/%s", fields[0])
if ok := ignoredFiles[p]; !ok {
backup = append(backup, map[string]interface{}{
"path": path,
"path": p,
"digests": []file.Digest{{
Algorithm: "md5",
Value: fields[1],
@ -178,6 +228,8 @@ func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
}
}
pkgFields[key] = backup
case "depends", "provides":
pkgFields[key] = processLibrarySpecs(value)
case "reason":
fallthrough
case "size":
@ -193,6 +245,19 @@ func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
return parsePkgFiles(pkgFields)
}
func processLibrarySpecs(value string) []string {
lines := strings.Split(value, "\n")
librarySpecs := make([]string, 0)
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
librarySpecs = append(librarySpecs, line)
}
return librarySpecs
}
func parsePkgFiles(pkgFields map[string]interface{}) (*parsedData, error) {
var entry parsedData
if err := mapstructure.Decode(pkgFields, &entry); err != nil {
@ -203,6 +268,10 @@ func parsePkgFiles(pkgFields map[string]interface{}) (*parsedData, error) {
entry.Backup = make([]pkg.AlpmFileRecord, 0)
}
if entry.Files == nil {
entry.Files = make([]pkg.AlpmFileRecord, 0)
}
if entry.Package == "" && len(entry.Files) == 0 && len(entry.Backup) == 0 {
return nil, nil
}

View File

@ -17,12 +17,13 @@ func TestDatabaseParser(t *testing.T) {
tests := []struct {
name string
fixture string
expected pkg.AlpmDBEntry
expected *parsedData
}{
{
name: "test alpm database parsing",
name: "simple desc parsing",
fixture: "test-fixtures/files",
expected: pkg.AlpmDBEntry{
expected: &parsedData{
AlpmDBEntry: pkg.AlpmDBEntry{
Backup: []pkg.AlpmFileRecord{
{
Path: "/etc/pacman.conf",
@ -88,6 +89,51 @@ func TestDatabaseParser(t *testing.T) {
},
},
},
},
{
name: "with dependencies",
fixture: "test-fixtures/installed/var/lib/pacman/local/gmp-6.2.1-2/desc",
expected: &parsedData{
Licenses: "LGPL3\nGPL",
AlpmDBEntry: pkg.AlpmDBEntry{
BasePackage: "gmp",
Package: "gmp",
Version: "6.2.1-2",
Description: "A free library for arbitrary precision arithmetic",
Architecture: "x86_64",
Size: 1044438,
Packager: "Antonio Rojas <arojas@archlinux.org>",
URL: "https://gmplib.org/",
Validation: "pgp",
Reason: 1,
Files: []pkg.AlpmFileRecord{},
Backup: []pkg.AlpmFileRecord{},
Depends: []string{"gcc-libs", "sh", "libtree-sitter.so=1-64"},
},
},
},
{
name: "with provides",
fixture: "test-fixtures/installed/var/lib/pacman/local/tree-sitter-0.22.6-1/desc",
expected: &parsedData{
Licenses: "MIT",
AlpmDBEntry: pkg.AlpmDBEntry{
BasePackage: "tree-sitter",
Package: "tree-sitter",
Version: "0.22.6-1",
Description: "Incremental parsing library",
Architecture: "x86_64",
Size: 223539,
Packager: "Daniel M. Capella <polyzen@archlinux.org>",
URL: "https://github.com/tree-sitter/tree-sitter",
Validation: "pgp",
Reason: 1,
Files: []pkg.AlpmFileRecord{},
Backup: []pkg.AlpmFileRecord{},
Provides: []string{"libtree-sitter.so=0-64"},
},
},
},
}
for _, test := range tests {
@ -101,13 +147,10 @@ func TestDatabaseParser(t *testing.T) {
entry, err := parseAlpmDBEntry(reader)
require.NoError(t, err)
if diff := cmp.Diff(entry.Files, test.expected.Files); diff != "" {
t.Errorf("Files mismatch (-want +got):\n%s", diff)
if diff := cmp.Diff(test.expected, entry); diff != "" {
t.Errorf("parsed data mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(entry.Backup, test.expected.Backup); diff != "" {
t.Errorf("Backup mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@ -0,0 +1,38 @@
%NAME%
emacs
%VERSION%
29.3-3
%BASE%
emacs
%DESC%
The extensible, customizable, self-documenting real-time display editor
%URL%
https://www.gnu.org/software/emacs/emacs.html
%ARCH%
x86_64
%BUILDDATE%
1714249917
%INSTALLDATE%
1715026363
%PACKAGER%
Frederik Schwan <freswa@archlinux.org>
%SIZE%
126427862
%LICENSE%
GPL3
%VALIDATION%
pgp
%DEPENDS%
libtree-sitter.so=0-64

View File

@ -0,0 +1,8 @@
%NAME%
fuzzy
%VERSION%
1.2-3
%DEPENDS%
tree-sitter

View File

@ -0,0 +1,6 @@
%FILES%
etc/
etc/fuzzy.conf
%BACKUP%
etc/fuzzy.conf 79fce043df7dfc676ae5ecb903762d8b

View File

@ -0,0 +1,8 @@
%NAME%
madeup
%VERSION%
20.30-4
%DEPENDS%
libtree-sitter.so

View File

@ -0,0 +1,41 @@
%NAME%
tree-sitter
%VERSION%
0.22.6-1
%BASE%
tree-sitter
%DESC%
Incremental parsing library
%URL%
https://github.com/tree-sitter/tree-sitter
%ARCH%
x86_64
%BUILDDATE%
1714945746
%INSTALLDATE%
1715026360
%PACKAGER%
Daniel M. Capella <polyzen@archlinux.org>
%SIZE%
223539
%REASON%
1
%LICENSE%
MIT
%VALIDATION%
pgp
%PROVIDES%
libtree-sitter.so=0-64

View File

@ -8,8 +8,8 @@ import "github.com/quasilyte/go-ruleguard/dsl"
func resourceCleanup(m dsl.Matcher) {
m.Match(`$res, $err := $resolver.FileContentsByLocation($loc); if $*_ { $*_ }; $next`).
Where(m["res"].Type.Implements(`io.Closer`) &&
m["res"].Type.Implements(`io.Reader`) &&
m["err"].Type.Implements(`error`) &&
m["res"].Type.Implements(`io.Closer`) &&
!m["next"].Text.Matches(`defer internal.CloseAndLogError`)).
Report(`please call "defer internal.CloseAndLogError($res, $loc.RealPath)" right after checking the error returned from $resolver.FileContentsByLocation.`)
}