Custom scanners

edit

Use a regular expression

RegexScanner reads the files that match its patterns with a regular expression. It is enough for formats where a dependency is written on one line:

var options = new ScannerOptions
{
    Scanners =
    [
        new RegexScanner
        {
            FilePatterns = [Glob.Parse("**/*.custom", GlobDialect.Standard, GlobOptions.IgnoreCase)],
            DependencyType = DependencyType.DockerImage,
            Regex = new Regex(@"image:\s*(?<name>[a-z/]+)(:(?<version>[0-9.]+))?", RegexOptions.ExplicitCapture, TimeSpan.FromSeconds(10)),
        },
    ],
};

var dependencies = await DependencyScanner.ScanDirectoryAsync(
    "C:\\MyProject",
    options,
    cancellationToken);
  • FilePatterns are globs, from Meziantou.Framework.Globbing, matched against the path of the file relative to the scanned root folder, for instance build/*.yml or **/*.custom. See Globbing.
  • The regular expression must have a name group, and can have a version group. Each match is a dependency of DependencyType.
  • The name and the version are updatable.
  • Setting Scanners replaces the built-in scanners. Add the regex scanner to them to keep them, as in the next example.

Write a scanner

For other formats, derive from DependencyScanner:

// Reads the "name version" lines of the versions.txt files
public sealed class VersionsFileScanner : DependencyScanner
{
    protected override IReadOnlyCollection<DependencyType> SupportedDependencyTypes { get; } = [DependencyType.Unknown];

    // Called for every file: keep it cheap, the content is only read for the files it accepts
    protected override bool ShouldScanFileCore(CandidateFileContext context)
        => context.HasFileName("versions.txt", ignoreCase: true);

    public override async ValueTask ScanAsync(ScanFileContext context)
    {
        using var reader = new StreamReader(context.Content, leaveOpen: true);
        while (await reader.ReadLineAsync(context.CancellationToken) is { } line)
        {
            var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length is 2)
            {
                // Without a location, the dependency is reported but cannot be updated
                context.ReportDependency(this, parts[0], parts[1], DependencyType.Unknown, nameLocation: null, versionLocation: null);
            }
        }
    }
}

Add the scanner to the options:

var options = new ScannerOptions();

// Keep the built-in scanners, and add the custom one
options.Scanners = options.Scanners.Add(new VersionsFileScanner());

var dependencies = await DependencyScanner.ScanDirectoryAsync("C:\\MyProject", options, cancellationToken);

Make the values updatable

A dependency reported without locations is listed, but cannot be updated. To make it updatable, derive from Location: IsUpdatable says whether the value can be written, and UpdateCoreAsync receives the expected value and the new one. Read the file through the FileSystem of the location, check that it holds the expected value, and throw a DependencyScannerException when it does not, as the built-in locations do. See An update never writes blindly.