├── .rspec ├── formatter_test ├── detector_test ├── Gemfile ├── ws_lint-1.1.0.gem ├── screenshots ├── bad-code.png └── good-code.png ├── spec ├── formatter_spec.rb ├── detector_spec.rb └── spec_helper.rb ├── lib ├── detector.rb └── formatter.rb ├── .github └── workflows │ └── linters.yml ├── Gemfile.lock ├── ws_lint.gemspec ├── .rubocop.yml ├── LICENSE ├── README.md └── bin └── ws_lint /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /formatter_test: -------------------------------------------------------------------------------- 1 | print 'Hello world' 2 | 3 | print 'Good bye' -------------------------------------------------------------------------------- /detector_test: -------------------------------------------------------------------------------- 1 | print 'Hello world' 2 | 3 | 4 | print 'Good bye' -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'colorize', '~> 0.8.1' 4 | gem 'rspec' 5 | -------------------------------------------------------------------------------- /ws_lint-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meronogbai/whitespace-linter/HEAD/ws_lint-1.1.0.gem -------------------------------------------------------------------------------- /screenshots/bad-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meronogbai/whitespace-linter/HEAD/screenshots/bad-code.png -------------------------------------------------------------------------------- /screenshots/good-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meronogbai/whitespace-linter/HEAD/screenshots/good-code.png -------------------------------------------------------------------------------- /spec/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/formatter' 2 | 3 | describe Formatter do 4 | file_format = Formatter.new('formatter_test') 5 | describe '#format' do 6 | it 'formats file' do 7 | file_format.format 8 | expect(file_format.format).to eql ["print 'Hello world'\n", "\n", "print 'Good bye'"] 9 | end 10 | it "doesn't format unnecessary lines" do 11 | expect(file_format.format).not_to eql ["print 'Hello world'\n", "\n", "print 'Good bye'", "\n"] 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/detector.rb: -------------------------------------------------------------------------------- 1 | class Detector 2 | def initialize(file) 3 | @file = File.open(file).to_a 4 | end 5 | 6 | def detect_whitespace 7 | result = {} 8 | @file.each_with_index do |line, index| 9 | result[index + 1] = line if line.end_with?(" \n") || line.end_with?(' ') 10 | end 11 | result 12 | end 13 | 14 | def detect_empty 15 | result = {} 16 | @file.each_with_index do |line, index| 17 | result[index + 1] = line if @file[index] == "\n" && @file[index - 1] == "\n" 18 | end 19 | result 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | rubocop: 7 | name: Rubocop 8 | runs-on: ubuntu-18.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-ruby@v1 12 | with: 13 | ruby-version: 2.6.x 14 | - name: Setup Rubocop 15 | run: | 16 | gem install --no-document rubocop:'~>0.81.0' # https://docs.rubocop.org/en/stable/installation/ 17 | [ -f .rubocop.yml ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/ruby/.rubocop.yml 18 | - name: Rubocop Report 19 | run: rubocop --color -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | colorize (0.8.1) 5 | diff-lcs (1.4.4) 6 | rspec (3.10.0) 7 | rspec-core (~> 3.10.0) 8 | rspec-expectations (~> 3.10.0) 9 | rspec-mocks (~> 3.10.0) 10 | rspec-core (3.10.1) 11 | rspec-support (~> 3.10.0) 12 | rspec-expectations (3.10.1) 13 | diff-lcs (>= 1.2.0, < 2.0) 14 | rspec-support (~> 3.10.0) 15 | rspec-mocks (3.10.2) 16 | diff-lcs (>= 1.2.0, < 2.0) 17 | rspec-support (~> 3.10.0) 18 | rspec-support (3.10.2) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | colorize (~> 0.8.1) 25 | rspec 26 | 27 | BUNDLED WITH 28 | 2.2.15 29 | -------------------------------------------------------------------------------- /spec/detector_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/detector' 2 | 3 | describe Detector do 4 | test_file = Detector.new('detector_test') 5 | describe '#detect_whitespace' do 6 | it 'detects trailing whitespace' do 7 | expect(test_file.detect_whitespace).to eql({ 1 => "print 'Hello world' \n", 4 => "print 'Good bye' " }) 8 | end 9 | it "doesn't detect empty lines" do 10 | expect(test_file.detect_whitespace).not_to eql({ 3 => "\n", 4 => "\n" }) 11 | end 12 | end 13 | describe '#detect_empty' do 14 | it 'detects extra empty lines' do 15 | expect(test_file.detect_empty).to eql({ 3 => "\n" }) 16 | end 17 | it "doesn't detect non empty lines" do 18 | expect(test_file.detect_empty).not_to eql({ 1 => "print 'Hello world' \n", 4 => "print 'Good bye' " }) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /ws_lint.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'ws_lint' 3 | s.version = '1.1.0' 4 | s.executables << 'ws_lint' 5 | s.summary = 'Whitespace Linter' 6 | s.description = 7 | 'Basic linter that detects and formats trailing whitespace and extra empty lines in your code and config files' 8 | s.authors = ['Meron Ogbai'] 9 | s.email = 'okbaymeron@gmail.com' 10 | s.files = ['lib/detector.rb', 'lib/formatter.rb'] 11 | s.homepage = 'https://github.com/meronokbay/whitespace-linter' 12 | s.metadata = { 13 | 'source-code-uri' => 'https://github.com/meronokbay/whitespace-linter', 14 | 'documentation_uri' => 'https://github.com/meronokbay/whitespace-linter#readme', 15 | 'bug_tracker_uri' => 'https://github.com/meronokbay/whitespace-linter/issues', 16 | } 17 | s.license = 'MIT' 18 | s.add_runtime_dependency 'colorize', '~> 0.8.1' 19 | end 20 | -------------------------------------------------------------------------------- /lib/formatter.rb: -------------------------------------------------------------------------------- 1 | class Formatter 2 | def initialize(file) 3 | @file = file 4 | @temp = [] 5 | temp_file 6 | end 7 | 8 | def format 9 | whitespace_remover 10 | empty_line_remover 11 | @new_file = File.open(@file, 'w') 12 | @new_file.write(@temp.join) 13 | @temp 14 | end 15 | 16 | private 17 | 18 | def temp_file 19 | @read_file = File.open(@file, 'r') 20 | @read_file.each do |line| 21 | @temp << line 22 | end 23 | end 24 | 25 | def whitespace_remover 26 | @temp.each_with_index do |line, index| 27 | @temp[index] = if line.end_with?("\n") 28 | line.gsub(/\s+\n$/, "\n") 29 | else 30 | line.gsub(/\s+$/, '') 31 | end 32 | end 33 | end 34 | 35 | def empty_line_remover 36 | result = [] 37 | @temp.each_with_index do |line, index| 38 | result << line unless @temp[index - 1] == "\n" && line == "\n" 39 | end 40 | @temp = result 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - "Guardfile" 4 | - "Rakefile" 5 | 6 | DisplayCopNames: true 7 | 8 | Layout/LineLength: 9 | Max: 120 10 | Metrics/MethodLength: 11 | Max: 20 12 | Metrics/AbcSize: 13 | Max: 50 14 | Metrics/ClassLength: 15 | Max: 150 16 | Metrics/BlockLength: 17 | ExcludedMethods: ['describe'] 18 | Max: 30 19 | 20 | 21 | Style/Documentation: 22 | Enabled: false 23 | Style/ClassAndModuleChildren: 24 | Enabled: false 25 | Style/EachForSimpleLoop: 26 | Enabled: false 27 | Style/AndOr: 28 | Enabled: false 29 | Style/DefWithParentheses: 30 | Enabled: false 31 | Style/FrozenStringLiteralComment: 32 | EnforcedStyle: never 33 | 34 | Layout/HashAlignment: 35 | EnforcedColonStyle: key 36 | Layout/ExtraSpacing: 37 | AllowForAlignment: false 38 | Layout/MultilineMethodCallIndentation: 39 | Enabled: true 40 | EnforcedStyle: indented 41 | Lint/RaiseException: 42 | Enabled: false 43 | Lint/StructNewOverride: 44 | Enabled: false 45 | Style/HashEachMethods: 46 | Enabled: false 47 | Style/HashTransformKeys: 48 | Enabled: false 49 | Style/HashTransformValues: 50 | Enabled: false 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Meron Ogbai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Whitespace Linter 2 | 3 | > Basic linter that detects and formats trailing whitespace and extra empty lines. 4 | 5 | ## Features 6 | 7 | - Detects excessive whitespace in your file. 8 | - Allows you to fix detected errors. 9 | 10 | ## Built With 11 | 12 | - Ruby 13 | - Rspec 14 | 15 | ## Code examples 16 | 17 | ### Bad code 18 | 19 | ![screenshot](./screenshots/bad-code.png) 20 | 21 | ### Good code 22 | 23 | ![screenshot](./screenshots/good-code.png) 24 | 25 | ## Prerequisites 26 | 27 | - Ruby 28 | 29 | ## Install 30 | 31 | ```bash 32 | gem install ws_lint 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```bash 38 | ws_lint /path/to/file 39 | ``` 40 | 41 | ## Testing 42 | 43 | ```bash 44 | bundle exec rspec 45 | ``` 46 | ## Author 47 | 48 | 👤 **Meron Ogbai** 49 | 50 | - Github: [@meronokbay](https://github.com/meronokbay) 51 | - Twitter: [@MeronDev](https://twitter.com/MeronDev) 52 | - Linkedin: [Meron Ogbai](https://linkedin.com/in/meron-ogbai/) 53 | 54 | ## 🤝 Contributing 55 | 56 | Contributions, issues, and feature requests are welcome! 57 | 58 | ## Show your support 59 | 60 | Give a ⭐️ if you like this project! 61 | 62 | ## 📝 License 63 | 64 | This project is [MIT](lic.url) licensed. 65 | -------------------------------------------------------------------------------- /bin/ws_lint: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | require 'colorize' 3 | require_relative '../lib/detector' 4 | require_relative '../lib/formatter' 5 | 6 | begin 7 | file_lint = Detector.new(ARGV[0]) 8 | 9 | extra_whitespace = file_lint.detect_whitespace 10 | extra_empty_lines = file_lint.detect_empty 11 | 12 | unless extra_whitespace.empty? 13 | puts 14 | puts '....................................................' 15 | puts 16 | puts 'The following lines have extra trailing whitespace.' 17 | puts 18 | extra_whitespace.each do |num, line| 19 | print "#{num} #{line}".red 20 | end 21 | end 22 | 23 | unless extra_empty_lines.empty? 24 | puts 25 | puts '....................................................' 26 | puts 27 | puts 'The following empty lines are duplicate.' 28 | puts 29 | extra_empty_lines.each do |num, line| 30 | print "#{num} #{line}".red 31 | end 32 | puts '....................................................' 33 | puts 34 | end 35 | 36 | unless extra_whitespace.empty? && extra_empty_lines.empty? 37 | loop do 38 | puts 'Do you want to autoformat your file? y/n' 39 | format_prompt = $stdin.gets.chomp.downcase 40 | if format_prompt == 'y' 41 | file_format = Formatter.new(ARGV[0]) 42 | file_format.format 43 | puts 'Thanks for using whitespace linter' 44 | break 45 | elsif format_prompt == 'n' 46 | puts 'Thanks for using whitespace linter' 47 | break 48 | else 49 | puts 'Please input y or n' 50 | end 51 | end 52 | end 53 | 54 | puts 'No whitespace errors'.green if extra_empty_lines.empty? && extra_whitespace.empty? 55 | rescue StandardError 56 | puts 'Please input a valid file'.red 57 | end 58 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | # # This allows you to limit a spec run to individual examples or groups 50 | # # you care about by tagging them with `:focus` metadata. When nothing 51 | # # is tagged with `:focus`, all examples get run. RSpec also provides 52 | # # aliases for `it`, `describe`, and `context` that include `:focus` 53 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 54 | # config.filter_run_when_matching :focus 55 | # 56 | # # Allows RSpec to persist some state between runs in order to support 57 | # # the `--only-failures` and `--next-failure` CLI options. We recommend 58 | # # you configure your source control system to ignore this file. 59 | # config.example_status_persistence_file_path = "spec/examples.txt" 60 | # 61 | # # Limits the available syntax to the non-monkey patched syntax that is 62 | # # recommended. For more details, see: 63 | # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 64 | # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 65 | # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 66 | # config.disable_monkey_patching! 67 | # 68 | # # This setting enables warnings. It's recommended, but in some cases may 69 | # # be too noisy due to issues in dependencies. 70 | # config.warnings = true 71 | # 72 | # # Many RSpec users commonly either run the entire suite or an individual 73 | # # file, and it's useful to allow more verbose output when running an 74 | # # individual spec file. 75 | # if config.files_to_run.one? 76 | # # Use the documentation formatter for detailed output, 77 | # # unless a formatter has already been configured 78 | # # (e.g. via a command-line flag). 79 | # config.default_formatter = "doc" 80 | # end 81 | # 82 | # # Print the 10 slowest examples and example groups at the 83 | # # end of the spec run, to help surface which specs are running 84 | # # particularly slow. 85 | # config.profile_examples = 10 86 | # 87 | # # Run specs in random order to surface order dependencies. If you find an 88 | # # order dependency and want to debug it, you can fix the order by providing 89 | # # the seed, which is printed after each run. 90 | # # --seed 1234 91 | # config.order = :random 92 | # 93 | # # Seed global randomization in this process using the `--seed` CLI option. 94 | # # Setting this allows you to use `--seed` to deterministically reproduce 95 | # # test failures related to randomization by passing the same `--seed` value 96 | # # as the one that triggered the failure. 97 | # Kernel.srand config.seed 98 | end 99 | --------------------------------------------------------------------------------