linter_changes 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +69 -36
- data/bin/linter_changes +1 -2
- data/bin/tapioca +27 -0
- data/lib/linter_changes/cli.rb +75 -0
- data/lib/linter_changes/config.rb +21 -0
- data/lib/linter_changes/git_diff.rb +64 -0
- data/lib/linter_changes/linter/base.rb +82 -0
- data/lib/linter_changes/linter/eslint/adapter.rb +23 -0
- data/lib/{linter → linter_changes/linter}/rubocop/adapter.rb +7 -31
- data/lib/{logger.rb → linter_changes/logger.rb} +4 -1
- data/lib/{version.rb → linter_changes/version.rb} +2 -1
- metadata +59 -23
- data/lib/cli.rb +0 -89
- data/lib/git_diff.rb +0 -39
- data/lib/linter/base.rb +0 -46
- data/lib/linter/rubocop/default_config.yml +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5724a288156947b1ab1eca983b1ad38fc831e4523a504f2f574fb59f15a3bf72
|
|
4
|
+
data.tar.gz: e5c14aeda697cd7e68dc0b8812c0289e6eb2396960ead4c5517599bd56fa94a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1502bf26bea370f48f367107989f0cd51ccf0ee32df9f7df628808d126cd658f41d867c90cf951396ab93eb75a8e79ecac5da1de93f6962df51ea4ad16296c67
|
|
7
|
+
data.tar.gz: d1e82c6a623eddc5b784b95a61ebd379626473efb018bbf92ff0d9242228380bc50b3d78a25ba13d69f6638ac39161498d92ee9d089050930f673ec1d370c2ea
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ LinterChanges is a Ruby gem that runs linters on files changed between your curr
|
|
|
4
4
|
|
|
5
5
|
**What sets LinterChanges apart from other tools like Pronto is that it checks entire files rather than just the changed lines when raising errors. Additionally, if configuration changes for the linter occur, LinterChanges will run the linter on the entire repository, not just on the current changes.**
|
|
6
6
|
|
|
7
|
-
Currently, **LinterChanges** supports **RuboCop** for Ruby code. Support for additional linters can be added in the future.
|
|
7
|
+
Currently, **LinterChanges** supports **RuboCop** for Ruby code and **ESLint** for JavaScript/Vue. Support for additional linters can be added in the future.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -15,8 +15,8 @@ Currently, **LinterChanges** supports **RuboCop** for Ruby code. Support for add
|
|
|
15
15
|
- [Installation](#installation)
|
|
16
16
|
- [Usage](#usage)
|
|
17
17
|
- [Basic Usage](#basic-usage)
|
|
18
|
-
- [
|
|
19
|
-
- [Customizing
|
|
18
|
+
- [CLI](#cli)
|
|
19
|
+
- [Customizing Configuration](#customizing-configuration)
|
|
20
20
|
- [Contributing](#contributing)
|
|
21
21
|
- [Running the test suite](#running-the-test-suite)
|
|
22
22
|
- [Acknowledgments](#acknowledgments)
|
|
@@ -34,75 +34,108 @@ gem 'linter_changes', git: 'https://github.com/bukhr/linter_changes.git'
|
|
|
34
34
|
```bash
|
|
35
35
|
bundle install
|
|
36
36
|
```
|
|
37
|
+
|
|
37
38
|
---
|
|
38
39
|
|
|
39
40
|
## Usage
|
|
40
41
|
|
|
41
|
-
LinterChanges provides a command-line interface (CLI) to run
|
|
42
|
+
LinterChanges provides a command-line interface (CLI) to run Linters on the files changed between your current branch and the target branch. Also if you change a configuration file, it will run on the entire repository.
|
|
42
43
|
|
|
43
44
|
### Basic Usage
|
|
44
45
|
|
|
45
46
|
By default, LinterChanges will:
|
|
46
47
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
48
|
+
- Run globally on all files applying all linters specify in configuration.
|
|
49
|
+
- The linters will only run on files that the linter listen to.
|
|
50
|
+
- Optionally, if you pass CHANGE_TARGET environment variable to run only on changed files between HEAD and the CHANGE_TARGET branch.
|
|
49
51
|
|
|
50
52
|
**Command:**
|
|
51
53
|
|
|
52
54
|
```bash
|
|
53
|
-
bin/linter_changes lint
|
|
55
|
+
CHANGE_TARGET=origin/master bin/linter_changes lint
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
**Example Output:**
|
|
57
59
|
|
|
60
|
+
```bash
|
|
61
|
+
❯ CHANGE_TARGET=master ./Jenkins/test-scripts/rubocop.sh
|
|
62
|
+
[DEBUG] Using configuration file: .linter_changes.yml
|
|
63
|
+
[DEBUG] Running Rubocop linter
|
|
64
|
+
[DEBUG] Target branch: origin/master
|
|
65
|
+
[DEBUG] Executing command: git diff --name-only origin/master...HEAD
|
|
66
|
+
[DEBUG] Changed files: bin/linter_changes, packs/hcm/core/employee_management/app/models/employee.rb
|
|
67
|
+
[DEBUG] Executing command: bin/rubocop --list-target-files
|
|
68
|
+
[DEBUG] Linting files with [Rubocop]: packs/hcm/core/employee_management/app/models/employee.rb
|
|
69
|
+
[DEBUG] [Rubocop] Executing command: bin/rubocop --parallel --extra-details --display-style-guide --fail-level convention --display-only-fail-level-offenses --format clang packs/hcm/core/employee_management/app/models/employee.rb
|
|
70
|
+
|
|
71
|
+
1 file inspected, no offenses detected
|
|
58
72
|
```
|
|
59
|
-
Running RuboCop linter
|
|
60
|
-
Linting files with RuboCop: app/models/user.rb, app/controllers/users_controller.rb
|
|
61
|
-
Inspecting 2 files
|
|
62
|
-
..
|
|
63
|
-
|
|
64
|
-
2 files inspected, no offenses detected
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Specifying the Target Branch
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
### CLI
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
The `linter_changes` command provides several options to customize its behavior:
|
|
72
77
|
|
|
73
78
|
```bash
|
|
74
|
-
bin/linter_changes lint
|
|
79
|
+
bin/linter_changes lint [options]
|
|
75
80
|
```
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
**Global Options:**
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
- `--debug`: Enable debug mode to see detailed logging information
|
|
85
|
+
- `--force_global`: Force running linters on all files, ignoring git diff
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
**Lint Command Options:**
|
|
82
88
|
|
|
83
|
-
|
|
89
|
+
- `--linters=rubocop,eslint,...`: Specify which linters to run (comma-separated list)
|
|
90
|
+
- `--config_file=PATH`: Path to the configuration file (default: `.linter_changes.yml`)
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
**Environment Variables:**
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
bin/linter_changes lint --config-files rubocop:rubocop,custom_rubocop.yml
|
|
89
|
-
```
|
|
94
|
+
- `CHANGE_TARGET`: Specify the target branch for comparison (e.g., `CHANGE_TARGET=origin/master`). If not specified, it will run on all files.
|
|
90
95
|
|
|
91
|
-
**
|
|
96
|
+
**Examples:**
|
|
92
97
|
|
|
93
98
|
```bash
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
# Run with debug logging
|
|
100
|
+
bin/linter_changes lint --debug
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
# Force global run regardless of changed files
|
|
103
|
+
bin/linter_changes lint --force_global
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
bin/linter_changes lint
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
# Only run specific linters
|
|
106
|
+
bin/linter_changes lint --linters=rubocop,eslint
|
|
107
|
+
|
|
108
|
+
# Use a custom config file
|
|
109
|
+
bin/linter_changes lint --config_file=custom_linter_config.yml
|
|
110
|
+
|
|
111
|
+
# Combine options
|
|
112
|
+
CHANGE_TARGET=main bin/linter_changes lint --debug --linters=rubocop
|
|
103
113
|
```
|
|
104
114
|
|
|
115
|
+
### Customizing Configuration
|
|
116
|
+
|
|
117
|
+
You can customize the configuration creating a file called `.linter_changes.yml` at the root of your proyect.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
|
|
121
|
+
```yml
|
|
105
122
|
---
|
|
123
|
+
rubocop:
|
|
124
|
+
linter_command: "bin/rubocop \
|
|
125
|
+
--parallel \
|
|
126
|
+
--extra-details \
|
|
127
|
+
--display-style-guide \
|
|
128
|
+
--fail-level convention \
|
|
129
|
+
--display-only-fail-level-offenses \
|
|
130
|
+
--format clang"
|
|
131
|
+
config_files:
|
|
132
|
+
- "rubocop"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
each key represents a linter (e.g., `rubocop`) and inside it you can specify the linter command and the files that trigger a global run of the linter.
|
|
136
|
+
In this example, if a file containing `rubocop` is edited, it would trigger a full run on all the proyect.
|
|
137
|
+
|
|
138
|
+
Also, if you want to specify another yml file, you can pass it though the cli with the option `--config_file`. The default is `.linter_changes.yml`.
|
|
106
139
|
|
|
107
140
|
## Contributing
|
|
108
141
|
|
|
@@ -154,4 +187,4 @@ bundle exec rake test
|
|
|
154
187
|
- **Full File Linting:** Unlike tools like Pronto that only check the changed lines, LinterChanges lints the entire files that have been modified. This ensures that any issues in the modified files are caught, not just those in the changed lines.
|
|
155
188
|
- **Configuration Change Detection:** If a configuration file for the linter (e.g., `.rubocop.yml`) has changed, LinterChanges will run the linter on the entire repository. This ensures that any new or altered linting rules are applied across all files.
|
|
156
189
|
|
|
157
|
-
---
|
|
190
|
+
---
|
data/bin/linter_changes
CHANGED
data/bin/tapioca
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# This file was generated by Bundler.
|
|
6
|
+
#
|
|
7
|
+
# The application 'tapioca' is installed as part of a gem, and
|
|
8
|
+
# this file is here to facilitate running it.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
12
|
+
|
|
13
|
+
bundle_binstub = File.expand_path("bundle", __dir__)
|
|
14
|
+
|
|
15
|
+
if File.file?(bundle_binstub)
|
|
16
|
+
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
|
17
|
+
load(bundle_binstub)
|
|
18
|
+
else
|
|
19
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
|
20
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
require "rubygems"
|
|
25
|
+
require "bundler/setup"
|
|
26
|
+
|
|
27
|
+
load Gem.bin_path("tapioca", "tapioca")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'thor'
|
|
5
|
+
require 'sorbet-runtime'
|
|
6
|
+
|
|
7
|
+
require_relative 'git_diff'
|
|
8
|
+
require_relative 'logger'
|
|
9
|
+
require_relative 'linter/base'
|
|
10
|
+
require_relative 'linter/rubocop/adapter'
|
|
11
|
+
require_relative 'linter/eslint/adapter'
|
|
12
|
+
require_relative 'config'
|
|
13
|
+
|
|
14
|
+
module LinterChanges
|
|
15
|
+
class CLI < Thor
|
|
16
|
+
extend T::Sig
|
|
17
|
+
|
|
18
|
+
class_option :debug, type: :boolean, default: false, desc: 'Enable debug mode'
|
|
19
|
+
class_option :force_global, type: :boolean, default: false, desc: 'Run all linters on global configuration'
|
|
20
|
+
|
|
21
|
+
method_option :linters, type: :array, default: [],
|
|
22
|
+
desc: 'Specify linters to run (e.g., rubocop,eslint). If no option provided, will run everything at config file'
|
|
23
|
+
method_option :config_file, type: :string, default: LinterChanges::Config::USER_CONFIG_PATH,
|
|
24
|
+
desc: 'Path to the configuration file. Default: $(pwd)/.linter_changes.yml'
|
|
25
|
+
desc 'lint', 'Run linters on changed files'
|
|
26
|
+
|
|
27
|
+
sig { returns(T.noreturn) }
|
|
28
|
+
def lint
|
|
29
|
+
Logger.debug_mode = options[:debug]
|
|
30
|
+
|
|
31
|
+
Logger.debug "Using configuration file: #{options[:config_file]}"
|
|
32
|
+
@config = LinterChanges::Config.load(config_file: options[:config_file])
|
|
33
|
+
overall_success = T.let(true, T::Boolean)
|
|
34
|
+
|
|
35
|
+
select_linters.each do |linter|
|
|
36
|
+
Logger.debug "Running #{linter.name.capitalize} linter"
|
|
37
|
+
overall_success &&= linter.run
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
exit(overall_success ? 0 : 1)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { returns(T::Boolean) }
|
|
44
|
+
def self.exit_on_failure?
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
no_commands do
|
|
49
|
+
def select_linters
|
|
50
|
+
linter_names = @config.keys && options[:linters]
|
|
51
|
+
if linter_names.empty?
|
|
52
|
+
Logger.error 'No linters specified on configuration file.'
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
linter_names.map do |name|
|
|
56
|
+
klass = AVAILABLE_LINTERS[name]
|
|
57
|
+
unless klass
|
|
58
|
+
Logger.error "Unknown linter specified: #{name}"
|
|
59
|
+
exit 1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# TODO: raise if no command found
|
|
63
|
+
config_files = @config[name]['config_files'] || []
|
|
64
|
+
command = @config[name]['linter_command']
|
|
65
|
+
klass.new(config_files:, command:, force_global: options[:force_global])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
AVAILABLE_LINTERS = {
|
|
71
|
+
'rubocop' => LinterChanges::Linter::RuboCop::Adapter,
|
|
72
|
+
'eslint' => LinterChanges::Linter::Eslint::Adapter
|
|
73
|
+
}.freeze
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module LinterChanges
|
|
6
|
+
class Config
|
|
7
|
+
extend T::Sig
|
|
8
|
+
USER_CONFIG_PATH = '.linter_changes.yml'
|
|
9
|
+
|
|
10
|
+
sig { params(config_file: String).returns(T::Hash[String, T.untyped]) }
|
|
11
|
+
def self.load config_file: USER_CONFIG_PATH
|
|
12
|
+
config_file = File.join(Dir.pwd, config_file)
|
|
13
|
+
user_config = File.exist?(config_file) ? YAML.load_file(config_file) : nil
|
|
14
|
+
unless user_config
|
|
15
|
+
raise StandardError.new 'No configuration file provided, you need the file .linter_changes.yml at the base of your proyect'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
user_config
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module LinterChanges
|
|
6
|
+
class GitDiff
|
|
7
|
+
extend T::Sig
|
|
8
|
+
sig { params(target_branch: String).void }
|
|
9
|
+
def initialize(target_branch:)
|
|
10
|
+
@target_branch = target_branch
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sig { returns(T::Array[String]) }
|
|
14
|
+
def changed_files
|
|
15
|
+
return @changed_files if defined? @changed_files
|
|
16
|
+
|
|
17
|
+
Logger.debug "Target branch: #{@target_branch}"
|
|
18
|
+
cmd = "git diff --name-only #{@target_branch}...HEAD"
|
|
19
|
+
Logger.debug "Executing command: #{cmd}"
|
|
20
|
+
|
|
21
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
22
|
+
unless status.success?
|
|
23
|
+
Logger.error "Error obtaining git changes: #{stderr}"
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@changed_files = stdout.strip.split("\n")
|
|
28
|
+
Logger.debug "Changed files: #{@changed_files.join(', ')}"
|
|
29
|
+
@changed_files
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { params(file: String, pattern: String).returns(T::Boolean) }
|
|
33
|
+
def changed_lines_contains? file:, pattern:
|
|
34
|
+
cmd = "git diff #{@target_branch}...HEAD -- #{file}"
|
|
35
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
36
|
+
unless status.success?
|
|
37
|
+
Logger.error "Error obtaining git diff for #{file}: #{stderr}"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
stdout.include? pattern
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(ref: String).returns(T::Boolean) }
|
|
44
|
+
def reference_exists?(ref)
|
|
45
|
+
!!system("git rev-parse --verify #{ref} > /dev/null 2>&1")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { returns(T::Boolean) }
|
|
49
|
+
def references_exists?
|
|
50
|
+
return @references_exist if defined?(@references_exists)
|
|
51
|
+
|
|
52
|
+
@references_exist = if !reference_exists?(@target_branch)
|
|
53
|
+
Logger.debug("Reference for #{@target_branch} does not exists")
|
|
54
|
+
false
|
|
55
|
+
elsif !reference_exists?('HEAD')
|
|
56
|
+
Logger.debug('Reference for HEAD does not exists')
|
|
57
|
+
false
|
|
58
|
+
else
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
@references_exist
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
3
|
+
module LinterChanges
|
|
4
|
+
module Linter
|
|
5
|
+
class Base
|
|
6
|
+
extend T::Sig
|
|
7
|
+
attr_reader :name
|
|
8
|
+
|
|
9
|
+
sig { params(config_files: T::Array[String], command: String, force_global: T::Boolean).void }
|
|
10
|
+
def initialize(config_files:, command:, force_global:)
|
|
11
|
+
@name = T.must(self.class.name).split('::')[-2].then do |adapter_name|
|
|
12
|
+
T.must(adapter_name).downcase
|
|
13
|
+
end
|
|
14
|
+
@config_files = config_files
|
|
15
|
+
@command = command
|
|
16
|
+
@base_command = @command.split(' ').first # used for listing files with in the adaptars
|
|
17
|
+
@target_branch = ENV['CHANGE_TARGET']
|
|
18
|
+
# We force origin if the target_branch is present
|
|
19
|
+
@target_branch = "origin/#{@target_branch}" if !@target_branch.nil? && !@target_branch['origin']
|
|
20
|
+
if @target_branch
|
|
21
|
+
@git_diff = GitDiff.new(target_branch: @target_branch)
|
|
22
|
+
end
|
|
23
|
+
@force_global = force_global || @target_branch.nil?
|
|
24
|
+
@force_global = true unless @git_diff&.references_exists?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { returns(T::Array[String]) }
|
|
28
|
+
def changed_files
|
|
29
|
+
@git_diff.changed_files
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns an array of files that the linter targets
|
|
33
|
+
sig { returns(T::Array[String]) }
|
|
34
|
+
def list_target_files
|
|
35
|
+
raise NotImplementedError, "#{self.class} must implement #list_target_files"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Checks if any configuration files have changed
|
|
39
|
+
sig { returns(T::Boolean) }
|
|
40
|
+
def config_changed?
|
|
41
|
+
@config_files.any? do |pattern|
|
|
42
|
+
changed_files.any? { |file| file.match? Regexp.new(pattern) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Runs the linter on the specified files
|
|
47
|
+
sig { returns(T::Boolean) }
|
|
48
|
+
def run
|
|
49
|
+
if @force_global
|
|
50
|
+
if @target_branch.nil?
|
|
51
|
+
Logger.debug "[#{name.capitalize}] No git branch provided by CHANGE_TARGET enviroment variable, running globally."
|
|
52
|
+
elsif !@git_diff.references_exists?
|
|
53
|
+
Logger.debug "[#{name.capitalize}] Some git reference does not exists locally. Forced to run globally"
|
|
54
|
+
else
|
|
55
|
+
Logger.debug "[#{name.capitalize}] Forced to run globally."
|
|
56
|
+
end
|
|
57
|
+
execute_linter(@command)
|
|
58
|
+
elsif config_changed?
|
|
59
|
+
Logger.debug "[#{name.capitalize}] Configuration changed. Running linter globally."
|
|
60
|
+
execute_linter(@command)
|
|
61
|
+
else
|
|
62
|
+
files_to_lint = list_target_files & changed_files
|
|
63
|
+
if files_to_lint.empty?
|
|
64
|
+
Logger.debug "No files to lint for [#{name.capitalize}]."
|
|
65
|
+
true
|
|
66
|
+
else
|
|
67
|
+
Logger.debug "Linting files with [#{name.capitalize}]: #{files_to_lint.join(', ')}"
|
|
68
|
+
execute_linter("#{@command} #{files_to_lint.join(' ')}")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
sig { params(command: String).returns(T::Boolean) }
|
|
76
|
+
def execute_linter(command)
|
|
77
|
+
Logger.debug "[#{name.capitalize}] Executing command: #{command}"
|
|
78
|
+
!!system(command)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
3
|
+
module LinterChanges
|
|
4
|
+
module Linter
|
|
5
|
+
module Eslint
|
|
6
|
+
class Adapter < Base
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
# ESLint no ofrece un equivalente a `bin/rubocop --list-target-files`,
|
|
10
|
+
# asi que derivamos los archivos objetivo filtrando los cambiados por
|
|
11
|
+
# extension. `Base#run` los intersecta con los archivos del diff, de
|
|
12
|
+
# modo que solo se linteara lo modificado. Los archivos en `.eslintignore`
|
|
13
|
+
# que pasen este filtro son descartados por el propio ESLint.
|
|
14
|
+
EXTENSIONS = T.let(%w[.js .jsx .ts .tsx .vue .mjs .cjs].freeze, T::Array[String])
|
|
15
|
+
|
|
16
|
+
sig { returns(T::Array[String]) }
|
|
17
|
+
def list_target_files
|
|
18
|
+
changed_files.select { |file| EXTENSIONS.include?(File.extname(file)) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# typed: true
|
|
2
2
|
|
|
3
3
|
module LinterChanges
|
|
4
4
|
module Linter
|
|
5
5
|
module RuboCop
|
|
6
6
|
class Adapter < Base
|
|
7
|
-
|
|
8
|
-
DEFAULT_CONFIG_FILES = [/rubocop/].freeze
|
|
9
|
-
DEFAULT_COMMAND = 'bin/rubocop'.freeze
|
|
7
|
+
extend T::Sig
|
|
10
8
|
|
|
9
|
+
sig { returns(T.any(T::Array[String], T.noreturn)) }
|
|
11
10
|
def list_target_files
|
|
12
|
-
cmd = "#{
|
|
11
|
+
cmd = "#{@base_command} --list-target-files"
|
|
13
12
|
Logger.debug "Executing command: #{cmd}"
|
|
14
13
|
|
|
15
14
|
stdout, stderr, status = Open3.capture3(cmd)
|
|
@@ -21,43 +20,20 @@ module LinterChanges
|
|
|
21
20
|
stdout.strip.split("\n")
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@command
|
|
27
|
-
else
|
|
28
|
-
"#{@command} #{files.join(' ')}"
|
|
29
|
-
end
|
|
30
|
-
execute_linter(cmd)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def config_changed?(changed_files)
|
|
23
|
+
sig { returns(T::Boolean) }
|
|
24
|
+
def config_changed?
|
|
34
25
|
# Check if any of the config files have changed
|
|
35
26
|
return true if super
|
|
36
27
|
|
|
37
28
|
# TODO: get the location of Gemfile.lock with bundle command
|
|
38
29
|
# Check if Gemfile.lock has changed and contains something related to rubocop
|
|
39
30
|
if changed_files.include?('Gemfile.lock') && @git_diff.changed_lines_contains?(file: 'Gemfile.lock',
|
|
40
|
-
|
|
31
|
+
pattern: 'rubocop')
|
|
41
32
|
Logger.debug 'Something related to rubocop gem version changed in Gemfile.lock'
|
|
42
33
|
return true
|
|
43
34
|
end
|
|
44
35
|
false
|
|
45
36
|
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def default_config_files
|
|
50
|
-
DEFAULT_CONFIG_FILES
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def default_command
|
|
54
|
-
DEFAULT_COMMAND
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def execute_linter(command)
|
|
58
|
-
Logger.debug "Executing RuboCop command: #{command}"
|
|
59
|
-
system(command)
|
|
60
|
-
end
|
|
61
37
|
end
|
|
62
38
|
end
|
|
63
39
|
end
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# typed: true
|
|
2
2
|
|
|
3
3
|
module LinterChanges
|
|
4
4
|
class Logger
|
|
5
5
|
class << self
|
|
6
|
+
extend T::Sig
|
|
6
7
|
attr_accessor :debug_mode
|
|
7
8
|
|
|
9
|
+
sig { params(message: String).void }
|
|
8
10
|
def debug(message)
|
|
9
11
|
puts "[DEBUG] #{message}" if debug_mode
|
|
10
12
|
end
|
|
11
13
|
|
|
14
|
+
sig { params(message: String).void }
|
|
12
15
|
def error(message)
|
|
13
16
|
puts "[ERROR] #{message}"
|
|
14
17
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: linter_changes
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jose Lara
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -19,7 +19,7 @@ dependencies:
|
|
|
19
19
|
version: '5.0'
|
|
20
20
|
- - "<"
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: '
|
|
22
|
+
version: '9.0'
|
|
23
23
|
type: :runtime
|
|
24
24
|
prerelease: false
|
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -29,21 +29,21 @@ dependencies:
|
|
|
29
29
|
version: '5.0'
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '9.0'
|
|
33
33
|
- !ruby/object:Gem::Dependency
|
|
34
|
-
name:
|
|
34
|
+
name: sorbet-runtime
|
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
39
|
+
version: '0.5'
|
|
40
40
|
type: :runtime
|
|
41
41
|
prerelease: false
|
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0'
|
|
46
|
+
version: '0.5'
|
|
47
47
|
- !ruby/object:Gem::Dependency
|
|
48
48
|
name: thor
|
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -62,16 +62,22 @@ dependencies:
|
|
|
62
62
|
name: activesupport
|
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
|
-
- - "
|
|
65
|
+
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '7'
|
|
68
|
+
- - "<"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '9.0'
|
|
68
71
|
type: :development
|
|
69
72
|
prerelease: false
|
|
70
73
|
version_requirements: !ruby/object:Gem::Requirement
|
|
71
74
|
requirements:
|
|
72
|
-
- - "
|
|
75
|
+
- - ">="
|
|
73
76
|
- !ruby/object:Gem::Version
|
|
74
77
|
version: '7'
|
|
78
|
+
- - "<"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '9.0'
|
|
75
81
|
- !ruby/object:Gem::Dependency
|
|
76
82
|
name: minitest
|
|
77
83
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -184,7 +190,35 @@ dependencies:
|
|
|
184
190
|
- - "~>"
|
|
185
191
|
- !ruby/object:Gem::Version
|
|
186
192
|
version: '6.0'
|
|
187
|
-
|
|
193
|
+
- !ruby/object:Gem::Dependency
|
|
194
|
+
name: sorbet
|
|
195
|
+
requirement: !ruby/object:Gem::Requirement
|
|
196
|
+
requirements:
|
|
197
|
+
- - ">="
|
|
198
|
+
- !ruby/object:Gem::Version
|
|
199
|
+
version: '0'
|
|
200
|
+
type: :development
|
|
201
|
+
prerelease: false
|
|
202
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
203
|
+
requirements:
|
|
204
|
+
- - ">="
|
|
205
|
+
- !ruby/object:Gem::Version
|
|
206
|
+
version: '0'
|
|
207
|
+
- !ruby/object:Gem::Dependency
|
|
208
|
+
name: tapioca
|
|
209
|
+
requirement: !ruby/object:Gem::Requirement
|
|
210
|
+
requirements:
|
|
211
|
+
- - ">="
|
|
212
|
+
- !ruby/object:Gem::Version
|
|
213
|
+
version: '0'
|
|
214
|
+
type: :development
|
|
215
|
+
prerelease: false
|
|
216
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
217
|
+
requirements:
|
|
218
|
+
- - ">="
|
|
219
|
+
- !ruby/object:Gem::Version
|
|
220
|
+
version: '0'
|
|
221
|
+
description:
|
|
188
222
|
email:
|
|
189
223
|
- jvlara@uc.cl
|
|
190
224
|
executables:
|
|
@@ -195,18 +229,20 @@ files:
|
|
|
195
229
|
- README.md
|
|
196
230
|
- bin/linter_changes
|
|
197
231
|
- bin/setup
|
|
198
|
-
-
|
|
199
|
-
- lib/
|
|
200
|
-
- lib/
|
|
201
|
-
- lib/
|
|
202
|
-
- lib/linter/
|
|
203
|
-
- lib/
|
|
204
|
-
- lib/
|
|
232
|
+
- bin/tapioca
|
|
233
|
+
- lib/linter_changes/cli.rb
|
|
234
|
+
- lib/linter_changes/config.rb
|
|
235
|
+
- lib/linter_changes/git_diff.rb
|
|
236
|
+
- lib/linter_changes/linter/base.rb
|
|
237
|
+
- lib/linter_changes/linter/eslint/adapter.rb
|
|
238
|
+
- lib/linter_changes/linter/rubocop/adapter.rb
|
|
239
|
+
- lib/linter_changes/logger.rb
|
|
240
|
+
- lib/linter_changes/version.rb
|
|
205
241
|
homepage: https://github.com/bukhr/linter_changes
|
|
206
242
|
licenses:
|
|
207
243
|
- MIT
|
|
208
244
|
metadata: {}
|
|
209
|
-
post_install_message:
|
|
245
|
+
post_install_message:
|
|
210
246
|
rdoc_options: []
|
|
211
247
|
require_paths:
|
|
212
248
|
- lib
|
|
@@ -224,8 +260,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
224
260
|
- !ruby/object:Gem::Version
|
|
225
261
|
version: '0'
|
|
226
262
|
requirements: []
|
|
227
|
-
rubygems_version: 3.
|
|
228
|
-
signing_key:
|
|
263
|
+
rubygems_version: 3.4.10
|
|
264
|
+
signing_key:
|
|
229
265
|
specification_version: 4
|
|
230
266
|
summary: Runs linters on code changes based on Git, either globally or only on modified
|
|
231
267
|
files, depending on the changes.
|
data/lib/cli.rb
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# lib/linter_changes/cli.rb
|
|
2
|
-
|
|
3
|
-
require 'bundler/setup'
|
|
4
|
-
require 'thor'
|
|
5
|
-
require_relative 'git_diff'
|
|
6
|
-
require_relative 'logger'
|
|
7
|
-
require_relative 'linter/base'
|
|
8
|
-
require_relative 'linter/rubocop/adapter'
|
|
9
|
-
require 'pry-byebug'
|
|
10
|
-
|
|
11
|
-
module LinterChanges
|
|
12
|
-
class CLI < Thor
|
|
13
|
-
class_option :debug, type: :boolean, default: false, desc: 'Enable debug mode'
|
|
14
|
-
class_option :target_branch, type: :string, default: nil, desc: 'Specify the target branch'
|
|
15
|
-
|
|
16
|
-
desc 'lint', 'Run linters on changed files'
|
|
17
|
-
method_option :linters, type: :array, default: [], desc: 'Specify linters to run (e.g., rubocop,eslint)'
|
|
18
|
-
method_option :config_files, type: :hash, default: {},
|
|
19
|
-
desc: 'Specify config files per linter (e.g., rubocop=.rubocop.yml)'
|
|
20
|
-
method_option :linter_command, type: :hash, default: {},
|
|
21
|
-
desc: 'Specify command per linter (e.g., rubocop="rubocop --parallel")'
|
|
22
|
-
def lint
|
|
23
|
-
Logger.debug_mode = options[:debug]
|
|
24
|
-
|
|
25
|
-
git_diff = GitDiff.new(target_branch: options[:target_branch])
|
|
26
|
-
changed_files = git_diff.changed_files
|
|
27
|
-
linters = select_linters git_diff
|
|
28
|
-
overall_success = true
|
|
29
|
-
|
|
30
|
-
linters.each do |linter|
|
|
31
|
-
Logger.debug "Running #{linter.name.capitalize} linter"
|
|
32
|
-
if linter.config_changed?(changed_files)
|
|
33
|
-
Logger.debug "#{linter.name.capitalize} configuration changed. Running linter globally."
|
|
34
|
-
success = linter.run
|
|
35
|
-
else
|
|
36
|
-
files_to_lint = linter.list_target_files & changed_files
|
|
37
|
-
if files_to_lint.empty?
|
|
38
|
-
Logger.debug "No files to lint for #{linter.name.capitalize}."
|
|
39
|
-
next
|
|
40
|
-
else
|
|
41
|
-
Logger.debug "Linting files with #{linter.name.capitalize}: #{files_to_lint.join(', ')}"
|
|
42
|
-
success = linter.run(files: files_to_lint)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
overall_success &&= success
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
exit(overall_success ? 0 : 1)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def self.exit_on_failure?
|
|
53
|
-
true
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
no_commands do
|
|
57
|
-
def select_linters git_diff
|
|
58
|
-
linter_names = if options[:linters].empty?
|
|
59
|
-
AVAILABLE_LINTERS.keys
|
|
60
|
-
else
|
|
61
|
-
options[:linters]
|
|
62
|
-
end
|
|
63
|
-
linter_names.map do |name|
|
|
64
|
-
klass = AVAILABLE_LINTERS[name]
|
|
65
|
-
unless klass
|
|
66
|
-
Logger.error "Unknown linter specified: #{name}"
|
|
67
|
-
exit 1
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Pass custom config files and commands if provided
|
|
71
|
-
config_files = parse_config_files_option(name)
|
|
72
|
-
command = options[:linter_command][name]
|
|
73
|
-
klass.new(config_files:, command:, git_diff:)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def parse_config_files_option(linter_name)
|
|
78
|
-
config_files_option = options[:config_files][linter_name]
|
|
79
|
-
return nil unless config_files_option
|
|
80
|
-
|
|
81
|
-
config_files_option.split(',')
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
AVAILABLE_LINTERS = {
|
|
86
|
-
'rubocop' => LinterChanges::Linter::RuboCop::Adapter
|
|
87
|
-
}.freeze
|
|
88
|
-
end
|
|
89
|
-
end
|
data/lib/git_diff.rb
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# lib/your_linter_gem/git_diff.rb
|
|
2
|
-
|
|
3
|
-
require 'open3'
|
|
4
|
-
|
|
5
|
-
module LinterChanges
|
|
6
|
-
class GitDiff
|
|
7
|
-
DEFAULT_TARGET_BRANCH = 'origin/master'
|
|
8
|
-
|
|
9
|
-
def initialize(target_branch: nil)
|
|
10
|
-
@target_branch = target_branch || ENV['CHANGE_TARGET'] || DEFAULT_TARGET_BRANCH
|
|
11
|
-
Logger.debug "Target branch: #{@target_branch}"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def changed_files
|
|
15
|
-
cmd = "git diff --name-only #{@target_branch}...HEAD"
|
|
16
|
-
Logger.debug "Executing command: #{cmd}"
|
|
17
|
-
|
|
18
|
-
stdout, stderr, status = Open3.capture3(cmd)
|
|
19
|
-
unless status.success?
|
|
20
|
-
Logger.error "Error obtaining git changes: #{stderr}"
|
|
21
|
-
exit 1
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
files = stdout.strip.split("\n")
|
|
25
|
-
Logger.debug "Changed files: #{files.join(', ')}"
|
|
26
|
-
files
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def changed_lines_contains? file:, pattern:
|
|
30
|
-
cmd = "git diff #{@target_branch}...HEAD -- #{file}"
|
|
31
|
-
stdout, stderr, status = Open3.capture3(cmd)
|
|
32
|
-
unless status.success?
|
|
33
|
-
Logger.error "Error obtaining git diff for #{file}: #{stderr}"
|
|
34
|
-
exit 1
|
|
35
|
-
end
|
|
36
|
-
stdout.include? pattern
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
data/lib/linter/base.rb
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# lib/linter_changes/linter/base.rb
|
|
2
|
-
|
|
3
|
-
module LinterChanges
|
|
4
|
-
module Linter
|
|
5
|
-
class Base
|
|
6
|
-
attr_reader :name
|
|
7
|
-
|
|
8
|
-
def initialize(config_files: nil, command: nil, git_diff: nil)
|
|
9
|
-
@name = self.class.name.split('::')[-2].downcase
|
|
10
|
-
default_config_file = YAML.load_file("lib/linter/#{@name}/default_config.yml")
|
|
11
|
-
@config_files = config_files || default_config_file['config_files']
|
|
12
|
-
@command = command || default_config_file['command']
|
|
13
|
-
@git_diff = git_diff
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Returns an array of files that the linter targets
|
|
17
|
-
def list_target_files
|
|
18
|
-
raise NotImplementedError, "#{self.class} must implement #list_target_files"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Checks if any configuration files have changed
|
|
22
|
-
def config_changed?(changed_files)
|
|
23
|
-
changed = @config_files.any? do |pattern|
|
|
24
|
-
changed_files.any? { |file| file.match? Regexp.new(pattern) }
|
|
25
|
-
end
|
|
26
|
-
Logger.debug "#{@name.capitalize} configuration changed: #{changed}"
|
|
27
|
-
changed
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Runs the linter on the specified files
|
|
31
|
-
def run(files: [])
|
|
32
|
-
raise NotImplementedError, "#{self.class} must implement #run"
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def default_config_files
|
|
38
|
-
raise NotImplementedError, "#{self.class} must implement #default_config_files"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def default_command
|
|
42
|
-
raise NotImplementedError, "#{self.class} must implement #default_command"
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|