diff --git a/coverage.svg b/coverage.svg index e200c27d..ce48b808 100644 --- a/coverage.svg +++ b/coverage.svg @@ -1 +1 @@ -coveragecoverage33.8%33.8% +coveragecoverage34.4%34.4% diff --git a/pkg/linters/module/rules/license.go b/pkg/linters/module/rules/license.go index 091a698f..1f50367f 100644 --- a/pkg/linters/module/rules/license.go +++ b/pkg/linters/module/rules/license.go @@ -19,10 +19,8 @@ package rules import ( errs "errors" "io" - "os" "github.com/deckhouse/dmt/internal/fsutils" - "github.com/deckhouse/dmt/internal/logger" "github.com/deckhouse/dmt/internal/module" "github.com/deckhouse/dmt/pkg" "github.com/deckhouse/dmt/pkg/errors" @@ -53,7 +51,7 @@ type LicenseRule struct { func (r *LicenseRule) CheckFiles(mod *module.Module, errorList *errors.LintRuleErrorsList) { errorList = errorList.WithRule(r.GetName()) - files := fsutils.GetFiles(mod.GetPath(), false, filterFiles) + files := fsutils.GetFiles(mod.GetPath(), true, filterFiles) for _, fileName := range files { name := fsutils.Rel(mod.GetPath(), fileName) @@ -74,14 +72,6 @@ func (r *LicenseRule) CheckFiles(mod *module.Module, errorList *errors.LintRuleE } func filterFiles(rootPath, path string) bool { - f, err := os.Stat(path) - if err != nil { - logger.DebugF("Error getting file info: %v", err) - return false - } - if f.IsDir() { - return false - } path = fsutils.Rel(rootPath, path) if fileToCheckRe.MatchString(path) && !fileToSkipRe.MatchString(path) { return true diff --git a/pkg/linters/module/rules/license_library.go b/pkg/linters/module/rules/license_library.go index a9d2d104..e8e4b5fa 100644 --- a/pkg/linters/module/rules/license_library.go +++ b/pkg/linters/module/rules/license_library.go @@ -20,10 +20,12 @@ import ( "errors" "fmt" "os" + "path/filepath" "regexp" + "strings" ) -var CELicenseRe = regexp.MustCompile(`(?s)[/#{!-]*(\s)*Copyright 202[1-9] Flant JSC[-!}\n#/]* +var CELicenseRe = regexp.MustCompile(`(?s)[/#{!-]*(\s)*Copyright 202[1-9] Flant C?JSC[-!}\n#/]* [/#{!-]*(\s)*Licensed under the Apache License, Version 2\.0 \(the "License"\);[-!}\n]* [/#{!-]*(\s)*you may not use this file except in compliance with the License\.[-!}\n]* [/#{!-]*(\s)*You may obtain a copy of the License at[-!}\n#/]* @@ -34,6 +36,8 @@ var CELicenseRe = regexp.MustCompile(`(?s)[/#{!-]*(\s)*Copyright 202[1-9] Flant [/#{!-]*(\s)*See the License for the specific language governing permissions and[-!}\n]* [/#{!-]*(\s)*limitations under the License\.[-!}\n]*`) +var EELicenseRe = regexp.MustCompile(`(?s)[/#{!-]*(\s)*Copyright 202[1-9] Flant JSC[\t ]*\n([\t ]*\n)*[#{!-]*(\s)*Licensed under the Deckhouse Platform Enterprise Edition \(EE\) license\. See https://github\.com/deckhouse/deckhouse/blob/main/ee/LICENSE;[-!}\n]*`) + var fileToCheckRe = regexp.MustCompile( `\.go$|/[^.]+$|\.sh$|\.lua$|\.py$`, ) @@ -48,7 +52,32 @@ var flantRe = regexp.MustCompile(`Flant|Deckhouse`) const bufSize int = 1024 -// checkFileCopyright returns true if file is readable and has no copyright information in it. +// LicenseType represents the type of license expected for a file +type LicenseType int + +const ( + LicenseTypeCE LicenseType = iota + LicenseTypeEE +) + +// getLicenseType determines the expected license type based on the file path +// Files in directories starting with "ee" should have EE license, others should have CE license +func getLicenseType(filePath string) LicenseType { + // Split the path into components + pathComponents := strings.Split(filePath, string(filepath.Separator)) + + // Check if any directory in the path starts with "ee" + for _, component := range pathComponents { + if strings.EqualFold(component, "ee") { + return LicenseTypeEE + } + } + + return LicenseTypeCE +} + +// checkFileCopyright returns true if file is readable and has the correct copyright information. +// It now checks for the appropriate license type based on the file path. func checkFileCopyright(fName string) (bool, error) { // Original script 'validate_copyright.sh' used 'head -n 10'. // Here we just read first 1024 bytes. @@ -62,9 +91,29 @@ func checkFileCopyright(fName string) (bool, error) { return true, errors.New("generated code or other license") } - // Check Flant license if file contains keywords. - if flantRe.Match(headBuf) { - return true, nil + // Determine expected license type based on file path + licenseType := getLicenseType(fName) + + // Check for the appropriate license type + switch licenseType { + case LicenseTypeCE: + // Check for CE license (Apache 2.0) + if CELicenseRe.Match(headBuf) { + return true, nil + } + // Check if file contains Flant keywords but no proper license + if flantRe.Match(headBuf) { + return false, errors.New("file contains Flant references but missing proper CE license header") + } + case LicenseTypeEE: + // Check for EE license + if EELicenseRe.Match(headBuf) { + return true, nil + } + // Check if file contains Flant keywords but no proper license + if flantRe.Match(headBuf) { + return false, errors.New("file contains Flant references but missing proper EE license header") + } } // Skip file with some other copyright @@ -72,7 +121,8 @@ func checkFileCopyright(fName string) (bool, error) { return true, errors.New("contains other license") } - return false, errors.New("no copyright or license information") + return false, fmt.Errorf("no copyright or license information found (expected %s license)", + map[LicenseType]string{LicenseTypeCE: "CE", LicenseTypeEE: "EE"}[licenseType]) } func readFileHead(fName string, size int) ([]byte, error) { diff --git a/pkg/linters/module/rules/license_library_test.go b/pkg/linters/module/rules/license_library_test.go index ca9d3bda..cfd241bc 100644 --- a/pkg/linters/module/rules/license_library_test.go +++ b/pkg/linters/module/rules/license_library_test.go @@ -16,7 +16,185 @@ limitations under the License. package rules -import "testing" +import ( + "os" + "path/filepath" + "testing" +) + +func Test_getLicenseType(t *testing.T) { + tests := []struct { + name string + filePath string + expected LicenseType + }{ + { + name: "CE license for regular file", + filePath: "internal/module/module.go", + expected: LicenseTypeCE, + }, + { + name: "CE license for file in regular directory", + filePath: "pkg/linters/module/rules/license.go", + expected: LicenseTypeCE, + }, + { + name: "EE license for file in ee directory", + filePath: "ee/module/rules/license.go", + expected: LicenseTypeEE, + }, + { + name: "EE license for file in nested ee directory", + filePath: "internal/ee/module/rules/license.go", + expected: LicenseTypeEE, + }, + { + name: "EE license for file in EE directory (case insensitive)", + filePath: "EE/module/rules/license.go", + expected: LicenseTypeEE, + }, + { + name: "CE license for file in eetools directory", + filePath: "eetools/module/rules/license.go", + expected: LicenseTypeCE, + }, + { + name: "CE license for file with ee in middle of path", + filePath: "internal/feedback/module/rules/license.go", + expected: LicenseTypeCE, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getLicenseType(tt.filePath) + if result != tt.expected { + t.Errorf("getLicenseType() = %v, want %v", result, tt.expected) + } + }) + } +} + +func Test_ee_license_re(t *testing.T) { + invalidCases := []struct { + title string + content string + }{ + { + title: "No license", + content: `package main + +no license +`, + }, + { + title: "CE license instead of EE", + content: `/* +Copyright 2025 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 main +`, + }, + } + + for _, c := range invalidCases { + t.Run(c.title, func(t *testing.T) { + res := EELicenseRe.MatchString(c.content) + if res { + t.Errorf("should not detect EE license") + } + }) + } + + validCases := []struct { + title string + content string + }{ + { + title: "EE license in Go multiline comment", + content: `/* +Copyright 2025 Flant JSC +Licensed under the Deckhouse Platform Enterprise Edition (EE) license. See https://github.com/deckhouse/deckhouse/blob/main/ee/LICENSE; +*/ + +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Printf("Hello, world!") + os.Exit(0) +} +`, + }, + { + title: "EE license in Go single line comments", + content: `/* +Copyright 2025 Flant JSC +Licensed under the Deckhouse Platform Enterprise Edition (EE) license. See https://github.com/deckhouse/deckhouse/blob/main/ee/LICENSE; +*/ + +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Printf("Hello, world!") + os.Exit(0) +} +`, + }, + { + title: "EE license in Bash comments", + content: `#!/bin/bash +# Copyright 2025 Flant JSC +# Licensed under the Deckhouse Platform Enterprise Edition (EE) license. See https://github.com/deckhouse/deckhouse/blob/main/ee/LICENSE; + +set -Eeo pipefail +`, + }, + { + title: "EE license in Lua comments", + content: `--[[ +Copyright 2025 Flant JSC +Licensed under the Deckhouse Platform Enterprise Edition (EE) license. See https://github.com/deckhouse/deckhouse/blob/main/ee/LICENSE; +--]] + +local a = require "table.nkeys" + +print("Hello") +`, + }, + } + + for _, c := range validCases { + t.Run(c.title, func(t *testing.T) { + res := EELicenseRe.MatchString(c.content) + if !res { + t.Errorf("should detect EE license") + } + }) + } +} func Test_copyright_re(t *testing.T) { in := `package main @@ -39,7 +217,7 @@ no license content: ` #!/bin/bash -# Copyright 2021 Flant JSC +# Copyright 2025 Flant JSC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -61,7 +239,7 @@ set -Eeo pipefail { title: "Bash comment without previous spaces", content: `#!/bin/bash -# Copyright 2021 Flant JSC +# Copyright 2025 Flant JSC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -83,7 +261,7 @@ set -Eeo pipefail { title: "Golang multiline comment without previous spaces", content: `/* -Copyright 2021 Flant JSC +Copyright 2025 Flant JSC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -116,7 +294,7 @@ func main() { title: "Golang multiline comment with previous spaces", content: ` /* -Copyright 2021 Flant JSC +Copyright 2025 Flant JSC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -146,7 +324,7 @@ func main() { { title: "Golang multiple one line comments without previous spaces", - content: `// Copyright 2021 Flant JSC + content: `// Copyright 2025 Flant JSC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -177,7 +355,7 @@ func main() { { title: "Golang multiple one line comments with previous spaces", content: ` -// Copyright 2021 Flant JSC +// Copyright 2025 Flant JSC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -207,7 +385,7 @@ func main() { { title: "Lua multiple one line comments without previous spaces", content: `--[[ -Copyright 2021 Flant JSC +Copyright 2025 Flant JSC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -239,3 +417,149 @@ print("Hello") }) } } + +func Test_checkFileCopyright_Integration(t *testing.T) { + // Create temporary test files + tmpDir := t.TempDir() + + // Test CE license file + ceFile := filepath.Join(tmpDir, "ce_file.go") + ceContent := `/* +Copyright 2025 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 main + +func main() { + fmt.Println("Hello, CE!") +} +` + if err := os.WriteFile(ceFile, []byte(ceContent), 0600); err != nil { + t.Fatalf("Failed to create CE test file: %v", err) + } + + // Test EE license file + eeDir := filepath.Join(tmpDir, "ee") + if err := os.MkdirAll(eeDir, 0755); err != nil { + t.Fatalf("Failed to create EE directory: %v", err) + } + + eeFile := filepath.Join(eeDir, "ee_file.go") + eeContent := `/* +Copyright 2025 Flant JSC + +Licensed under the Deckhouse Platform Enterprise Edition (EE) license. See https://github.com/deckhouse/deckhouse/blob/main/ee/LICENSE; +*/ + +package main + +func main() { + fmt.Println("Hello, EE!") +} +` + if err := os.WriteFile(eeFile, []byte(eeContent), 0600); err != nil { + t.Fatalf("Failed to create EE test file: %v", err) + } + + // Test file without license + noLicenseFile := filepath.Join(tmpDir, "no_license.go") + noLicenseContent := `package main + +func main() { + fmt.Println("No license!") +} +` + if err := os.WriteFile(noLicenseFile, []byte(noLicenseContent), 0600); err != nil { + t.Fatalf("Failed to create no license test file: %v", err) + } + + // Test file with wrong license type + wrongLicenseFile := filepath.Join(eeDir, "wrong_license.go") + wrongLicenseContent := `/* +Copyright 2025 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 main + +func main() { + fmt.Println("Wrong license in EE directory!") +} +` + if err := os.WriteFile(wrongLicenseFile, []byte(wrongLicenseContent), 0600); err != nil { + t.Fatalf("Failed to create wrong license test file: %v", err) + } + + tests := []struct { + name string + filePath string + expectOK bool + expectError string + }{ + { + name: "CE file with correct license", + filePath: ceFile, + expectOK: true, + }, + { + name: "EE file with correct license", + filePath: eeFile, + expectOK: true, + }, + { + name: "File without license", + filePath: noLicenseFile, + expectOK: false, + expectError: "no copyright or license information found (expected CE license)", + }, + { + name: "EE file with wrong license type", + filePath: wrongLicenseFile, + expectOK: false, + expectError: "file contains Flant references but missing proper EE license header", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ok, err := checkFileCopyright(tt.filePath) + + if tt.expectOK && !ok { + t.Errorf("Expected file to be OK, but got error: %v", err) + } + + if !tt.expectOK && ok { + t.Errorf("Expected file to have error, but it was OK") + } + + if !tt.expectOK && err != nil && tt.expectError != "" { + if err.Error() != tt.expectError { + t.Errorf("Expected error '%s', but got '%s'", tt.expectError, err.Error()) + } + } + }) + } +}