internal/platform/commoncontext/configurator.go (191 lines of code) (raw):
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* 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
*
* https://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 commoncontext
import (
"bytes"
"io"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"github.com/JetBrains/qodana-cli/internal/platform/strutil"
"github.com/go-enry/go-enry/v2"
log "github.com/sirupsen/logrus"
)
const (
QodanaSarifName = "qodana.sarif.json"
)
// ignoredDirectories is a list of directories that should be ignored by the configurator.
var ignoredDirectories = []string{
".idea",
".vscode",
".git",
}
// isInIgnoredDirectory returns true if the given path should be ignored by the configurator.
func isInIgnoredDirectory(path string) bool {
parts := strings.Split(path, string(os.PathSeparator))
for _, part := range parts {
for _, ignored := range ignoredDirectories {
if part == ignored {
return true
}
}
}
return false
}
// recognizeDirLanguages returns the languages detected in the given directory.
func recognizeDirLanguages(projectPath string) ([]string, error) {
const limitKb = 64
out := make(map[string]int)
err := filepath.Walk(
projectPath, func(path string, f os.FileInfo, err error) error {
if err != nil {
return filepath.SkipDir
}
if f.Mode().IsDir() && !f.Mode().IsRegular() {
return nil
}
relpath, err := filepath.Rel(projectPath, path)
relpath = filepath.ToSlash(relpath) // enry always uses forward slashes for regex matching
if err != nil {
return nil
}
if relpath == "." {
return nil
}
if f.IsDir() {
relpath += "/"
}
if isInIgnoredDirectory(path) || enry.IsVendor(relpath) || enry.IsDotFile(relpath) ||
enry.IsDocumentation(relpath) || enry.IsConfiguration(relpath) ||
enry.IsGenerated(relpath, nil) {
if f.IsDir() {
return filepath.SkipDir
}
return nil
}
if f.IsDir() {
return nil
}
content, err := readFile(path, limitKb)
if err != nil {
return nil
}
if enry.IsGenerated(relpath, content) {
return nil
}
language := enry.GetLanguage(filepath.Base(path), content)
if language == enry.OtherLanguage {
return nil
}
if enry.GetLanguageType(language) != enry.Programming {
return nil
}
out[language] += 1
return nil
},
)
if err != nil {
return nil, err
}
type languageCount struct {
Language string
Count int
}
langCounts := make([]languageCount, 0, len(out))
for language, count := range out {
langCounts = append(langCounts, languageCount{Language: language, Count: count})
}
sort.Slice(
langCounts, func(i, j int) bool {
return langCounts[i].Count > langCounts[j].Count
},
)
languages := make([]string, 0, len(langCounts))
for _, langCount := range langCounts {
languages = append(languages, langCount.Language)
}
slices.Sort(languages)
return languages, nil
}
// readFile reads the file at the given path and returns its content.
func readFile(path string, limit int64) ([]byte, error) {
if limit <= 0 {
return os.ReadFile(path)
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() {
err := f.Close()
if err != nil {
log.Print(err)
}
}()
st, err := f.Stat()
if err != nil {
return nil, err
}
size := st.Size()
if size > limit {
size = limit
}
buf := bytes.NewBuffer(nil)
buf.Grow(int(size))
_, err = io.Copy(buf, io.LimitReader(f, limit))
return buf.Bytes(), err
}
// readIdeaDir reads .idea directory and tries to detect which languages are used in the project.
func readIdeaDir(project string) []string {
var languages []string
var files []string
root := filepath.Join(project, ".idea")
if _, err := os.Stat(root); os.IsNotExist(err) {
return languages
}
err := filepath.Walk(
root, func(path string, info os.FileInfo, err error) error {
files = append(files, path)
return nil
},
)
if err != nil {
panic(err)
}
for _, file := range files {
if filepath.Ext(file) == ".iml" {
iml, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
}
text := string(iml)
if strings.Contains(text, "JAVA_MODULE") {
languages = strutil.Append(languages, "Java")
}
if strings.Contains(text, "PYTHON_MODULE") {
languages = strutil.Append(languages, "Python")
}
if strings.Contains(text, "Go") {
languages = strutil.Append(languages, "Go")
}
}
}
return languages
}
// isAndroidProject checks if the given directory is an Android project by checking AndroidManifest
// https://developer.android.com/guide/topics/manifest/manifest-intro
func isAndroidProject(projectDir string) bool {
var foundManifest bool
err := filepath.Walk(
projectDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(info.Name(), "AndroidManifest.xml") {
foundManifest = true
return filepath.SkipDir
}
return nil
},
)
if err != nil {
log.Fatal("Error walking the path: ", err)
}
return foundManifest
}