Skip to content

Commit

Permalink
feat: configuration file of rules (#91)
Browse files Browse the repository at this point in the history
fixes #82

This PR adds a few things:
- A severity of issues and rules (info, warning and error)
- Parsing of a configuration file to add new rules, update severity of
rules or remove default rules
- Appropriated documentation for configuration file and the new system
when creating a rule
- A flag to init an empty configuration file

This refactor a lot of code so I am really open to any criticism as I
don't believe it is the best way to implement these features.
I also added a data field in the rules to be used later on.
  • Loading branch information
0xtekgrinder authored Oct 14, 2024
1 parent 0db3a94 commit 798dc04
Show file tree
Hide file tree
Showing 41 changed files with 719 additions and 142 deletions.
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,34 @@ To check the current directory, run:
tlin .
```

## Configuration

tlin supports a configuration file (`.tlin.yaml`) to customize its behavior. You can generate a default configuration file by running:

```bash
tlin -init
```

This command will create a `.tlin.yaml` file in the current directory with the following content:

```yaml
# .tlin.yaml
name: tlin
rules:
```
You can customize the configuration file to enable or disable specific lint rules, set cyclomatic complexity thresholds, and more.
```yaml
# .tlin.yaml
name: tlin
rules:
useless-break:
severity: WARNING
deprecated-function:
severity: OFF
```
## Adding Gno-Specific Lint Rules
Our linter allows addition of custom lint rules beyond the default golangci-lint rules. To add a new lint rule, follow these steps:
Expand Down Expand Up @@ -91,7 +119,7 @@ Our linter allows addition of custom lint rules beyond the default golangci-lint
}
```

b. Register your new rule in the `registerDefaultRules` method of the `Engine` struct in `internal/engine.go`:
b. Register your new rule in the `registerAllRules` and maybe `registerDefaultRules` method of the `Engine` struct in `internal/engine.go`:

```go
func (e *Engine) registerDefaultRules() {
Expand All @@ -103,6 +131,16 @@ Our linter allows addition of custom lint rules beyond the default golangci-lint
}
```

```go
func (e *Engine) registerAllRules() {
e.rules = append(e.rules,
&GolangciLintRule{},
// ...
&NewRule{}, // Add your new rule here
)
}
```

5. (Optional) If your rule requires special formatting, create a new formatter in the `formatter` package:

a. Create a new file (e.g., `formatter/new_rule.go`).
Expand Down Expand Up @@ -157,6 +195,8 @@ tlin supports several flags to customize its behavior:
- `-confidence <float>`: Set confidence threshold for auto-fixing (0.0 to 1.0, default: 0.75)
- `-o <path>`: Write output to a file instead of stdout
- `-json-output`: Output results in JSON format
- `-init`: Initialize a new tlin configuration file in the current directory
- `-c <path>`: Specify a custom configuration file

## Contributing

Expand Down
48 changes: 46 additions & 2 deletions cmd/tlin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
tt "github.com/gnolang/tlin/internal/types"
"github.com/gnolang/tlin/lint"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)

const (
Expand All @@ -31,6 +32,7 @@ type Config struct {
IgnoreRules string
FuncName string
Output string
ConfigurationPath string
Paths []string
Timeout time.Duration
CyclomaticThreshold int
Expand All @@ -40,6 +42,7 @@ type Config struct {
AutoFix bool
DryRun bool
JsonOutput bool
Init bool
}

func main() {
Expand All @@ -51,7 +54,16 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
defer cancel()

engine, err := lint.New(".", nil)
if config.Init {
err := initConfigurationFile(config.ConfigurationPath)
if err != nil {
logger.Error("Error initializing config file", zap.Error(err))
os.Exit(1)
}
return
}

engine, err := lint.New(".", nil, config.ConfigurationPath)
if err != nil {
logger.Fatal("Failed to initialize lint engine", zap.Error(err))
}
Expand Down Expand Up @@ -97,6 +109,8 @@ func parseFlags(args []string) Config {
flagSet.BoolVar(&config.DryRun, "dry-run", false, "Run in dry-run mode (show fixes without applying them)")
flagSet.BoolVar(&config.JsonOutput, "json", false, "Output issues in JSON format")
flagSet.Float64Var(&config.ConfidenceThreshold, "confidence", defaultConfidenceThreshold, "Confidence threshold for auto-fixing (0.0 to 1.0)")
flagSet.BoolVar(&config.Init, "init", false, "Initialize a new linter configuration file")
flagSet.StringVar(&config.ConfigurationPath, "c", ".tlin.yaml", "Path to the linter configuration file")

err := flagSet.Parse(args)
if err != nil {
Expand All @@ -105,7 +119,7 @@ func parseFlags(args []string) Config {
}

config.Paths = flagSet.Args()
if len(config.Paths) == 0 {
if !config.Init && len(config.Paths) == 0 {
fmt.Println("error: Please provide file or directory paths")
os.Exit(1)
}
Expand Down Expand Up @@ -212,6 +226,36 @@ func runAutoFix(ctx context.Context, logger *zap.Logger, engine lint.LintEngine,
}
}

func initConfigurationFile(configurationPath string) error {
if configurationPath == "" {
configurationPath = ".tlin.yaml"
}

// Create a yaml file with rules
config := lint.Config{
Name: "tlin",
Rules: map[string]tt.ConfigRule{},
}
d, err := yaml.Marshal(config)
if err != nil {
return err
}

f, err := os.Create(configurationPath)
if err != nil {
return err
}

defer f.Close()

_, err = f.Write(d)
if err != nil {
return err
}

return nil
}

func printIssues(logger *zap.Logger, issues []tt.Issue, isJson bool, jsonOutput string) {
issuesByFile := make(map[string][]tt.Issue)
for _, issue := range issues {
Expand Down
63 changes: 54 additions & 9 deletions cmd/tlin/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,33 @@ import (
"testing"
"time"

"github.com/gnolang/tlin/internal/types"
tt "github.com/gnolang/tlin/internal/types"
"github.com/gnolang/tlin/lint"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)

type mockLintEngine struct {
mock.Mock
}

func (m *mockLintEngine) Run(filePath string) ([]types.Issue, error) {
func (m *mockLintEngine) Run(filePath string) ([]tt.Issue, error) {
args := m.Called(filePath)
return args.Get(0).([]types.Issue), args.Error(1)
return args.Get(0).([]tt.Issue), args.Error(1)
}

func (m *mockLintEngine) RunSource(source []byte) ([]types.Issue, error) {
func (m *mockLintEngine) RunSource(source []byte) ([]tt.Issue, error) {
args := m.Called(source)
return args.Get(0).([]types.Issue), args.Error(1)
return args.Get(0).([]tt.Issue), args.Error(1)
}

func (m *mockLintEngine) IgnoreRule(rule string) {
m.Called(rule)
}

func setupMockEngine(expectedIssues []types.Issue, filePath string) *mockLintEngine {
func setupMockEngine(expectedIssues []tt.Issue, filePath string) *mockLintEngine {
mockEngine := new(mockLintEngine)
mockEngine.On("Run", filePath).Return(expectedIssues, nil)
return mockEngine
Expand All @@ -58,6 +60,7 @@ func TestParseFlags(t *testing.T) {
AutoFix: true,
Paths: []string{"file.go"},
ConfidenceThreshold: defaultConfidenceThreshold,
ConfigurationPath: ".tlin.yaml",
},
},
{
Expand All @@ -68,6 +71,7 @@ func TestParseFlags(t *testing.T) {
DryRun: true,
Paths: []string{"file.go"},
ConfidenceThreshold: defaultConfidenceThreshold,
ConfigurationPath: ".tlin.yaml",
},
},
{
Expand All @@ -77,6 +81,7 @@ func TestParseFlags(t *testing.T) {
AutoFix: true,
Paths: []string{"file.go"},
ConfidenceThreshold: 0.9,
ConfigurationPath: ".tlin.yaml",
},
},
{
Expand All @@ -86,6 +91,7 @@ func TestParseFlags(t *testing.T) {
Paths: []string{"file.go"},
JsonOutput: true,
ConfidenceThreshold: defaultConfidenceThreshold,
ConfigurationPath: ".tlin.yaml",
},
},
{
Expand All @@ -95,6 +101,16 @@ func TestParseFlags(t *testing.T) {
Paths: []string{"file.go"},
Output: "output.svg",
ConfidenceThreshold: defaultConfidenceThreshold,
ConfigurationPath: ".tlin.yaml",
},
},
{
name: "Configuration File",
args: []string{"-c", "config.yaml", "file.go"},
expected: Config{
Paths: []string{"file.go"},
ConfidenceThreshold: defaultConfidenceThreshold,
ConfigurationPath: "config.yaml",
},
},
}
Expand All @@ -111,6 +127,7 @@ func TestParseFlags(t *testing.T) {
assert.Equal(t, tt.expected.Paths, config.Paths)
assert.Equal(t, tt.expected.JsonOutput, config.JsonOutput)
assert.Equal(t, tt.expected.Output, config.Output)
assert.Equal(t, tt.expected.ConfigurationPath, config.ConfigurationPath)
})
}
}
Expand All @@ -136,6 +153,33 @@ func TestRunWithTimeout(t *testing.T) {
}
}

func TestInitConfigurationFile(t *testing.T) {
t.Parallel()
tempDir, err := os.MkdirTemp("", "init-test")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)

configPath := filepath.Join(tempDir, ".tlin.yaml")

err = initConfigurationFile(configPath)
assert.NoError(t, err)

_, err = os.Stat(configPath)
assert.NoError(t, err)

content, err := os.ReadFile(configPath)
assert.NoError(t, err)

expectedConfig := lint.Config{
Name: "tlin",
Rules: map[string]tt.ConfigRule{},
}
config := &lint.Config{}
yaml.Unmarshal(content, config)

assert.Equal(t, expectedConfig, *config)
}

func TestRunCFGAnalysis(t *testing.T) {
t.Parallel()
logger, _ := zap.NewProduction()
Expand Down Expand Up @@ -208,7 +252,7 @@ func TestRunAutoFix(t *testing.T) {
err = os.WriteFile(testFile, []byte(sliceRangeIssueExample), 0o644)
assert.NoError(t, err)

expectedIssues := []types.Issue{
expectedIssues := []tt.Issue{
{
Rule: "simplify-slice-range",
Filename: testFile,
Expand Down Expand Up @@ -267,7 +311,7 @@ func TestRunJsonOutput(t *testing.T) {
content, err := os.ReadFile(jsonOutput)
assert.NoError(t, err)

var actualContent map[string][]types.Issue
var actualContent map[string][]tt.Issue
err = json.Unmarshal(content, &actualContent)
assert.NoError(t, err)

Expand All @@ -284,6 +328,7 @@ func TestRunJsonOutput(t *testing.T) {
assert.Equal(t, 5, issue.Start.Column)
assert.Equal(t, 5, issue.End.Line)
assert.Equal(t, 24, issue.End.Column)
assert.Equal(t, tt.SeverityError, issue.Severity)
}

return
Expand All @@ -302,7 +347,7 @@ func TestRunJsonOutput(t *testing.T) {
err = os.WriteFile(testFile, []byte(sliceRangeIssueExample), 0o644)
assert.NoError(t, err)

expectedIssues := []types.Issue{
expectedIssues := []tt.Issue{
{
Rule: "simplify-slice-range",
Filename: testFile,
Expand Down
19 changes: 6 additions & 13 deletions formatter/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,22 +120,15 @@ func NewIssueFormatterBuilder(issue tt.Issue, snippet *internal.SourceCode) *Iss
}
}

// headerType represents the type of header to be added to the formatted issue.
// The header can be either a warning or an error.
type headerType int

const (
warningHeader headerType = iota
errorHeader
)

func (b *IssueFormatterBuilder) AddHeader(kind headerType) *IssueFormatterBuilder {
func (b *IssueFormatterBuilder) AddHeader() *IssueFormatterBuilder {
// add header type and rule name
switch kind {
case errorHeader:
switch b.issue.Severity {
case tt.SeverityError:
b.result.WriteString(errorStyle.Sprint("error: "))
case warningHeader:
case tt.SeverityWarning:
b.result.WriteString(warningStyle.Sprint("warning: "))
case tt.SeverityInfo:
b.result.WriteString(messageStyle.Sprint("info: "))
}

b.result.WriteString(ruleStyle.Sprintln(b.issue.Rule))
Expand Down
2 changes: 1 addition & 1 deletion formatter/cyclomatic_complexity.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type CyclomaticComplexityFormatter struct{}
func (f *CyclomaticComplexityFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string {
builder := NewIssueFormatterBuilder(issue, snippet)
return builder.
AddHeader(warningHeader).
AddHeader().
AddCodeSnippet().
AddComplexityInfo().
AddSuggestion().
Expand Down
2 changes: 1 addition & 1 deletion formatter/defers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type DefersFormatter struct{}
func (f *DefersFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string {
builder := NewIssueFormatterBuilder(issue, snippet)
return builder.
AddHeader(errorHeader).
AddHeader().
AddCodeSnippet().
AddUnderlineAndMessage().
AddNote().
Expand Down
2 changes: 1 addition & 1 deletion formatter/deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type DeprecatedFuncFormatter struct{}
func (f *DeprecatedFuncFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string {
builder := NewIssueFormatterBuilder(issue, snippet)
return builder.
AddHeader(errorHeader).
AddHeader().
AddCodeSnippet().
AddUnderlineAndMessage().
AddNote().
Expand Down
2 changes: 1 addition & 1 deletion formatter/early_return.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type EarlyReturnOpportunityFormatter struct{}
func (f *EarlyReturnOpportunityFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string {
builder := NewIssueFormatterBuilder(issue, snippet)
return builder.
AddHeader(warningHeader).
AddHeader().
AddCodeSnippet().
AddUnderlineAndMessage().
AddSuggestion().
Expand Down
Loading

0 comments on commit 798dc04

Please sign in to comment.