Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- KDL Document Language syntax validation (`.kdl`) via [sblinch/kdl-go](https://github.com/sblinch/kdl-go) (closes #463)
- Documentation website at https://boeing.github.io/config-file-validator
- `--reporter=github` option that emits validation errors as GitHub Actions workflow commands so they appear as inline PR annotations, without requiring the separate `validate-configs-action` wrapper (closes #459)
- `--ignore-file` option for applying gitignore-style patterns from files like `.dockerignore` or `.prettierignore` during file discovery (closes #457)
- Justfile syntax validation (`.just`, `justfile`, `Justfile`, `.justfile`) via embedded [go-just](https://github.com/Boeing/go-just) parser
- Automatic file type detection from GitHub Linguist's `languages.yml` via `go generate`
- ~90 known filenames auto-detected (`.babelrc`, `tsconfig.json`, `Pipfile`, `pom.xml`, `.gitconfig`, etc.)
Expand Down
58 changes: 58 additions & 0 deletions cmd/validator/testdata/ignore_file.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# --ignore-file skips files matched by explicit ignore files without a git repo
exec validator --ignore-file=.dockerignore project
stdout '✓.*keep.json'
! stdout 'ignored.json'
! stdout 'build'

# Multiple ignore files are additive
exec validator --ignore-file=.dockerignore --ignore-file=.prettierignore multi
stdout '✓.*keep.json'
! stdout 'drop.json'
! stdout 'skip.yaml'

# Missing ignore files are skipped
exec validator --ignore-file=missing.ignore project
stdout 'keep.json'
stdout 'ignored.json'

# .cfv.toml ignore-files key works
exec validator --config=cfv/.cfv.toml cfv
stdout '✓.*app.json'
! stdout 'out.json'

# CFV_IGNORE_FILES env var works
env CFV_IGNORE_FILES=.dockerignore,.prettierignore
exec validator multi
stdout '✓.*keep.json'
! stdout 'drop.json'
! stdout 'skip.yaml'

-- project/.dockerignore --
ignored.json
build/
-- project/keep.json --
{"key": "value"}
-- project/ignored.json --
{"ignored": true}
-- project/build/output.json --
{"build": true}

-- multi/.dockerignore --
drop.json
-- multi/.prettierignore --
skip.yaml
-- multi/keep.json --
{"key": "value"}
-- multi/drop.json --
{"drop": true}
-- multi/skip.yaml --
skip: true

-- cfv/.cfv.toml --
ignore-files = [".dockerignore"]
-- cfv/.dockerignore --
gen/
-- cfv/app.json --
{"app": true}
-- cfv/gen/out.json --
{"gen": true}
47 changes: 47 additions & 0 deletions cmd/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type validatorConfig struct {
configPath *string
noConfig *bool
gitignore *bool
ignoreFiles ignoreFileFlags
}

type reporterFlags []string
Expand Down Expand Up @@ -120,6 +121,17 @@ func (sf *schemaMapFlags) Set(value string) error {
return nil
}

type ignoreFileFlags []string

func (iff *ignoreFileFlags) String() string {
return fmt.Sprint(*iff)
}

func (iff *ignoreFileFlags) Set(value string) error {
*iff = append(*iff, value)
return nil
}

// Custom Usage function to cover. Uses the current flagSet when available.
func validatorUsage() {
fmt.Println("Usage: validator [OPTIONS] [<search_path>...]")
Expand Down Expand Up @@ -241,13 +253,22 @@ func getFlags(args []string) (validatorConfig, error) {
" --schema-map=\"**/config.xml:schemas/config.xsd\"",
)

ignoreFileConfigFlags := ignoreFileFlags{}
flagSet.Var(
&ignoreFileConfigFlags,
"ignore-file",
"Path to a gitignore-style file with patterns to skip during file discovery. "+
"Paths are relative to each search path. Can be specified multiple times.",
)

if err := flagSet.Parse(args); err != nil {
return validatorConfig{}, err
}

if err := applyDefaultFlagsFromEnv(); err != nil {
return validatorConfig{}, err
}
setIgnoreFilesFromEnvIfNotSet(&ignoreFileConfigFlags)

reporterConf, err := parseReporterFlags(reporterConfigFlags)
if err != nil {
Expand Down Expand Up @@ -287,6 +308,7 @@ func getFlags(args []string) (validatorConfig, error) {
configPathPtr,
noConfigPtr,
gitignorePtr,
ignoreFileConfigFlags,
}

return config, nil
Expand Down Expand Up @@ -500,6 +522,25 @@ func setFlagFromEnvIfNotSet(flagName string, envVar string) error {
return nil
}

func setIgnoreFilesFromEnvIfNotSet(flags *ignoreFileFlags) {
if isFlagSet("ignore-file") {
return
}

envVarValue, ok := os.LookupEnv("CFV_IGNORE_FILES")
if !ok || envVarValue == "" {
return
}

for _, ignoreFile := range strings.Split(envVarValue, ",") {
ignoreFile = strings.TrimSpace(ignoreFile)
if ignoreFile == "" {
continue
}
*flags = append(*flags, ignoreFile)
}
}

// Return the reporter associated with the
// reportType string
func getReporter(reportType, outputDest string) reporter.Reporter {
Expand Down Expand Up @@ -761,6 +802,9 @@ func buildFinderOpts(cfg validatorConfig, excludeFileTypes []string, fileTypes [
if *cfg.gitignore {
fsOpts = append(fsOpts, finder.WithGitignore(true))
}
if len(cfg.ignoreFiles) > 0 {
fsOpts = append(fsOpts, finder.WithIgnoreFiles([]string(cfg.ignoreFiles)))
}

return fsOpts, nil
}
Expand Down Expand Up @@ -918,6 +962,9 @@ func applyConfigFile(cfg *validatorConfig) (*configfile.ValidatorOptions, error)
if !isFlagSet("gitignore") && fileCfg.Gitignore != nil {
cfg.gitignore = fileCfg.Gitignore
}
if !isFlagSet("ignore-file") && len(fileCfg.IgnoreFiles) > 0 {
cfg.ignoreFiles = ignoreFileFlags(fileCfg.IgnoreFiles)
}
if len(cfg.schemaMap) == 0 && len(fileCfg.SchemaMap) > 0 {
for pattern, schema := range fileCfg.SchemaMap {
cfg.schemaMap = append(cfg.schemaMap, pattern+":"+schema)
Expand Down
40 changes: 40 additions & 0 deletions cmd/validator/validator_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -29,6 +31,8 @@ func Test_getFlags(t *testing.T) {
{"type-map", []string{"--type-map=**/inventory:ini", "."}, false},
{"multiple type-maps", []string{"--type-map=**/inventory:ini", "--type-map=**/configs/*:properties", "."}, false},
{"require-schema", []string{"--require-schema", "."}, false},
{"ignore-file", []string{"--ignore-file=.dockerignore", "."}, false},
{"multiple ignore-files", []string{"--ignore-file=.dockerignore", "--ignore-file=.prettierignore", "."}, false},

// Invalid flag combinations
{"negative depth", []string{"-depth=-1", "."}, true},
Expand Down Expand Up @@ -96,6 +100,42 @@ func Test_getFlagsRejectsDuplicateReporterOutputDest(t *testing.T) {
}
}

func Test_ignoreFilesEnvVar(t *testing.T) {
t.Setenv("CFV_IGNORE_FILES", ".dockerignore, .prettierignore")

cfg, err := getFlags([]string{"."})
require.NoError(t, err)
require.Equal(t, ignoreFileFlags{".dockerignore", ".prettierignore"}, cfg.ignoreFiles)
}

func Test_ignoreFilesConfigOverridesEnvVar(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, ".cfv.toml")
require.NoError(t, os.WriteFile(configPath, []byte(`ignore-files = ["config.ignore"]`), 0600))
t.Setenv("CFV_IGNORE_FILES", "env.ignore")

cfg, err := getFlags([]string{"--config=" + configPath, "."})
require.NoError(t, err)

_, err = applyConfigFile(&cfg)
require.NoError(t, err)
require.Equal(t, ignoreFileFlags{"config.ignore"}, cfg.ignoreFiles)
}

func Test_ignoreFilesFlagOverridesConfigAndEnv(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, ".cfv.toml")
require.NoError(t, os.WriteFile(configPath, []byte(`ignore-files = ["config.ignore"]`), 0600))
t.Setenv("CFV_IGNORE_FILES", "env.ignore")

cfg, err := getFlags([]string{"--config=" + configPath, "--ignore-file=cli.ignore", "."})
require.NoError(t, err)

_, err = applyConfigFile(&cfg)
require.NoError(t, err)
require.Equal(t, ignoreFileFlags{"cli.ignore"}, cfg.ignoreFiles)
}

func Test_getExcludeFileTypes(t *testing.T) {
cases := []struct {
name string
Expand Down
1 change: 1 addition & 0 deletions pkg/configfile/configfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const FileName = ".cfv.toml"
type Config struct {
ExcludeDirs []string `toml:"exclude-dirs"`
ExcludeFileTypes []string `toml:"exclude-file-types"`
IgnoreFiles []string `toml:"ignore-files"`
FileTypes []string `toml:"file-types"`
Depth *int `toml:"depth"`
Reporter []string `toml:"reporter"`
Expand Down
2 changes: 2 additions & 0 deletions pkg/configfile/configfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestLoadValid(t *testing.T) {
writeConfig(t, dir, `
exclude-dirs = ["node_modules", ".git"]
exclude-file-types = ["csv"]
ignore-files = [".dockerignore", ".prettierignore"]
depth = 2
quiet = true
schemastore = true
Expand All @@ -29,6 +30,7 @@ schemastore = true
require.NoError(t, err)
require.Equal(t, []string{"node_modules", ".git"}, cfg.ExcludeDirs)
require.Equal(t, []string{"csv"}, cfg.ExcludeFileTypes)
require.Equal(t, []string{".dockerignore", ".prettierignore"}, cfg.IgnoreFiles)
require.Equal(t, 2, *cfg.Depth)
require.True(t, *cfg.Quiet)
require.True(t, *cfg.SchemaStore)
Expand Down
5 changes: 5 additions & 0 deletions pkg/configfile/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
"items": { "type": "string" },
"description": "File types to ignore"
},
"ignore-files": {
"type": "array",
"items": { "type": "string" },
"description": "Gitignore-style pattern files to apply during file discovery"
},
"file-types": {
"type": "array",
"items": { "type": "string" },
Expand Down
51 changes: 51 additions & 0 deletions pkg/finder/finder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,57 @@ func Test_fsFinderGitignoreDisabled(t *testing.T) {
require.Len(t, files, 2, "without WithGitignore, .gitignore should have no effect")
}

func Test_fsFinderIgnoreFiles(t *testing.T) {
dir := t.TempDir()
testhelper.WriteFile(t, dir, "keep.json", testhelper.ValidContent["json"])
testhelper.WriteFile(t, dir, "drop.json", testhelper.ValidContent["json"])
build := testhelper.CreateSubdir(t, dir, "build")
testhelper.WriteFile(t, build, "skip.yaml", testhelper.ValidContent["yaml"])
testhelper.WriteFile(t, dir, ".dockerignore", "drop.json\nbuild/\n")

fsFinder := FileSystemFinderInit(
WithPathRoots(dir),
WithIgnoreFiles([]string{"missing.ignore", ".dockerignore"}),
)
files, err := fsFinder.Find()
require.NoError(t, err)
require.ElementsMatch(t, []string{"keep.json"}, fileNames(files))
}

func Test_fsFinderMultipleIgnoreFiles(t *testing.T) {
dir := t.TempDir()
testhelper.WriteFile(t, dir, "keep.json", testhelper.ValidContent["json"])
testhelper.WriteFile(t, dir, "drop.json", testhelper.ValidContent["json"])
testhelper.WriteFile(t, dir, "skip.yaml", testhelper.ValidContent["yaml"])
testhelper.WriteFile(t, dir, ".dockerignore", "drop.json\n")
testhelper.WriteFile(t, dir, ".prettierignore", "skip.yaml\n")

fsFinder := FileSystemFinderInit(
WithPathRoots(dir),
WithIgnoreFiles([]string{".dockerignore", ".prettierignore"}),
)
files, err := fsFinder.Find()
require.NoError(t, err)
require.ElementsMatch(t, []string{"keep.json"}, fileNames(files))
}

func Test_fsFinderSubdirectoryIgnoreFile(t *testing.T) {
dir := t.TempDir()
testhelper.WriteFile(t, dir, "foo.json", testhelper.ValidContent["json"])
sub := testhelper.CreateSubdir(t, dir, "subdir")
testhelper.WriteFile(t, sub, "foo.json", testhelper.ValidContent["json"])
testhelper.WriteFile(t, sub, ".dockerignore", "foo.json\n")

fsFinder := FileSystemFinderInit(
WithPathRoots(dir),
WithIgnoreFiles([]string{filepath.Join("subdir", ".dockerignore")}),
)
files, err := fsFinder.Find()
require.NoError(t, err)
require.ElementsMatch(t, []string{"foo.json"}, fileNames(files))
require.Equal(t, filepath.Join(dir, "foo.json"), files[0].Path)
}

// Test_fsFinderExtensionCacheNoPoison verifies that an unrecognized
// extensionless file (e.g. LICENSE) does not poison the extension
// cache and prevent subsequent extensionless known files from being found.
Expand Down
Loading
Loading