Skip to content

Commit 97593c3

Browse files
committed
refactor: Implement tool plugins with declarative configuration
This major refactoring replaces hardcoded tool configuration with a plugin-based system: - Created a descriptive plugin system for tools using YAML configuration files - Implemented configuration processing with plugins/tool-utils.go - Created tool installation and execution logic in config/tools-installer.go
1 parent 3607792 commit 97593c3

File tree

12 files changed

+539
-31
lines changed

12 files changed

+539
-31
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ go.work
2222
go.work.sum
2323

2424
.idea/
25+
.vscode/
2526

26-
cli-v2
27+
cli-v2

cmd/analyze.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ var analyzeCmd = &cobra.Command{
205205
}
206206

207207
eslint := config.Config.Tools()["eslint"]
208-
eslintInstallationDirectory := eslint.Info()["installDir"]
208+
eslintInstallationDirectory := eslint.InstallDir
209209
nodeRuntime := config.Config.Runtimes()["node"]
210210
nodeBinary := nodeRuntime.Binaries["node"]
211211

@@ -220,4 +220,4 @@ var analyzeCmd = &cobra.Command{
220220

221221
tools.RunEslint(workDirectory, eslintInstallationDirectory, nodeBinary, args, autoFix, outputFile, outputFormat)
222222
},
223-
}
223+
}

cmd/install.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cmd
22

33
import (
44
cfg "codacy/cli-v2/config"
5-
"fmt"
65
"log"
76

87
"github.com/spf13/cobra"
@@ -33,18 +32,9 @@ func installRuntimes(config *cfg.ConfigType) {
3332
}
3433

3534
func installTools(config *cfg.ConfigType) {
36-
for _, tool := range config.Tools() {
37-
switch tool.Name() {
38-
case "eslint":
39-
// eslint needs node runtime
40-
nodeRuntime := config.Runtimes()["node"]
41-
err := cfg.InstallEslint(nodeRuntime, tool, registry)
42-
if err != nil {
43-
fmt.Println(err.Error())
44-
log.Fatal(err)
45-
}
46-
default:
47-
log.Fatal("Unknown tool:", tool.Name())
48-
}
35+
// Use the new tools-installer instead of manual installation
36+
err := cfg.InstallTools()
37+
if err != nil {
38+
log.Fatal(err)
4939
}
50-
}
40+
}

config-file/configFile.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,22 @@ func parseConfigFile(configContents []byte) error {
3636
return err
3737
}
3838

39+
// Convert the tool strings to ToolConfig objects
40+
toolConfigs := make([]plugins.ToolConfig, 0, len(configFile.TOOLS))
3941
for _, tl := range configFile.TOOLS {
4042
ct, err := parseConfigTool(tl)
4143
if err != nil {
4244
return err
4345
}
44-
config.Config.AddTool(config.NewRuntime(ct.name, ct.version))
46+
toolConfigs = append(toolConfigs, plugins.ToolConfig{
47+
Name: ct.name,
48+
Version: ct.version,
49+
})
50+
}
51+
52+
// Add all tools at once
53+
if err := config.Config.AddTools(toolConfigs); err != nil {
54+
return err
4555
}
4656

4757
return nil

config/config.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type ConfigType struct {
1717
projectConfigFile string
1818

1919
runtimes map[string]*plugins.RuntimeInfo
20-
tools map[string]*Runtime
20+
tools map[string]*plugins.ToolInfo
2121
}
2222

2323
func (c *ConfigType) HomePath() string {
@@ -63,13 +63,23 @@ func (c *ConfigType) AddRuntimes(configs []plugins.RuntimeConfig) error {
6363
return nil
6464
}
6565

66-
// TODO do inheritance with tool
67-
func (c *ConfigType) Tools() map[string]*Runtime {
66+
func (c *ConfigType) Tools() map[string]*plugins.ToolInfo {
6867
return c.tools
6968
}
7069

71-
func (c *ConfigType) AddTool(t *Runtime) {
72-
c.tools[t.Name()] = t
70+
func (c *ConfigType) AddTools(configs []plugins.ToolConfig) error {
71+
// Process the tool configurations using the plugins.ProcessTools function
72+
toolInfoMap, err := plugins.ProcessTools(configs, c.toolsDirectory)
73+
if err != nil {
74+
return err
75+
}
76+
77+
// Store the tool information in the config
78+
for name, info := range toolInfoMap {
79+
c.tools[name] = info
80+
}
81+
82+
return nil
7383
}
7484

7585
func (c *ConfigType) initCodacyDirs() {
@@ -117,7 +127,7 @@ func Init() {
117127
Config.initCodacyDirs()
118128

119129
Config.runtimes = make(map[string]*plugins.RuntimeInfo)
120-
Config.tools = make(map[string]*Runtime)
130+
Config.tools = make(map[string]*plugins.ToolInfo)
121131
}
122132

123133
// Global singleton config-file

config/tools-installer.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package config
2+
3+
import (
4+
"bytes"
5+
"codacy/cli-v2/plugins"
6+
"fmt"
7+
"log"
8+
"os"
9+
"os/exec"
10+
"path"
11+
"strings"
12+
"text/template"
13+
)
14+
15+
// InstallTools installs all tools defined in the configuration
16+
func InstallTools() error {
17+
for name, toolInfo := range Config.Tools() {
18+
err := InstallTool(name, toolInfo)
19+
if err != nil {
20+
return fmt.Errorf("failed to install tool %s: %w", name, err)
21+
}
22+
}
23+
return nil
24+
}
25+
26+
// InstallTool installs a specific tool
27+
func InstallTool(name string, toolInfo *plugins.ToolInfo) error {
28+
// Check if the tool is already installed
29+
if isToolInstalled(toolInfo) {
30+
fmt.Printf("Tool %s v%s is already installed\n", name, toolInfo.Version)
31+
return nil
32+
}
33+
34+
// Get the runtime for this tool
35+
runtimeInfo, ok := Config.Runtimes()[toolInfo.Runtime]
36+
if !ok {
37+
return fmt.Errorf("required runtime %s not found for tool %s", toolInfo.Runtime, name)
38+
}
39+
40+
// Make sure the installation directory exists
41+
err := os.MkdirAll(toolInfo.InstallDir, 0755)
42+
if err != nil {
43+
return fmt.Errorf("failed to create installation directory: %w", err)
44+
}
45+
46+
// Set registry if provided
47+
if toolInfo.RegistryCommand != "" {
48+
regCmd, err := executeToolTemplate(toolInfo.RegistryCommand, map[string]string{
49+
"InstallDir": toolInfo.InstallDir,
50+
"PackageName": toolInfo.Name,
51+
"Version": toolInfo.Version,
52+
"Registry": "", // TODO: Get registry from config
53+
})
54+
if err != nil {
55+
return fmt.Errorf("failed to prepare registry command: %w", err)
56+
}
57+
58+
if regCmd != "" {
59+
// For Node.js tools, use npm to set registry
60+
if toolInfo.Runtime == "node" {
61+
registryCmd := exec.Command(runtimeInfo.Binaries["npm"], strings.Split(regCmd, " ")...)
62+
if output, err := registryCmd.CombinedOutput(); err != nil {
63+
return fmt.Errorf("failed to set registry: %s: %w", string(output), err)
64+
}
65+
}
66+
}
67+
}
68+
69+
// Execute installation command
70+
installCmd, err := executeToolTemplate(toolInfo.InstallCommand, map[string]string{
71+
"InstallDir": toolInfo.InstallDir,
72+
"PackageName": toolInfo.Name,
73+
"Version": toolInfo.Version,
74+
"Registry": "", // TODO: Get registry from config
75+
})
76+
if err != nil {
77+
return fmt.Errorf("failed to prepare install command: %w", err)
78+
}
79+
80+
// For Node.js tools, use npm to install
81+
if toolInfo.Runtime == "node" {
82+
cmd := exec.Command(runtimeInfo.Binaries["npm"], strings.Split(installCmd, " ")...)
83+
84+
log.Printf("Installing %s v%s...\n", toolInfo.Name, toolInfo.Version)
85+
output, err := cmd.CombinedOutput()
86+
if err != nil {
87+
return fmt.Errorf("failed to install tool: %s: %w", string(output), err)
88+
}
89+
}
90+
91+
log.Printf("Successfully installed %s v%s\n", toolInfo.Name, toolInfo.Version)
92+
return nil
93+
}
94+
95+
// isToolInstalled checks if a tool is already installed by checking for the binary
96+
func isToolInstalled(toolInfo *plugins.ToolInfo) bool {
97+
// If there are no binaries, check the install directory
98+
if len(toolInfo.Binaries) == 0 {
99+
_, err := os.Stat(toolInfo.InstallDir)
100+
return err == nil
101+
}
102+
103+
// Check if at least one binary exists
104+
for _, binaryPath := range toolInfo.Binaries {
105+
_, err := os.Stat(binaryPath)
106+
if err == nil {
107+
return true
108+
}
109+
}
110+
111+
return false
112+
}
113+
114+
// executeToolTemplate executes a template with the given data
115+
func executeToolTemplate(tmplStr string, data map[string]string) (string, error) {
116+
tmpl, err := template.New("command").Parse(tmplStr)
117+
if err != nil {
118+
return "", err
119+
}
120+
121+
var buf bytes.Buffer
122+
err = tmpl.Execute(&buf, data)
123+
if err != nil {
124+
return "", err
125+
}
126+
127+
return buf.String(), nil
128+
}
129+
130+
// RunTool runs a tool with the specified options
131+
func RunTool(
132+
toolInfo *plugins.ToolInfo,
133+
workingDir string,
134+
pathsToCheck []string,
135+
autoFix bool,
136+
outputFile string,
137+
outputFormat string,
138+
) error {
139+
// Get the runtime for this tool
140+
runtimeInfo, ok := Config.Runtimes()[toolInfo.Runtime]
141+
if !ok {
142+
return fmt.Errorf("required runtime %s not found for tool %s", toolInfo.Runtime, toolInfo.Name)
143+
}
144+
145+
// Get the primary binary for this tool
146+
binary, ok := toolInfo.Binaries[toolInfo.Name]
147+
if !ok {
148+
return fmt.Errorf("%s binary not found in tool configuration", toolInfo.Name)
149+
}
150+
151+
// Prepare command based on runtime
152+
// For Node.js, we run the binary with the node executable
153+
cmd := exec.Command(runtimeInfo.Binaries["node"], binary)
154+
155+
// Add autofix flag if requested
156+
if autoFix && toolInfo.AutofixFlag != "" {
157+
cmd.Args = append(cmd.Args, strings.Split(toolInfo.AutofixFlag, " ")...)
158+
}
159+
160+
// Add output format if specified
161+
if outputFormat != "" {
162+
if formatFlag, ok := toolInfo.Formatters[outputFormat]; ok {
163+
cmd.Args = append(cmd.Args, strings.Split(formatFlag, " ")...)
164+
}
165+
}
166+
167+
// Add output file if specified
168+
if outputFile != "" && toolInfo.OutputFlag != "" {
169+
cmd.Args = append(cmd.Args, toolInfo.OutputFlag, outputFile)
170+
}
171+
172+
// Add paths to check
173+
if len(pathsToCheck) > 0 {
174+
cmd.Args = append(cmd.Args, pathsToCheck...)
175+
} else if toolInfo.DefaultPath != "" {
176+
cmd.Args = append(cmd.Args, toolInfo.DefaultPath)
177+
}
178+
179+
// Set working directory
180+
cmd.Dir = workingDir
181+
182+
// Set environment variables based on runtime
183+
// For Node.js, set NODE_PATH
184+
if toolInfo.Runtime == "node" {
185+
nodeModulesPath := path.Join(toolInfo.InstallDir, "node_modules")
186+
cmd.Env = append(cmd.Env, "NODE_PATH="+nodeModulesPath)
187+
}
188+
189+
// Run the command
190+
cmd.Stdout = os.Stdout
191+
cmd.Stderr = os.Stderr
192+
193+
err := cmd.Run()
194+
if err != nil {
195+
// Handle expected error cases based on the tool
196+
// For ESLint, it returns exit code 1 when it finds issues, which is normal
197+
if toolInfo.Name == "eslint" && strings.Contains(err.Error(), "exit status 1") {
198+
return nil
199+
}
200+
return fmt.Errorf("error running tool: %w", err)
201+
}
202+
203+
return nil
204+
}

0 commit comments

Comments
 (0)