github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/lint/lintersdb/custom_linters.go (about)

     1  package lintersdb
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"plugin"
     9  
    10  	"github.com/spf13/viper"
    11  	"golang.org/x/tools/go/analysis"
    12  
    13  	"github.com/vanstinator/golangci-lint/pkg/config"
    14  	"github.com/vanstinator/golangci-lint/pkg/golinters/goanalysis"
    15  	"github.com/vanstinator/golangci-lint/pkg/lint/linter"
    16  )
    17  
    18  type AnalyzerPlugin interface {
    19  	GetAnalyzers() []*analysis.Analyzer
    20  }
    21  
    22  // getCustomLinterConfigs loads private linters that are specified in the golangci config file.
    23  func (m *Manager) getCustomLinterConfigs() []*linter.Config {
    24  	if m.cfg == nil || m.log == nil {
    25  		return nil
    26  	}
    27  
    28  	var linters []*linter.Config
    29  
    30  	for name, settings := range m.cfg.LintersSettings.Custom {
    31  		lc, err := m.loadCustomLinterConfig(name, settings)
    32  		if err != nil {
    33  			m.log.Errorf("Unable to load custom analyzer %s:%s, %v", name, settings.Path, err)
    34  		} else {
    35  			linters = append(linters, lc)
    36  		}
    37  	}
    38  
    39  	return linters
    40  }
    41  
    42  // loadCustomLinterConfig loads the configuration of private linters.
    43  // Private linters are dynamically loaded from .so plugin files.
    44  func (m *Manager) loadCustomLinterConfig(name string, settings config.CustomLinterSettings) (*linter.Config, error) {
    45  	analyzers, err := m.getAnalyzerPlugin(settings.Path, settings.Settings)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  
    50  	m.log.Infof("Loaded %s: %s", settings.Path, name)
    51  
    52  	customLinter := goanalysis.NewLinter(name, settings.Description, analyzers, nil).
    53  		WithLoadMode(goanalysis.LoadModeTypesInfo)
    54  
    55  	linterConfig := linter.NewConfig(customLinter).
    56  		WithEnabledByDefault().
    57  		WithLoadForGoAnalysis().
    58  		WithURL(settings.OriginalURL)
    59  
    60  	return linterConfig, nil
    61  }
    62  
    63  // getAnalyzerPlugin loads a private linter as specified in the config file,
    64  // loads the plugin from a .so file,
    65  // and returns the 'AnalyzerPlugin' interface implemented by the private plugin.
    66  // An error is returned if the private linter cannot be loaded
    67  // or the linter does not implement the AnalyzerPlugin interface.
    68  func (m *Manager) getAnalyzerPlugin(path string, settings any) ([]*analysis.Analyzer, error) {
    69  	if !filepath.IsAbs(path) {
    70  		// resolve non-absolute paths relative to config file's directory
    71  		configFilePath := viper.ConfigFileUsed()
    72  		absConfigFilePath, err := filepath.Abs(configFilePath)
    73  		if err != nil {
    74  			return nil, fmt.Errorf("could not get absolute representation of config file path %q: %w", configFilePath, err)
    75  		}
    76  		path = filepath.Join(filepath.Dir(absConfigFilePath), path)
    77  	}
    78  
    79  	plug, err := plugin.Open(path)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	analyzers, err := m.lookupPlugin(plug, settings)
    85  	if err != nil {
    86  		return nil, fmt.Errorf("lookup plugin %s: %w", path, err)
    87  	}
    88  
    89  	return analyzers, nil
    90  }
    91  
    92  func (m *Manager) lookupPlugin(plug *plugin.Plugin, settings any) ([]*analysis.Analyzer, error) {
    93  	symbol, err := plug.Lookup("New")
    94  	if err != nil {
    95  		analyzers, errP := m.lookupAnalyzerPlugin(plug)
    96  		if errP != nil {
    97  			return nil, errors.Join(err, errP)
    98  		}
    99  
   100  		return analyzers, nil
   101  	}
   102  
   103  	// The type func cannot be used here, must be the explicit signature.
   104  	constructor, ok := symbol.(func(any) ([]*analysis.Analyzer, error))
   105  	if !ok {
   106  		return nil, fmt.Errorf("plugin does not abide by 'New' function: %T", symbol)
   107  	}
   108  
   109  	return constructor(settings)
   110  }
   111  
   112  func (m *Manager) lookupAnalyzerPlugin(plug *plugin.Plugin) ([]*analysis.Analyzer, error) {
   113  	symbol, err := plug.Lookup("AnalyzerPlugin")
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	// TODO(ldez): remove this env var (but keep the log) in the next minor version (v1.55.0)
   119  	if _, ok := os.LookupEnv("GOLANGCI_LINT_HIDE_WARNING_ABOUT_PLUGIN_API_DEPRECATION"); !ok {
   120  		m.log.Warnf("plugin: 'AnalyzerPlugin' plugins are deprecated, please use the new plugin signature: " +
   121  			"https://golangci-lint.run/contributing/new-linters/#create-a-plugin")
   122  	}
   123  
   124  	analyzerPlugin, ok := symbol.(AnalyzerPlugin)
   125  	if !ok {
   126  		return nil, fmt.Errorf("plugin does not abide by 'AnalyzerPlugin' interface: %T", symbol)
   127  	}
   128  
   129  	return analyzerPlugin.GetAnalyzers(), nil
   130  }