diff --git a/README.md b/README.md index 2a20a1d..0137666 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Generate some automatic rules for you module ## Configuration -You can exclude linters or setup them via the config file `.dmt-lint` +You can exclude linters or setup them via the config file `.dmtlint.yaml`. This config file can be either in the module directory or in any of the top-level directories up to /. Example settings: @@ -51,9 +51,11 @@ linters-settings: no-cyrillic-file-excludes: - user-authz:/rbac.yaml - documentation:/images/web/site/_data/topnav.yml + - other-module:/external/**/*.txt license: copyright-excludes: - upmeter:/images/upmeter/stress.sh + - upmeter:/hooks/.venv/**/* - cni-simple-bridge:/images/simple-bridge/rootfs/bin/simple-bridge skip-oss-checks: - 001-priority-class diff --git a/internal/fsutils/fsutils.go b/internal/fsutils/fsutils.go index 7e35167..0da9e27 100644 --- a/internal/fsutils/fsutils.go +++ b/internal/fsutils/fsutils.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sync" ) @@ -91,3 +92,42 @@ func EvalSymlinks(path string) (string, error) { return er.path, er.err } + +func toRegexp(pattern string) string { + // code from https://github.com/guillermo/doubleglob/blob/main/double_glob.go + replaces := regexp.MustCompile(`(\.)|(\*\*/)|(\*)|([^/\*.]+)|(/)`) + + pat := replaces.ReplaceAllStringFunc(pattern, func(s string) string { + switch s { + case "/": + return "\\/" + case ".": + return "\\." + case "**/": + return ".*\\/" + case "*": + return "[^/]*" + default: + return s + } + }) + return "^" + pat + "$" +} + +func StringMatchMask(str, pattern string) bool { + regexpPat := regexp.MustCompile(toRegexp(pattern)) + + matched := regexpPat.MatchString(str) + + return matched +} + +func StringMatchAnyMask(str string, patterns []string) bool { + for _, p := range patterns { + if StringMatchMask(str, p) { + return true + } + } + + return false +} diff --git a/internal/fsutils/fsutils_test.go b/internal/fsutils/fsutils_test.go new file mode 100644 index 0000000..466e29b --- /dev/null +++ b/internal/fsutils/fsutils_test.go @@ -0,0 +1,198 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fsutils + +import ( + "testing" +) + +func Test_FSUtils_toRegexp(t *testing.T) { + type args struct { + pattern string + } + + tests := []struct { + name string + args args + want string + }{ + { + name: "test escape slashes", + args: args{ + pattern: "/home/user/test/project", + }, + want: "^\\/home\\/user\\/test\\/project$", + }, + { + name: "test escape slashes and dots", + args: args{ + pattern: "/home/user/test/project.txt", + }, + want: "^\\/home\\/user\\/test\\/project\\.txt$", + }, + { + name: "test glob mask **", + args: args{ + pattern: "/home/user/**/project.txt", + }, + want: "^\\/home\\/user\\/.*\\/project\\.txt$", + }, + { + name: "test glob mask ** and file mask", + args: args{ + pattern: "/home/user/**/*.txt", + }, + want: "^\\/home\\/user\\/.*\\/[^/]*\\.txt$", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toRegexp(tt.args.pattern); got != tt.want { + t.Errorf("toRegexp() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFSUtils_FileNames_StringMatchMask(t *testing.T) { + type args struct { + name string + pattern string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "without any mask (exact filenames)", + args: args{ + name: "/home/user/test/project.txt", + pattern: "/home/user/test/project.txt", + }, + want: true, + }, + { + name: "with filename mask and txt extension", + args: args{ + name: "/home/user/test/project.txt", + pattern: "/home/user/test/*.txt", + }, + want: true, + }, + { + name: "with filename mask and log extension", + args: args{ + name: "/home/user/test/project.log", + pattern: "/home/user/test/*.txt", + }, + want: false, + }, + { + name: "with glob mask and exact filename", + args: args{ + name: "/home/user/test/project.txt", + pattern: "/home/user/**/project.txt", + }, + want: true, + }, + { + name: "with glob mask and exact filename which not match with pattern", + args: args{ + name: "/home/user/test/newproject.txt", + pattern: "/home/user/**/project.txt", + }, + want: false, + }, + { + name: "with glob mask and any extension with exact filename", + args: args{ + name: "/home/user/test/project.txt", + pattern: "/home/user/**/project.*", + }, + want: true, + }, + { + name: "with mask without subdirectories", + args: args{ + name: "/home/user/test/project.txt", + pattern: "/home/user/*.txt", + }, + want: false, + }, + { + name: "with glob mask and any file inside directories", + args: args{ + name: "/home/user/test/something/other/project.txt", + pattern: "/home/user/**/*", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := StringMatchMask(tt.args.name, tt.args.pattern); got != tt.want { + t.Errorf("StringMatchMask() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFSUtils_PlainStrings_StringMatchMask(t *testing.T) { + type args struct { + str string + pattern string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "test module-name:filename with exact name", + args: args{ + str: "managed-pg:/enabled", + pattern: "managed-pg:/enabled", + }, + want: true, + }, + { + name: "test module-name:filename with glob mask", + args: args{ + str: "managed-pg:/.venv/lib/python3.13/site-packages/deckhouse/__init__.py", + pattern: "managed-pg:/.venv/**/*", + }, + want: true, + }, + { + name: "plain string with mask", + args: args{ + str: "test-string.delimited.by.dots", + pattern: "*.by.dots", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := StringMatchMask(tt.args.str, tt.args.pattern); got != tt.want { + t.Errorf("StringMatchMask() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/linters/license/license.go b/pkg/linters/license/license.go index 6cb8559..d4d3f00 100644 --- a/pkg/linters/license/license.go +++ b/pkg/linters/license/license.go @@ -1,7 +1,6 @@ package license import ( - "slices" "strings" "github.com/deckhouse/dmt/internal/fsutils" @@ -40,10 +39,12 @@ func (o *Copyright) Run(m *module.Module) (errors.LintRuleErrorsList, error) { result.Merge(OssModuleRule(m.GetName(), m.GetPath())) + moduleExcludes := getExcludes(o.cfg.CopyrightExcludes, m.GetName()) + for _, fileName := range files { name, _ := strings.CutPrefix(fileName, m.GetPath()) - name = m.GetName() + ":" + name - if slices.Contains(o.cfg.CopyrightExcludes, name) { + + if fsutils.StringMatchAnyMask(name, moduleExcludes) { continue } @@ -80,6 +81,20 @@ func getFiles(rootPath string) ([]string, error) { return result, nil } +func getExcludes(excludesList []string, moduleName string) []string { + var result = make([]string, 0) + + // prepare excludes by removing module name prefix + for _, exclude := range excludesList { + pattern, ok := strings.CutPrefix(exclude, moduleName+":") + if ok { + result = append(result, pattern) + } + } + + return result +} + func (o *Copyright) Name() string { return o.name } diff --git a/pkg/linters/license/license_test.go b/pkg/linters/license/license_test.go new file mode 100644 index 0000000..7729663 --- /dev/null +++ b/pkg/linters/license/license_test.go @@ -0,0 +1,34 @@ +package license + +import ( + "reflect" + "testing" +) + +func Test_getExcludes(t *testing.T) { + type args struct { + excludesList []string + moduleName string + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "exclude .venv folder for managed-pg module", + args: args{ + excludesList: []string{"managed-pg:/.venv/**/*", "somemodule:/.venv/**/*", "another-mod:/*.txt", "managed-pg:/enabled"}, + moduleName: "managed-pg", + }, + want: []string{"/.venv/**/*", "/enabled"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getExcludes(tt.args.excludesList, tt.args.moduleName); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getExcludes() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/linters/no-cyrillic/no-cyrillic.go b/pkg/linters/no-cyrillic/no-cyrillic.go index ff57763..7120fd7 100644 --- a/pkg/linters/no-cyrillic/no-cyrillic.go +++ b/pkg/linters/no-cyrillic/no-cyrillic.go @@ -66,7 +66,7 @@ func (o *NoCyrillic) Run(m *module.Module) (errors.LintRuleErrorsList, error) { for _, fileName := range files { name, _ := strings.CutPrefix(fileName, m.GetPath()) name = m.GetName() + ":" + name - if slices.Contains(o.cfg.NoCyrillicFileExcludes, name) { + if fsutils.StringMatchAnyMask(name, o.cfg.NoCyrillicFileExcludes) { continue } if o.skipDocRe.MatchString(fileName) {