github.com/editorconfig-checker/editorconfig-checker@v0.0.0-20231102090242-ddae3e68851e/pkg/files/files.go (about)

     1  // Package files contains functions and structs related to files
     2  package files
     3  
     4  import (
     5  	"bufio"
     6  	"bytes"
     7  	"fmt"
     8  	"io/fs"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/gabriel-vasile/mimetype"
    16  
    17  	"github.com/editorconfig/editorconfig-core-go/v2"
    18  
    19  	"github.com/editorconfig-checker/editorconfig-checker/pkg/config"
    20  	"github.com/editorconfig-checker/editorconfig-checker/pkg/utils"
    21  )
    22  
    23  const DefaultMimeType = "application/octet-stream"
    24  
    25  // FileInformation is a Struct which represents some FileInformation
    26  type FileInformation struct {
    27  	Line         string
    28  	Content      string
    29  	FilePath     string
    30  	LineNumber   int
    31  	Editorconfig *editorconfig.Definition
    32  }
    33  
    34  // IsExcluded returns whether the file is excluded via arguments or config file
    35  func IsExcluded(filePath string, config config.Config) (bool, error) {
    36  	if len(config.Exclude) == 0 && config.IgnoreDefaults {
    37  		return false, nil
    38  	}
    39  
    40  	relativeFilePath, err := GetRelativePath(filePath)
    41  	if err != nil {
    42  		return true, err
    43  	}
    44  
    45  	result, err := regexp.MatchString(config.GetExcludesAsRegularExpression(), relativeFilePath)
    46  	if err != nil {
    47  		return true, err
    48  	}
    49  
    50  	return result, nil
    51  }
    52  
    53  // AddToFiles adds a file to a slice if it isn't already in there
    54  // and meets the requirements and returns the new slice
    55  func AddToFiles(filePaths []string, filePath string, config config.Config) []string {
    56  	contentType, err := GetContentType(filePath, config)
    57  
    58  	config.Logger.Debug("AddToFiles: filePath: %s, contentType: %s", filePath, contentType)
    59  
    60  	if err != nil {
    61  		config.Logger.Error("Could not get the ContentType of file: %s", filePath)
    62  		config.Logger.Error(err.Error())
    63  	}
    64  
    65  	isExcluded, err := IsExcluded(filePath, config)
    66  
    67  	if err == nil && !isExcluded && IsAllowedContentType(contentType, config) {
    68  		config.Logger.Verbose("Add %s to be checked", filePath)
    69  		return append(filePaths, filePath)
    70  	}
    71  
    72  	config.Logger.Verbose("Don't add %s to be checked", filePath)
    73  	return filePaths
    74  }
    75  
    76  // GetFiles returns all files which should be checked
    77  func GetFiles(config config.Config) ([]string, error) {
    78  	var filePaths []string
    79  
    80  	// Handle explicit passed files
    81  	if len(config.PassedFiles) != 0 {
    82  		for _, passedFile := range config.PassedFiles {
    83  			if utils.IsDirectory(passedFile) {
    84  				_ = fs.WalkDir(os.DirFS(passedFile), ".", func(path string, de fs.DirEntry, err error) error {
    85  					if err != nil {
    86  						return err
    87  					}
    88  
    89  					fi, err := de.Info()
    90  					if err != nil {
    91  						return err
    92  					}
    93  
    94  					if fi.Mode().IsRegular() {
    95  						filePaths = AddToFiles(filePaths, path, config)
    96  					}
    97  
    98  					return nil
    99  				})
   100  			} else {
   101  				filePaths = AddToFiles(filePaths, passedFile, config)
   102  			}
   103  		}
   104  
   105  		return filePaths, nil
   106  	}
   107  
   108  	byteArray, err := exec.Command("git", "ls-files", "--cached", "--others", "--exclude-standard").Output()
   109  	if err != nil {
   110  		// It is not a git repository.
   111  		cwd, err := os.Getwd()
   112  		if err != nil {
   113  			return filePaths, err
   114  		}
   115  
   116  		_ = fs.WalkDir(os.DirFS(cwd), ".", func(path string, de fs.DirEntry, err error) error {
   117  			if err != nil {
   118  				return err
   119  			}
   120  
   121  			fi, err := de.Info()
   122  			if err != nil {
   123  				return err
   124  			}
   125  
   126  			if fi.Mode().IsRegular() {
   127  				filePaths = AddToFiles(filePaths, path, config)
   128  			}
   129  
   130  			return nil
   131  		})
   132  	}
   133  
   134  	filesSlice := strings.Split(string(byteArray[:]), "\n")
   135  
   136  	for _, filePath := range filesSlice {
   137  		if len(filePath) > 0 {
   138  			fi, err := os.Stat(filePath)
   139  
   140  			// The err would be a broken symlink for example,
   141  			// so we want to program to continue but the file should not be checked
   142  			if err == nil && fi.Mode().IsRegular() {
   143  				filePaths = AddToFiles(filePaths, filePath, config)
   144  			}
   145  		}
   146  	}
   147  
   148  	return filePaths, nil
   149  }
   150  
   151  // ReadLines returns the lines from a file as a slice
   152  func ReadLines(content string) []string {
   153  	var lines []string
   154  	stringReader := strings.NewReader(content)
   155  	fileScanner := bufio.NewScanner(stringReader)
   156  	for fileScanner.Scan() {
   157  		lines = append(lines, fileScanner.Text())
   158  	}
   159  
   160  	return lines
   161  }
   162  
   163  // GetContentType returns the content type of a file
   164  func GetContentType(path string, config config.Config) (string, error) {
   165  	fileStat, err := os.Stat(path)
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  
   170  	if fileStat.IsDir() {
   171  		return "", fmt.Errorf("%s is a directory", path)
   172  	}
   173  
   174  	if fileStat.Size() == 0 {
   175  		return "", nil
   176  	}
   177  
   178  	rawFileContent, err := os.ReadFile(path)
   179  	if err != nil {
   180  		panic(err)
   181  	}
   182  	return GetContentTypeBytes(rawFileContent, config)
   183  }
   184  
   185  // GetContentTypeBytes returns the content type of a byte slice
   186  func GetContentTypeBytes(rawFileContent []byte, config config.Config) (string, error) {
   187  	bytesReader := bytes.NewReader(rawFileContent)
   188  
   189  	mimeType, err := mimetype.DetectReader(bytesReader)
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  
   194  	mime := mimeType.String()
   195  	// Always returns a valid content-type and "application/octet-stream" if no others seemed to match.
   196  	parts := strings.Split(mime, ";")
   197  	if len(parts) > 0 {
   198  		mime = strings.TrimSpace(parts[0])
   199  	}
   200  	if mime == "" {
   201  		return DefaultMimeType, nil
   202  	}
   203  	return mime, nil
   204  }
   205  
   206  // PathExists checks whether a path of a file or directory exists or not
   207  func PathExists(filePath string) bool {
   208  	absolutePath, _ := filepath.Abs(filePath)
   209  	_, err := os.Stat(absolutePath)
   210  
   211  	return err == nil
   212  }
   213  
   214  // GetRelativePath returns the relative path of a file from the current working directory
   215  func GetRelativePath(filePath string) (string, error) {
   216  	filePath = filepath.FromSlash(filePath)
   217  	if !filepath.IsAbs(filePath) {
   218  		// Path is already relative. No changes needed
   219  		return filepath.ToSlash(filePath), nil
   220  	}
   221  
   222  	cwd, err := os.Getwd()
   223  	if err != nil {
   224  		return "", fmt.Errorf("Could not get the current working directory")
   225  	}
   226  
   227  	cwd = filepath.FromSlash(cwd)
   228  	rel, err := filepath.Rel(cwd, filePath)
   229  	return filepath.ToSlash(rel), err
   230  }
   231  
   232  // IsAllowedContentType returns whether the contentType is
   233  // an allowed content type to check or not
   234  func IsAllowedContentType(contentType string, config config.Config) bool {
   235  	result := false
   236  
   237  	for _, allowedContentType := range config.AllowedContentTypes {
   238  		result = result || strings.Contains(contentType, allowedContentType)
   239  	}
   240  
   241  	return result
   242  }