-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New experimental check: Avoid shadowing (#149)
* New experimental check: Avoid shadowing From https://github.saobby.my.eu.orgmunity/t/order-of-owners-in-codeowners-file/143073/2 This is something that has bitten us. We initially put a "*" at the end of the CODEOWNERS files to catch unassigned files However, this entry should've been put at the beginning, only the last matching pattern is considered
- Loading branch information
1 parent
7dfc6dc
commit 0e995bc
Showing
8 changed files
with
221 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package check | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/mszostok/codeowners-validator/internal/ctxutil" | ||
"github.com/mszostok/codeowners-validator/pkg/codeowners" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
type AvoidShadowing struct{} | ||
|
||
func NewAvoidShadowing() *AvoidShadowing { | ||
return &AvoidShadowing{} | ||
} | ||
|
||
func (c *AvoidShadowing) Check(ctx context.Context, in Input) (output Output, err error) { | ||
var bldr OutputBuilder | ||
|
||
previousEntries := []codeowners.Entry{} | ||
for _, entry := range in.CodeownersEntries { | ||
if ctxutil.ShouldExit(ctx) { | ||
return Output{}, ctx.Err() | ||
} | ||
re, err := wildCardToRegexp(endWithSlash(entry.Pattern)) | ||
if err != nil { | ||
return Output{}, errors.Wrapf(err, "while compiling pattern %s into a regexp", entry.Pattern) | ||
} | ||
shadowed := []codeowners.Entry{} | ||
for _, previous := range previousEntries { | ||
if re.MatchString(endWithSlash(previous.Pattern)) { | ||
shadowed = append(shadowed, previous) | ||
} | ||
} | ||
if len(shadowed) > 0 { | ||
msg := fmt.Sprintf("Pattern %q shadows the following patterns:\n%s\nEntries should go from least-specific to most-specific.", entry.Pattern, c.listFormatFunc(shadowed)) | ||
bldr.ReportIssue(msg, WithEntry(entry)) | ||
} | ||
previousEntries = append(previousEntries, entry) | ||
} | ||
|
||
return bldr.Output(), nil | ||
} | ||
|
||
// listFormatFunc is a basic formatter that outputs a bullet point list of the pattern. | ||
func (c *AvoidShadowing) listFormatFunc(es []codeowners.Entry) string { | ||
points := make([]string, len(es)) | ||
for i, err := range es { | ||
points[i] = fmt.Sprintf(" * %d: %q", err.LineNo, err.Pattern) | ||
} | ||
|
||
return strings.Join(points, "\n") | ||
} | ||
|
||
// Name returns human readable name of the validator | ||
func (AvoidShadowing) Name() string { | ||
return "[Experimental] Avoid Shadowing Checker" | ||
} | ||
|
||
// endWithSlash adds a trailing slash to a string if it doesn't already end with one. | ||
// This is useful when matching CODEOWNERS pattern because the trailing slash is optional. | ||
func endWithSlash(s string) string { | ||
if !strings.HasSuffix(s, "/") { | ||
return s + "/" | ||
} | ||
return s | ||
} | ||
|
||
// wildCardToRegexp converts a wildcard pattern to a regular expression pattern. | ||
func wildCardToRegexp(pattern string) (*regexp.Regexp, error) { | ||
var result strings.Builder | ||
for i, literal := range strings.Split(pattern, "*") { | ||
// Replace * with .* | ||
if i > 0 { | ||
result.WriteString(".*") | ||
} | ||
|
||
// Quote any regular expression meta characters in the | ||
// literal text. | ||
result.WriteString(regexp.QuoteMeta(literal)) | ||
} | ||
return regexp.Compile("^" + result.String() + "$") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package check_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/mszostok/codeowners-validator/internal/check" | ||
"github.com/mszostok/codeowners-validator/internal/ptr" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestAvoidShadowing(t *testing.T) { | ||
tests := map[string]struct { | ||
codeownersInput string | ||
expectedIssues []check.Issue | ||
}{ | ||
"Should report info about shadowed entries": { | ||
codeownersInput: ` | ||
/build/logs/ @doctocat | ||
/script @mszostok | ||
# Shadows | ||
* @s1 | ||
/s*/ @s2 | ||
/s* @s3 | ||
/b* @s4 | ||
/b*/logs @s5 | ||
# OK | ||
/b*/other @o1 | ||
/script/* @o2 | ||
`, | ||
expectedIssues: []check.Issue{ | ||
{ | ||
Severity: check.Error, | ||
LineNo: ptr.Uint64Ptr(6), | ||
Message: `Pattern "*" shadows the following patterns: | ||
* 2: "/build/logs/" | ||
* 3: "/script" | ||
Entries should go from least-specific to most-specific.`, | ||
}, | ||
{ | ||
Severity: check.Error, | ||
LineNo: ptr.Uint64Ptr(7), | ||
Message: `Pattern "/s*/" shadows the following patterns: | ||
* 3: "/script" | ||
Entries should go from least-specific to most-specific.`, | ||
}, | ||
{ | ||
Severity: check.Error, | ||
LineNo: ptr.Uint64Ptr(8), | ||
Message: `Pattern "/s*" shadows the following patterns: | ||
* 3: "/script" | ||
* 7: "/s*/" | ||
Entries should go from least-specific to most-specific.`, | ||
}, | ||
{ | ||
Severity: check.Error, | ||
LineNo: ptr.Uint64Ptr(9), | ||
Message: `Pattern "/b*" shadows the following patterns: | ||
* 2: "/build/logs/" | ||
Entries should go from least-specific to most-specific.`, | ||
}, | ||
{ | ||
Severity: check.Error, | ||
LineNo: ptr.Uint64Ptr(10), | ||
Message: `Pattern "/b*/logs" shadows the following patterns: | ||
* 2: "/build/logs/" | ||
Entries should go from least-specific to most-specific.`, | ||
}, | ||
}, | ||
}, | ||
"Should not report any issues with correct CODEOWNERS file": { | ||
codeownersInput: FixtureValidCODEOWNERS, | ||
expectedIssues: nil, | ||
}, | ||
} | ||
|
||
for tn, tc := range tests { | ||
t.Run(tn, func(t *testing.T) { | ||
// given | ||
sut := check.NewAvoidShadowing() | ||
|
||
// when | ||
out, err := sut.Check(context.TODO(), LoadInput(tc.codeownersInput)) | ||
|
||
// then | ||
require.NoError(t, err) | ||
assert.ElementsMatch(t, tc.expectedIssues, out.Issues) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
tests/integration/testdata/TestCheckFailures/avoid-shadowing.golden.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
==> Executing [Experimental] Avoid Shadowing Checker (<duration>) | ||
[err] line 11: Pattern "/some/awesome/dir" shadows the following patterns: | ||
* 10: "/some/awesome/dir" | ||
Entries should go from least-specific to most-specific. | ||
|
||
1 check(s) executed, 1 failure(s) |
4 changes: 4 additions & 0 deletions
4
...tion/testdata/TestCheckSuccess/offline_checks/GitHubCODEOWNERS/avoid-shadowing.golden.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
==> Executing [Experimental] Avoid Shadowing Checker (<duration>) | ||
Check OK | ||
|
||
1 check(s) executed, no failure(s) |
4 changes: 4 additions & 0 deletions
4
...gration/testdata/TestCheckSuccess/offline_checks/gh-codeowners/avoid-shadowing.golden.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
==> Executing [Experimental] Avoid Shadowing Checker (<duration>) | ||
Check OK | ||
|
||
1 check(s) executed, no failure(s) |