mirror of
https://github.com/mgechev/revive.git
synced 2025-11-23 22:04:49 +02:00
195 lines
4.8 KiB
Go
195 lines
4.8 KiB
Go
package rule
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/mgechev/revive/lint"
|
|
)
|
|
|
|
// PackageDirectoryMismatchRule detects when package name doesn't match directory name.
|
|
type PackageDirectoryMismatchRule struct {
|
|
ignoredDirs *regexp.Regexp
|
|
}
|
|
|
|
const defaultIgnoredDirs = "testdata"
|
|
|
|
// Configure the rule to exclude certain directories.
|
|
func (r *PackageDirectoryMismatchRule) Configure(arguments lint.Arguments) error {
|
|
if len(arguments) < 1 {
|
|
var err error
|
|
r.ignoredDirs, err = r.buildIgnoreRegex([]string{defaultIgnoredDirs})
|
|
return err
|
|
}
|
|
|
|
args, ok := arguments[0].(map[string]any)
|
|
if !ok {
|
|
return fmt.Errorf("invalid argument type: expected map[string]any, got %T", arguments[0])
|
|
}
|
|
|
|
for k, v := range args {
|
|
if !isRuleOption(k, "ignoreDirectories") {
|
|
return fmt.Errorf("unknown argument %s for %s rule", k, r.Name())
|
|
}
|
|
|
|
ignoredAny, ok := v.([]any)
|
|
if !ok {
|
|
return fmt.Errorf("invalid value %v for argument %s of rule %s, expected []string got %T", v, k, r.Name(), v)
|
|
}
|
|
|
|
ignoredDirs := make([]string, len(ignoredAny))
|
|
for i, item := range ignoredAny {
|
|
str, ok := item.(string)
|
|
if !ok {
|
|
return fmt.Errorf("invalid value in %s argument of rule %s: expected string, got %T", k, r.Name(), item)
|
|
}
|
|
ignoredDirs[i] = str
|
|
}
|
|
|
|
var err error
|
|
r.ignoredDirs, err = r.buildIgnoreRegex(ignoredDirs)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (*PackageDirectoryMismatchRule) buildIgnoreRegex(ignoredDirs []string) (*regexp.Regexp, error) {
|
|
if len(ignoredDirs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
patterns := make([]string, len(ignoredDirs))
|
|
for i, dir := range ignoredDirs {
|
|
patterns[i] = regexp.QuoteMeta(dir)
|
|
}
|
|
pattern := strings.Join(patterns, "|")
|
|
|
|
regex, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compile regex for ignored directories: %w", err)
|
|
}
|
|
|
|
return regex, nil
|
|
}
|
|
|
|
// skipDirs contains directory names that should be unconditionally ignored when checking.
|
|
// These entries handle edge cases where filepath.Base might return these values.
|
|
var skipDirs = map[string]struct{}{
|
|
".": {}, // Current directory
|
|
"/": {}, // Root directory
|
|
"": {}, // Empty path
|
|
}
|
|
|
|
// semanticallyEqual checks if package and directory names are semantically equal to each other.
|
|
func (PackageDirectoryMismatchRule) semanticallyEqual(packageName, dirName string) bool {
|
|
normDir := normalizePath(dirName)
|
|
normPkg := normalizePath(packageName)
|
|
return normDir == normPkg || normDir == "go"+normPkg
|
|
}
|
|
|
|
// Apply applies the rule to the given file.
|
|
func (r *PackageDirectoryMismatchRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
|
if file.Pkg.IsMain() {
|
|
return nil
|
|
}
|
|
|
|
absPath, err := filepath.Abs(file.Name)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
dirPath := filepath.Dir(absPath)
|
|
dirName := filepath.Base(dirPath)
|
|
|
|
if r.ignoredDirs != nil && r.ignoredDirs.MatchString(dirPath) {
|
|
return nil
|
|
}
|
|
|
|
// Check if we got an invalid directory.
|
|
if _, skipDir := skipDirs[dirName]; skipDir {
|
|
return nil
|
|
}
|
|
|
|
// Files directly in 'internal/' (like 'internal/abcd.go') should not be checked.
|
|
// But files in subdirectories of 'internal/' (like 'internal/foo/abcd.go') should be checked.
|
|
if dirName == "internal" {
|
|
return nil
|
|
}
|
|
|
|
packageName := file.AST.Name.Name
|
|
|
|
if r.semanticallyEqual(packageName, dirName) {
|
|
return nil
|
|
}
|
|
|
|
if isRootDir(dirPath) {
|
|
return nil
|
|
}
|
|
|
|
if file.IsTest() {
|
|
// treat main_test differently because it's a common package name for tests
|
|
if packageName == "main_test" {
|
|
return nil
|
|
}
|
|
// External test package (directory + '_test' suffix)
|
|
if r.semanticallyEqual(packageName, dirName+"_test") {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// define a default failure message
|
|
failure := fmt.Sprintf("package name %q does not match directory name %q", packageName, dirName)
|
|
|
|
// For version directories (v1, v2, etc.), we need to check also the parent directory
|
|
if isVersionPath(dirName) {
|
|
parentDirName := filepath.Base(filepath.Dir(dirPath))
|
|
if r.semanticallyEqual(packageName, parentDirName) {
|
|
return nil
|
|
}
|
|
|
|
if file.IsTest() {
|
|
// External test package (directory + '_test' suffix)
|
|
if r.semanticallyEqual(packageName, parentDirName+"_test") {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
failure = fmt.Sprintf("package name %q does not match directory name %q or parent directory name %q", packageName, dirName, parentDirName)
|
|
}
|
|
|
|
return []lint.Failure{
|
|
{
|
|
Failure: failure,
|
|
Confidence: 1,
|
|
Node: file.AST.Name,
|
|
Category: lint.FailureCategoryNaming,
|
|
},
|
|
}
|
|
}
|
|
|
|
// isRootDir checks if the given directory contains go.mod or .git, indicating it's a root directory.
|
|
func isRootDir(dirPath string) bool {
|
|
entries, err := os.ReadDir(dirPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
for _, e := range entries {
|
|
switch e.Name() {
|
|
case "go.mod", ".git":
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Name returns the rule name.
|
|
func (*PackageDirectoryMismatchRule) Name() string {
|
|
return "package-directory-mismatch"
|
|
}
|