Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Add globs (**/*) feature to excludes #16

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions internal/fsutils/fsutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"sync"
)

Expand Down Expand Up @@ -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(`(\.)|(\*\*/)|(\*)|([^/\*.]+)|(/)`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing, why do we use groups here? As far as I can see, we just match whether this regexp fits our string or not.


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
juev marked this conversation as resolved.
Show resolved Hide resolved
}

func StringMatchAnyMask(str string, patterns []string) bool {
for _, p := range patterns {
if StringMatchMask(str, p) {
return true
}
}

return false
}
198 changes: 198 additions & 0 deletions internal/fsutils/fsutils_test.go
Original file line number Diff line number Diff line change
@@ -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",
juev marked this conversation as resolved.
Show resolved Hide resolved
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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, then the pattern will fit any module without restrictions?
Do we want to add regexp only for parsing the file path? Or for the whole line?
If only for the path, then it is reasonable, if for the string, then in my opinion, it will introduce unnecessary errors and unprovable behavior.

},
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)
}
})
}
}
21 changes: 18 additions & 3 deletions pkg/linters/license/license.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package license

import (
"slices"
"strings"

"github.com/deckhouse/dmt/internal/fsutils"
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we create a name including the module name so that it can be compared with the list of exceptions. The proposed change below does not do this. As a result, we will compare files with exceptions from other modules.

if slices.Contains(o.cfg.CopyrightExcludes, name) {

if fsutils.StringMatchAnyMask(name, moduleExcludes) {
continue
}

Expand Down Expand Up @@ -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
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/linters/license/license_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/linters/no-cyrillic/no-cyrillic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name will look like this: upmeter:/hooks/.venv/dir/file.py

We compare it with an exception like upmeter:/hooks/. venv/**/*

It looks like it should work. It would not be bad to make a test for this solution.

continue
}
if o.skipDocRe.MatchString(fileName) {
Expand Down