├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── docs └── assets │ └── linterbot-comment-pull-request.png ├── exe └── linterbot ├── lib ├── linterbot.rb └── linterbot │ ├── comment.rb │ ├── comment_generator.rb │ ├── commit_approver.rb │ ├── description.rb │ ├── github_pull_request_commenter.rb │ ├── linter_report.rb │ ├── patch.rb │ ├── pull_request.rb │ ├── pull_request_analysis_result.rb │ ├── pull_request_analyzer.rb │ ├── result_handler.rb │ ├── runner.rb │ ├── runner_configuration.rb │ ├── tty_approver.rb │ ├── tty_pull_request_commenter.rb │ └── version.rb ├── linterbot.gemspec └── spec ├── fixtures └── swiftlint-report.json ├── linterbot_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | .linterbot.yml 12 | vendor -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.0 4 | before_install: gem install bundler -v 1.11.2 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in linterbot.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Guido Marucci Blas 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linterbot 2 | 3 | [![Gem Version](https://badge.fury.io/rb/linterbot.svg)](https://badge.fury.io/rb/linterbot) 4 | 5 | A bot that parses [SwiftLint](https://github.com/realm/SwiftLint) output and analyzes a GitHub pull request. Then for each linter violation it will make comment in the pull request diff on the line where the violation was made. 6 | 7 | ![linterbot commenting on pull request](./docs/assets/linterbot-comment-pull-request.png) 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'linterbot' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install linterbot 24 | 25 | ## Usage 26 | 27 | ### Locally 28 | 29 | If you want to try it locally: 30 | 31 | ``` 32 | swiftlint lint --reporter json | linterbot REPOSITORY PULL_REQUEST_NUMBER 33 | ``` 34 | 35 | ### TravisCI 36 | 37 | If you want to run it in TravisCI for every pull request triggered build you can create a script (with execution permission) called `linter` with the following content: 38 | 39 | ```bash 40 | #!/bin/bash 41 | 42 | if [ "$TRAVIS_PULL_REQUEST" != "false" ] 43 | then 44 | swiftlint --reporter json > swiftlint-report.json || false 45 | linterbot $TRAVIS_REPO_SLUG $TRAVIS_PULL_REQUEST < swiftlint-report.json 46 | fi 47 | ``` 48 | *`|| false` avoids a build fail if there are severe lint error* 49 | 50 | Finally in your `.travis.yml` file: 51 | 52 | ```yml 53 | language: objective-c 54 | osx_image: xcode7.2 55 | before_install: 56 | - gem install bundler 57 | - gem install linterbot 58 | - brew install swiftlint 59 | script: 60 | - linter 61 | - xcodebuild clean build test -project YourProject.xcodeproj -scheme YourProject 62 | ``` 63 | 64 | For more help run: 65 | 66 | ``` 67 | linterbot -h 68 | ``` 69 | 70 | and if you want to check all the available options for the `run` command (which is the default command to be run if none is provided) then run: 71 | 72 | ``` 73 | linterbot help run 74 | ``` 75 | 76 | ### GitHub access 77 | 78 | In order for linterbot to be able to comment on your pull request it needs write access to the specified repository. Remember to add the `repo` scope when you create the GitHub access token. If you don't lintebot won't be able to run. For more information on how to create an access token check [this](https://help.github.com/articles/creating-an-access-token-for-command-line-use/) tutorial. If you want to know more about GitHub's OAuth scopes check [this](https://developer.github.com/v3/oauth/#scopes) section in their documentation. 79 | 80 | You can provided an access token by either using the environmental variable `GITHUB_ACCESS_TOKEN` or using the `.linterbot.yml` (which should not be committed to your git repository). 81 | 82 | ### Configuration file 83 | 84 | You can define some configuration parameters in configuration file. By default `linterbot` will try to load `.linterbot.yml` from the current working directory. You can change it using the `--config-file-path` option. 85 | 86 | The following are the supported parameters you can configure in the `.linterbot.yml` file: 87 | 88 | ```yml 89 | github_access_token: 'YOUR_GITHUB_ACCESS_TOKEN' 90 | linter_report_file: 'PATH/TO/SWIFTLINT/JSON/OUTPUT/FILE' 91 | project_base_path: 'BASE/PROJECT/PATH' 92 | ``` 93 | 94 | By default `linterbot` will read from the standard input the JSON output of the `swiftlint lint --reporter json` command. You can tell `linterbot` to read the `swiftlint` output from a specific file either using the `--linter-report-file-path` option or through the `.linterbot.yml` file. 95 | 96 | The base path of project must be provided. By default the current working directory where `linterbot` was executed is used. You can change it either using the `--project-base-path` or through the `.linterbot.yml` file. 97 | 98 | ## Development 99 | 100 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 101 | 102 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 103 | 104 | ## Contributing 105 | 106 | Bug reports and pull requests are welcome on GitHub at https://github.com/guidomb/linterbot. 107 | 108 | 109 | ## License 110 | 111 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 112 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "linterbot" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /docs/assets/linterbot-comment-pull-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guidomb/linterbot/a8d1521635eba7c6e5238e92689e67728f4e33ea/docs/assets/linterbot-comment-pull-request.png -------------------------------------------------------------------------------- /exe/linterbot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if Gem::Specification::find_all_by_name('bundler').any? 4 | require 'bundler/setup' 5 | else 6 | require 'rubygems' 7 | gem 'linterbot' 8 | end 9 | 10 | require 'commander/import' 11 | require 'linterbot' 12 | 13 | PROGRAM_NAME = 'linterbot' 14 | DEFAULT_DRY_RUN = false 15 | 16 | program :name, PROGRAM_NAME 17 | program :version, Linterbot::VERSION 18 | program :description, Linterbot::DESCRIPTION 19 | default_command :run 20 | 21 | def error(message, command) 22 | STDERR.puts "Error: #{message}" 23 | STDERR.puts "Run 'linterbot help #{command}' for more information." 24 | exit 1 25 | end 26 | 27 | command :run do |c| 28 | c.syntax = "#{PROGRAM_NAME} run [options]" 29 | c.description = 'Analyzes a GitHub pull request for linter violations.' 30 | c.option '-p', '--project-base-path PROJECT_BASE_PATH', String, "Sets the project's base path. Default '#{Linterbot::RunnerConfiguration::DEFAULT_PROJECT_BASE_PATH}'." 31 | c.option '-c', '--config-file-path', "Sets the config file path. Default '#{Linterbot::RunnerConfiguration::DEFAULT_CONFIG_FILE_PATH}'" 32 | c.option '-x', '--dry-run', "Executes bot without modifing GitHub's pull request, Default '#{DEFAULT_DRY_RUN}'" 33 | c.option '-f', '--linter-report-file-path LINTER_REPORT_FILE_PATH', String, "Sets the linter report to be used. Default stdin." 34 | c.action do |args, options| 35 | options.default :'dry-run' => DEFAULT_DRY_RUN 36 | repository, pull_request_number = *args.take(2) 37 | 38 | error("You must provide the name of the repository.", "run") unless repository 39 | error("You must provide the pull request number.", "run") unless pull_request_number 40 | 41 | begin 42 | configuration = Linterbot::RunnerConfiguration.configuration!(options) 43 | runner = Linterbot::Runner.new(configuration) 44 | puts "Running linterbot for '#{repository}##{pull_request_number}' ..." 45 | runner.run(repository, pull_request_number) 46 | puts "Linterbot has successfully reviewed your PR ;-)" 47 | rescue Linterbot::RunnerConfiguration::MissingAttribute => exception 48 | STDERR.puts "Missing configuration attribute '#{exception.attribute_name}'" 49 | STDERR.puts "#{exception.fix_description}\n" 50 | exit 1 51 | rescue Linterbot::RunnerConfiguration::InvalidGitHubCredentials => exception 52 | STDERR.puts "Invalid GitHub credentials" 53 | STDERR.puts "The given access token does not have the 'repo' scope" 54 | STDERR.puts "\nFor more info on how to create an access token check:" 55 | STDERR.puts "https://help.github.com/articles/creating-an-access-token-for-command-line-use/" 56 | STDERR.puts "\nIf you want to know more about GitHub's OAuth scopes check:" 57 | STDERR.puts "https://developer.github.com/v3/oauth/#scopes\n" 58 | exit 1 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/linterbot.rb: -------------------------------------------------------------------------------- 1 | require "linterbot/comment_generator" 2 | require "linterbot/comment" 3 | require "linterbot/commit_approver" 4 | require "linterbot/description" 5 | require "linterbot/github_pull_request_commenter" 6 | require "linterbot/linter_report" 7 | require "linterbot/patch" 8 | require "linterbot/pull_request_analyzer" 9 | require "linterbot/pull_request_analysis_result" 10 | require "linterbot/pull_request" 11 | require "linterbot/result_handler" 12 | require "linterbot/runner_configuration" 13 | require "linterbot/runner" 14 | require "linterbot/tty_approver" 15 | require "linterbot/tty_pull_request_commenter" 16 | require "linterbot/version" 17 | 18 | module Linterbot 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/linterbot/comment.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Linterbot 4 | 5 | class Comment 6 | extend Forwardable 7 | 8 | attr_accessor :sha 9 | attr_accessor :patch_line_number 10 | 11 | def_delegator :@hint, :reason, :message 12 | def_delegators :@hint, 13 | :severity, 14 | :file 15 | 16 | def initialize(sha:, hint:, patch_line_number:) 17 | @sha = sha 18 | @hint = hint 19 | @patch_line_number = patch_line_number 20 | end 21 | 22 | private 23 | 24 | attr_accessor :hint 25 | 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /lib/linterbot/comment_generator.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class CommentGenerator 4 | 5 | attr_accessor :filename 6 | attr_accessor :commit 7 | attr_accessor :pull_request_file_patch 8 | attr_accessor :commits_count_for_file 9 | 10 | def initialize(filename, commit, pull_request_file_patch, commits_count_for_file) 11 | @filename = filename 12 | @commit = commit 13 | @pull_request_file_patch = Patch.new(pull_request_file_patch) 14 | @commits_count_for_file = commits_count_for_file 15 | end 16 | 17 | def generate_comments(hints) 18 | hints.map { |hint| generate_comment_for_hint(hint) } 19 | .select { |comment| comment != nil } 20 | end 21 | 22 | def generate_comment_for_hint(hint) 23 | patch_line_number = comment_position_for_hint(hint) 24 | if patch_line_number 25 | Comment.new(sha: commit.sha, patch_line_number: patch_line_number, hint: hint) 26 | end 27 | end 28 | 29 | def file 30 | @file ||= find_file 31 | end 32 | 33 | private 34 | 35 | def find_file 36 | file_index = commit.files.find_index { |file| file.filename == filename } 37 | commit.files[file_index] 38 | end 39 | 40 | def new_file? 41 | file.status == "added" 42 | end 43 | 44 | def modified_file? 45 | file.status == "modified" 46 | end 47 | 48 | def file_patch 49 | Patch.new(file.patch) 50 | end 51 | 52 | def included_in_file_patch?(hint) 53 | file_patch.included_in_patch?(hint) 54 | end 55 | 56 | def pull_request_file_patch_line_number(hint) 57 | pull_request_file_patch 58 | .additions_ranges_for_hint(hint) 59 | .map { |diff_range, line_number| line_number + (hint.line - diff_range.first) + 1 } 60 | .first 61 | end 62 | 63 | def comment_position_for_hint(hint) 64 | if new_file? && commits_count_for_file == 1 65 | hint.line 66 | elsif modified_file? && included_in_file_patch?(hint) 67 | pull_request_file_patch_line_number(hint) 68 | end 69 | end 70 | 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /lib/linterbot/commit_approver.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class CommitApprover 4 | 5 | attr_accessor :github_client 6 | 7 | def initialize(github_client) 8 | @github_client = github_client 9 | end 10 | 11 | def approve(repository, sha) 12 | github_client.create_status(repository, sha, "success", context: context, description: approve_description) 13 | end 14 | 15 | def reject(repository, sha, serious_violations_count) 16 | github_client.create_status(repository, sha, "failure", context: context, description: reject_description(serious_violations_count)) 17 | end 18 | 19 | def pending(repository, sha) 20 | github_client.create_status(repository, sha, "pending", context: context) 21 | end 22 | 23 | def error(repository, sha) 24 | github_client.create_status(repository, sha, "error", context: context) 25 | end 26 | 27 | private 28 | 29 | def context 30 | "linterbot" 31 | end 32 | 33 | def approve_description 34 | "The pull request passed the linter validations!" 35 | end 36 | 37 | def reject_description(serious_violations_count) 38 | "There are #{serious_violations_count} serious linter violations that must be fixed!" 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/linterbot/description.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | DESCRIPTION = "A bot that parses swiftlint output and analyzes a GitHub pull request." 3 | end 4 | -------------------------------------------------------------------------------- /lib/linterbot/github_pull_request_commenter.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module Linterbot 4 | 5 | class GitHubPullRequestCommenter 6 | 7 | attr_accessor :repository 8 | attr_accessor :pull_request_number 9 | attr_accessor :github_client 10 | 11 | def initialize(repository, pull_request_number, github_client) 12 | @repository = repository 13 | @pull_request_number = pull_request_number 14 | @github_client = github_client 15 | end 16 | 17 | def publish_comment(comment) 18 | message = "#{comment.severity.upcase} - #{comment.message}\n" 19 | if comment_exist?(message) 20 | puts "Comment was not published because it already exists: #{message}" 21 | else 22 | create_pull_request_comment(message, comment.sha, comment.file, comment.patch_line_number) 23 | end 24 | end 25 | 26 | def publish_summary(summary) 27 | if same_as_last_summary?(summary) 28 | puts "Summary was not published because it's the same as the last result summary:\n #{summary}" 29 | else 30 | github_client.add_comment(repository, pull_request_number, summary) 31 | end 32 | end 33 | 34 | private 35 | 36 | def create_pull_request_comment(message, sha, file, patch_line_number) 37 | args = [ 38 | repository, 39 | pull_request_number, 40 | message, 41 | sha, 42 | file, 43 | patch_line_number 44 | ] 45 | github_client.create_pull_request_comment(*args) 46 | end 47 | 48 | def comment_exist?(message) 49 | existing_comments.find { |comment| same_comment?(comment, message) } 50 | end 51 | 52 | def existing_summaries 53 | @existing_summaries ||= fetch_existing_comments("issue") 54 | end 55 | 56 | def latest_existing_comment 57 | @latest_existing_comment ||= existing_summaries.sort { |a, b| b.created_at <=> a.created_at }.first 58 | end 59 | 60 | def same_as_last_summary?(summary) 61 | latest_existing_comment && same_comment?(latest_existing_comment, summary) 62 | end 63 | 64 | def same_comment?(comment, message) 65 | comment.body == message && comment.user.id == bot_github_id 66 | end 67 | 68 | def existing_comments 69 | @existing_comments ||= fetch_existing_comments("pull_request") 70 | end 71 | 72 | def fetch_existing_comments(source) 73 | github_client.send("#{source}_comments", repository, pull_request_number).map do |comment| 74 | user = OpenStruct.new(comment[:user].to_h) 75 | OpenStruct.new(comment.to_h.merge(user: user)) 76 | end 77 | end 78 | 79 | def bot_github_id 80 | @bot_github_id ||= github_client.user[:id] 81 | end 82 | 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /lib/linterbot/linter_report.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'json' 3 | 4 | module Linterbot 5 | 6 | class LinterReport 7 | 8 | attr_accessor :report_file 9 | 10 | def initialize(report_file) 11 | @report_file = report_file 12 | end 13 | 14 | def linter_report 15 | @linter_report ||= JSON.parse(report_file_content) 16 | end 17 | 18 | def hints_by_file(base_path) 19 | hints_for_base_path(base_path).reduce(Hash.new) do |result, hint| 20 | hints_for_file = result[hint.file] ||= [] 21 | hints_for_file << hint 22 | result 23 | end 24 | end 25 | 26 | private 27 | 28 | def hints_for_base_path(base_path) 29 | base_path = File.expand_path(base_path) 30 | base_path = base_path + "/" unless base_path.end_with?("/") 31 | hints = linter_report.map do |hint| 32 | hint = hint.merge("file_full_path" => hint["file"], "file" => hint["file"].sub(base_path, "")) 33 | OpenStruct.new(hint) 34 | end 35 | end 36 | 37 | def report_file_content 38 | if report_file.kind_of?(IO) 39 | report_file.read 40 | else 41 | File.read(report_file) 42 | end 43 | end 44 | 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /lib/linterbot/patch.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class Patch 4 | 5 | MODIFIED_FILE_DIFF_REGEXP = /^@@ -\d+,\d+ \+(\d+),(\d+) @@.*$/ 6 | 7 | attr_accessor :patch_content 8 | 9 | def initialize(patch_content) 10 | @patch_content = patch_content || "" 11 | end 12 | 13 | def chunks_headers 14 | @chunks_headers ||= parse_chunks_headers 15 | end 16 | 17 | def additions_ranges 18 | chunks_headers.map do |diff_header, line_number| 19 | match = diff_header.match(MODIFIED_FILE_DIFF_REGEXP) 20 | line_start = match[1].to_i 21 | line_end = line_start + match[2].to_i 22 | [line_start...line_end, line_number] 23 | end 24 | end 25 | 26 | def additions_ranges_for_hint(hint) 27 | additions_ranges.select { |diff_range, line_number| diff_range.include?(hint.line) } 28 | end 29 | 30 | def included_in_patch?(hint) 31 | additions_ranges_for_hint(hint).count > 0 32 | end 33 | 34 | private 35 | 36 | def parse_chunks_headers 37 | patch_content 38 | .split("\n") 39 | .each_with_index 40 | .select { |line, line_number| line.start_with?("@@") } 41 | end 42 | 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /lib/linterbot/pull_request.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class PullRequest 4 | 5 | class AddedModifiedFiles 6 | 7 | def initialize(files) 8 | files_key_values = files.select { |file| file.status == "modified" || file.status == "added" } 9 | .map { |file| [file.filename, file]} 10 | .flatten 11 | @files_hash = Hash[*files_key_values] 12 | end 13 | 14 | def include?(filename) 15 | files_hash.include?(filename) 16 | end 17 | 18 | def [](filename) 19 | files_hash[filename] 20 | end 21 | 22 | private 23 | 24 | attr_accessor :files_hash 25 | 26 | end 27 | 28 | attr_accessor :github_client 29 | attr_accessor :repository 30 | attr_accessor :pull_request_number 31 | 32 | def initialize(repository, pull_request_number, github_client) 33 | @github_client = github_client 34 | @repository = repository 35 | @pull_request_number = pull_request_number 36 | end 37 | 38 | def added_and_modified_files 39 | @added_and_modified_files ||= AddedModifiedFiles.new(files) 40 | end 41 | 42 | def files 43 | @files ||= fetch_pull_request_files 44 | end 45 | 46 | def commits 47 | @commits ||= fetch_pull_request_commits 48 | end 49 | 50 | def commits_for_file(filename) 51 | commits.select { |commit| commit.files.map(&:filename).include?(filename) } 52 | end 53 | 54 | def file_for_filename(filename) 55 | files.select { |file| file.filename == filename } 56 | .first 57 | end 58 | 59 | def patch_for_file(filename) 60 | file = file_for_filename(filename) 61 | return file.patch if file 62 | end 63 | 64 | def newest_commit 65 | commits.first 66 | end 67 | 68 | private 69 | 70 | def fetch_pull_request_files 71 | github_client.pull_request_files(repository, pull_request_number) 72 | .map { |file| OpenStruct.new(file.to_h) } 73 | end 74 | 75 | def fetch_pull_request_commits 76 | github_client.pull_request_commits(repository, pull_request_number) 77 | .reverse 78 | .map do |commit| 79 | full_commit = github_client.commit(repository, commit.sha).to_h 80 | full_commit[:files].map! { |file| OpenStruct.new(file.to_h) } 81 | OpenStruct.new(full_commit) 82 | end 83 | end 84 | 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/linterbot/pull_request_analysis_result.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class PullRequestAnalysisResult 4 | 5 | attr_accessor :comments 6 | 7 | def initialize(comments) 8 | @comments = comments 9 | end 10 | 11 | def approved? 12 | comments.empty? 13 | end 14 | 15 | def violations? 16 | comments.count > 0 17 | end 18 | 19 | def violations_count 20 | comments.count 21 | end 22 | 23 | def summary 24 | if violations? 25 | "Total linter violations in pull request: #{comments.count}\n" + 26 | "Serious: #{serious_violations.count}\n" + 27 | "Warnings: #{warning_violations.count}" 28 | else 29 | ":+1: There are no linter violations." 30 | end 31 | end 32 | 33 | def serious_violations_count 34 | serious_violations.count 35 | end 36 | 37 | def serious_violations? 38 | serious_violations_count > 0 39 | end 40 | 41 | private 42 | 43 | def violations_with_severity(severity) 44 | comments.select { |violation| violation.severity == severity } 45 | end 46 | 47 | def serious_violations 48 | @serious_violations ||= violations_with_severity("Serious") 49 | end 50 | 51 | def warning_violations 52 | @warning_violations ||= violations_with_severity("Warning") 53 | end 54 | 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /lib/linterbot/pull_request_analyzer.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class PullRequestAnalyzer 4 | 5 | attr_accessor :pull_request 6 | attr_accessor :linter_report 7 | 8 | def initialize(linter_report, pull_request) 9 | @pull_request = pull_request 10 | @linter_report = linter_report 11 | end 12 | 13 | def analyze(base_path) 14 | comments = hints_in_pull_request(base_path) 15 | .each_pair 16 | .reduce([]) do |comments, (filename, hints)| 17 | comments + generate_comments(filename, hints) 18 | end 19 | PullRequestAnalysisResult.new(comments) 20 | end 21 | 22 | private 23 | 24 | def hints_in_pull_request(base_path) 25 | linter_report.hints_by_file(base_path) 26 | .select { |filename, hints| analyze_file?(filename) } 27 | end 28 | 29 | def analyze_file?(filename) 30 | added_and_modified_files.include?(filename) 31 | end 32 | 33 | def added_and_modified_files 34 | pull_request.added_and_modified_files 35 | end 36 | 37 | def generate_comments(filename, hints) 38 | pull_request_file_patch = pull_request.patch_for_file(filename) 39 | commits = pull_request.commits_for_file(filename) 40 | commits.map do |commit| 41 | CommentGenerator.new(filename, commit, pull_request_file_patch, commits.length) 42 | .generate_comments(hints) 43 | end.flatten 44 | end 45 | 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/linterbot/result_handler.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class ResultHandler 4 | 5 | attr_accessor :pull_request 6 | attr_accessor :github_client 7 | attr_accessor :commenter 8 | attr_accessor :approver 9 | 10 | def initialize(pull_request, commenter, approver) 11 | @pull_request = pull_request 12 | @commenter = commenter 13 | @approver = approver 14 | end 15 | 16 | def handle(result) 17 | result.comments.each { |comment| commenter.publish_comment(comment) } 18 | commenter.publish_summary(result.summary) 19 | if result.serious_violations? 20 | reject_pull_request(result.serious_violations_count) 21 | else 22 | approve_pull_request 23 | end 24 | end 25 | 26 | private 27 | 28 | def approve_pull_request 29 | approver.approve(pull_request.repository, pull_request.newest_commit.sha) 30 | end 31 | 32 | def reject_pull_request(serious_violations_count) 33 | approver.reject(pull_request.repository, pull_request.newest_commit.sha, serious_violations_count) 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/linterbot/runner.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Linterbot 4 | 5 | class Runner 6 | extend Forwardable 7 | 8 | def_delegators :@configuration, 9 | :project_base_path, 10 | :github_client, 11 | :linter_report_file, 12 | :commenter_class, 13 | :approver_class 14 | 15 | def initialize(configuration) 16 | @configuration = configuration 17 | end 18 | 19 | def run(repository, pull_request_number) 20 | pull_request = new_pull_request(repository, pull_request_number) 21 | mark_pull_request_status_as_pending(pull_request) 22 | analyze(pull_request) 23 | end 24 | 25 | private 26 | 27 | def linter_report 28 | @linter_report ||= LinterReport.new(linter_report_file) 29 | end 30 | 31 | def analyze(pull_request) 32 | analyzer = new_pull_request_analyzer(pull_request) 33 | handler = new_result_handler(pull_request) 34 | result = analyzer.analyze(project_base_path) 35 | handler.handle(result) 36 | rescue Exception => exception 37 | mark_pull_request_status_as_error(pull_request) 38 | raise exception 39 | end 40 | 41 | def new_pull_request(repository, pull_request_number) 42 | PullRequest.new(repository, pull_request_number, github_client) 43 | end 44 | 45 | def new_commenter(pull_request) 46 | commenter_class.new(pull_request.repository, pull_request.pull_request_number, github_client) 47 | end 48 | 49 | def new_pull_request_analyzer(pull_request) 50 | PullRequestAnalyzer.new(linter_report, pull_request) 51 | end 52 | 53 | def new_result_handler(pull_request) 54 | commenter = new_commenter(pull_request) 55 | ResultHandler.new(pull_request, commenter, approver) 56 | end 57 | 58 | def approver 59 | @approver ||= approver_class.new(github_client) 60 | end 61 | 62 | def mark_pull_request_status_as_pending(pull_request) 63 | approver.pending(pull_request.repository, pull_request.newest_commit.sha) 64 | end 65 | 66 | def mark_pull_request_status_as_error(pull_request) 67 | approver.error(pull_request.repository, pull_request.newest_commit.sha) 68 | end 69 | 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/linterbot/runner_configuration.rb: -------------------------------------------------------------------------------- 1 | require 'octokit' 2 | require 'yaml' 3 | require 'ostruct' 4 | require 'forwardable' 5 | 6 | module Linterbot 7 | 8 | class RunnerConfiguration 9 | extend Forwardable 10 | 11 | class MissingAttribute < Exception 12 | 13 | attr_accessor :attribute_name 14 | attr_accessor :fix_description 15 | 16 | def initialize(attribute_name, fix_description) 17 | super("Missing attribute #{attribute_name}") 18 | @attribute_name = attribute_name 19 | @fix_description = fix_description 20 | end 21 | 22 | end 23 | 24 | class InvalidGitHubCredentials < Exception 25 | end 26 | 27 | DEFAULT_PROJECT_BASE_PATH = './' 28 | DEFAULT_CONFIG_FILE_PATH = './.linterbot.yml' 29 | 30 | attr_accessor :github_client 31 | attr_accessor :commenter_class 32 | attr_accessor :approver_class 33 | attr_accessor :project_base_path 34 | attr_accessor :linter_report_file 35 | 36 | class << self 37 | 38 | def missing_github_access_token 39 | fix_description = "You must either define the enviromental variable 'GITHUB_ACCESS_TOKEN " + 40 | "or the attribute 'github_access_token' in the configuration file.'" 41 | MissingAttribute.new("GitHub access token", fix_description) 42 | end 43 | 44 | def load_config_file(config_file_path) 45 | return {} unless File.exist?(config_file_path) 46 | file_content = File.read(config_file_path) 47 | config = YAML.load(file_content) 48 | # YAML.load retunrs false if file could not be parsed 49 | # for example in the case of an empty file. 50 | if config 51 | Hash[config.each.map { |key, value| [key.to_sym, value] }] 52 | else 53 | STDERR.puts "WARNING: Linterbot configuration file '#{config_file_path}' " \ 54 | "has been ignored because is not a valid YAML file." 55 | return {} 56 | end 57 | end 58 | 59 | def default_configuration 60 | { 61 | project_base_path: File.expand_path(DEFAULT_PROJECT_BASE_PATH), 62 | linter_report_file: STDIN, 63 | commenter_class: GitHubPullRequestCommenter, 64 | approver_class: CommitApprover 65 | } 66 | end 67 | 68 | def configuration!(options) 69 | config_file_path = options.config_file_path || File.expand_path(DEFAULT_CONFIG_FILE_PATH) 70 | loaded_config = load_config_file(config_file_path) 71 | base_config = default_configuration.merge(loaded_config) 72 | 73 | github_access_token = ENV["GITHUB_ACCESS_TOKEN"] || base_config[:github_access_token] 74 | raise missing_github_access_token unless github_access_token 75 | github_client = Octokit::Client.new(access_token: github_access_token) 76 | validate_github_access!(github_client) 77 | 78 | configuration = new(github_client, base_config) 79 | configuration.project_base_path = options.project_base_path if options.project_base_path 80 | configuration.linter_report_file = options.linter_report_file_path if options.linter_report_file_path 81 | 82 | if options.dry_run 83 | configuration.commenter_class = TTYPullRequestCommenter 84 | configuration.approver_class = TTYApprover 85 | end 86 | 87 | configuration 88 | end 89 | 90 | def validate_github_access!(github_client) 91 | raise InvalidGitHubCredentials.new unless full_repo_access?(github_client) 92 | end 93 | 94 | def full_repo_access?(github_client) 95 | github_client.scopes.include?("repo") 96 | end 97 | 98 | end 99 | 100 | def initialize(github_client, options) 101 | @github_client = github_client 102 | @options = options 103 | @commenter_class = options[:commenter_class] 104 | @approver_class = options[:approver_class] 105 | @project_base_path = options[:project_base_path] 106 | @linter_report_file = options[:linter_report_file] 107 | end 108 | 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /lib/linterbot/tty_approver.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class TTYApprover 4 | 5 | def initialize(github_client) 6 | end 7 | 8 | def approve(repository, sha) 9 | puts approve_description 10 | end 11 | 12 | def reject(repository, sha, serious_violations_count) 13 | puts reject_description(serious_violations_count) 14 | end 15 | 16 | def pending(repository, sha) 17 | end 18 | 19 | def error(repository, sha) 20 | end 21 | 22 | private 23 | 24 | def approve_description 25 | "The pull request passed the linter validations!" 26 | end 27 | 28 | def reject_description(serious_violations_count) 29 | "There are #{serious_violations_count} serious linter violations that must be fixed!" 30 | end 31 | 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /lib/linterbot/tty_pull_request_commenter.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | 3 | class TTYPullRequestCommenter 4 | 5 | attr_accessor :repository 6 | attr_accessor :pull_request_number 7 | 8 | def initialize(repository, pull_request_number, github_client) 9 | @repository = repository 10 | @pull_request_number = pull_request_number 11 | end 12 | 13 | def publish_comment(comment) 14 | puts "#{repository}##{pull_request_number}" 15 | puts "#{comment.sha} - #{comment.file}" 16 | puts "#{comment.severity} - #{comment.message}" 17 | puts "" 18 | end 19 | 20 | def publish_summary(summary) 21 | puts summary 22 | end 23 | 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/linterbot/version.rb: -------------------------------------------------------------------------------- 1 | module Linterbot 2 | VERSION = "0.2.7" 3 | end 4 | -------------------------------------------------------------------------------- /linterbot.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'linterbot/version' 5 | require 'linterbot/description' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "linterbot" 9 | spec.version = Linterbot::VERSION 10 | spec.authors = ["Guido Marucci Blas"] 11 | spec.email = ["guidomb@gmail.com"] 12 | 13 | spec.summary = Linterbot::DESCRIPTION 14 | spec.description = %q{ 15 | A bot that parses swiftlint output and analyzes a GitHub pull request. 16 | Then for each linter violation it will make comment in the pull request diff on the 17 | line where the violation was made. 18 | } 19 | spec.homepage = "https://github.com/guidomb/linterbot" 20 | spec.license = "MIT" 21 | 22 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 23 | spec.bindir = "exe" 24 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 25 | spec.require_paths = ["lib"] 26 | 27 | spec.add_development_dependency "bundler", "~> 1.11" 28 | spec.add_development_dependency "rake", "~> 10.0" 29 | spec.add_development_dependency "rspec", "~> 3.0" 30 | spec.add_development_dependency "pry-byebug", "~> 3.3" 31 | 32 | spec.add_dependency "octokit", "~> 4.2" 33 | spec.add_dependency "commander", "~> 4.3" 34 | end 35 | -------------------------------------------------------------------------------- /spec/fixtures/swiftlint-report.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "reason" : "Files should have a single trailing newline.", 4 | "character" : null, 5 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SMOTrackerBridge\/BatteryLevelEventBus.swift", 6 | "rule_id" : "trailing_newline", 7 | "line" : 65, 8 | "severity" : "Warning", 9 | "type" : "Trailing Newline" 10 | }, 11 | { 12 | "reason" : "Files should have a single trailing newline.", 13 | "character" : null, 14 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SMOTrackerBridge\/BatteryService.swift", 15 | "rule_id" : "trailing_newline", 16 | "line" : 43, 17 | "severity" : "Warning", 18 | "type" : "Trailing Newline" 19 | }, 20 | { 21 | "reason" : "There should be no space before and one after any comma.", 22 | "character" : 41, 23 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SMOTrackerBridge\/SMOSkateSessionRecorder.swift", 24 | "rule_id" : "comma", 25 | "line" : 53, 26 | "severity" : "Warning", 27 | "type" : "Comma Spacing" 28 | }, 29 | { 30 | "reason" : "There should be no space before and one after any comma.", 31 | "character" : 41, 32 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SMOTrackerBridge\/SMOSkateSessionRecorder.swift", 33 | "rule_id" : "comma", 34 | "line" : 61, 35 | "severity" : "Warning", 36 | "type" : "Comma Spacing" 37 | }, 38 | { 39 | "reason" : "Files should have a single trailing newline.", 40 | "character" : null, 41 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SMOTrackerBridgeTests\/BatteryLevelEventBusSpec.swift", 42 | "rule_id" : "trailing_newline", 43 | "line" : 46, 44 | "severity" : "Warning", 45 | "type" : "Trailing Newline" 46 | }, 47 | { 48 | "reason" : "Function should have complexity 10 or less: currently complexity equals 14", 49 | "character" : 14, 50 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SMOTrackerBridgeTests\/TrackerEventBusSpec.swift", 51 | "rule_id" : "cyclomatic_complexity", 52 | "line" : 18, 53 | "severity" : "Warning", 54 | "type" : "Cyclomatic Complexity" 55 | }, 56 | { 57 | "reason" : "Function body should span 30 lines or less excluding comments and whitespace: currently spans 120 lines", 58 | "character" : 5, 59 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/AppDelegate.swift", 60 | "rule_id" : "function_body_length", 61 | "line" : 62, 62 | "severity" : "Warning", 63 | "type" : "Function Body Length" 64 | }, 65 | { 66 | "reason" : "Files should have a single trailing newline.", 67 | "character" : null, 68 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/BatteryMetricsBootstrapper.swift", 69 | "rule_id" : "trailing_newline", 70 | "line" : 25, 71 | "severity" : "Warning", 72 | "type" : "Trailing Newline" 73 | }, 74 | { 75 | "reason" : "TODOs and FIXMEs should be avoided.", 76 | "character" : 12, 77 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Bootstrappers\/LocalWebServerBootstrapper.swift", 78 | "rule_id" : "todo", 79 | "line" : 27, 80 | "severity" : "Warning", 81 | "type" : "Todo" 82 | }, 83 | { 84 | "reason" : "Files should have a single trailing newline.", 85 | "character" : null, 86 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Bootstrappers\/TrickPersistorBootstrapper.swift", 87 | "rule_id" : "trailing_newline", 88 | "line" : 29, 89 | "severity" : "Warning", 90 | "type" : "Trailing Newline" 91 | }, 92 | { 93 | "reason" : "Operators should be surrounded by a single whitespace when defining them.", 94 | "character" : 8, 95 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Extensions\/ArrayExtension.swift", 96 | "rule_id" : "operator_whitespace", 97 | "line" : 10, 98 | "severity" : "Warning", 99 | "type" : "Operator Function Whitespace" 100 | }, 101 | { 102 | "reason" : "Files should have a single trailing newline.", 103 | "character" : null, 104 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Extensions\/NSDataExtension.swift", 105 | "rule_id" : "trailing_newline", 106 | "line" : 27, 107 | "severity" : "Warning", 108 | "type" : "Trailing Newline" 109 | }, 110 | { 111 | "reason" : "Documented declarations should be valid.", 112 | "character" : 5, 113 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Extensions\/NSTimerExtension.swift", 114 | "rule_id" : "valid_docs", 115 | "line" : 21, 116 | "severity" : "Warning", 117 | "type" : "Valid Docs" 118 | }, 119 | { 120 | "reason" : "Documented declarations should be valid.", 121 | "character" : 5, 122 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Extensions\/NSTimerExtension.swift", 123 | "rule_id" : "valid_docs", 124 | "line" : 36, 125 | "severity" : "Warning", 126 | "type" : "Valid Docs" 127 | }, 128 | { 129 | "reason" : "Documented declarations should be valid.", 130 | "character" : 5, 131 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Extensions\/NSTimerExtension.swift", 132 | "rule_id" : "valid_docs", 133 | "line" : 47, 134 | "severity" : "Warning", 135 | "type" : "Valid Docs" 136 | }, 137 | { 138 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 139 | "character" : 70, 140 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Extensions\/StringExtension.swift", 141 | "rule_id" : "legacy_constructor", 142 | "line" : 20, 143 | "severity" : "Warning", 144 | "type" : "Legacy Constructor" 145 | }, 146 | { 147 | "reason" : "Documented declarations should be valid.", 148 | "character" : 12, 149 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Extensions\/StringExtension.swift", 150 | "rule_id" : "valid_docs", 151 | "line" : 49, 152 | "severity" : "Warning", 153 | "type" : "Valid Docs" 154 | }, 155 | { 156 | "reason" : "Files should have a single trailing newline.", 157 | "character" : null, 158 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/LocalNotificationBootstrapper.swift", 159 | "rule_id" : "trailing_newline", 160 | "line" : 53, 161 | "severity" : "Warning", 162 | "type" : "Trailing Newline" 163 | }, 164 | { 165 | "reason" : "Function should have 5 parameters or less: it currently has 7", 166 | "character" : 12, 167 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Models\/SkateSession.swift", 168 | "rule_id" : "function_parameter_count", 169 | "line" : 32, 170 | "severity" : "Warning", 171 | "type" : "Function Parameter Count" 172 | }, 173 | { 174 | "reason" : "Function should have 5 parameters or less: it currently has 8", 175 | "character" : 12, 176 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Models\/SkateTrick.swift", 177 | "rule_id" : "function_parameter_count", 178 | "line" : 22, 179 | "severity" : "Warning", 180 | "type" : "Function Parameter Count" 181 | }, 182 | { 183 | "reason" : "Function should have 5 parameters or less: it currently has 8", 184 | "character" : 12, 185 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Models\/SkateTrick.swift", 186 | "rule_id" : "function_parameter_count", 187 | "line" : 41, 188 | "severity" : "Warning", 189 | "type" : "Function Parameter Count" 190 | }, 191 | { 192 | "reason" : "Function should have 5 parameters or less: it currently has 8", 193 | "character" : 12, 194 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Models\/SkateTrick.swift", 195 | "rule_id" : "function_parameter_count", 196 | "line" : 45, 197 | "severity" : "Warning", 198 | "type" : "Function Parameter Count" 199 | }, 200 | { 201 | "reason" : "Files should have a single trailing newline.", 202 | "character" : null, 203 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Repositories\/Local\/LocalFileSystemManager.swift", 204 | "rule_id" : "trailing_newline", 205 | "line" : 42, 206 | "severity" : "Warning", 207 | "type" : "Trailing Newline" 208 | }, 209 | { 210 | "reason" : "Operators should be surrounded by a single whitespace when defining them.", 211 | "character" : 8, 212 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Repositories\/Local\/LocalFileSystemManager.swift", 213 | "rule_id" : "operator_whitespace", 214 | "line" : 12, 215 | "severity" : "Warning", 216 | "type" : "Operator Function Whitespace" 217 | }, 218 | { 219 | "reason" : "Function should have 5 parameters or less: it currently has 7", 220 | "character" : 13, 221 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Repositories\/Parse\/PFObject\/SkateSession+PFObject.swift", 222 | "rule_id" : "function_parameter_count", 223 | "line" : 29, 224 | "severity" : "Warning", 225 | "type" : "Function Parameter Count" 226 | }, 227 | { 228 | "reason" : "Function should have 5 parameters or less: it currently has 7", 229 | "character" : 13, 230 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Repositories\/Parse\/PFObject\/SkateTrick+PFObject.swift", 231 | "rule_id" : "function_parameter_count", 232 | "line" : 41, 233 | "severity" : "Warning", 234 | "type" : "Function Parameter Count" 235 | }, 236 | { 237 | "reason" : "Files should have a single trailing newline.", 238 | "character" : null, 239 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Repositories\/TrickSensorDataRepository.swift", 240 | "rule_id" : "trailing_newline", 241 | "line" : 17, 242 | "severity" : "Warning", 243 | "type" : "Trailing Newline" 244 | }, 245 | { 246 | "reason" : "Files should have a single trailing newline.", 247 | "character" : null, 248 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Services\/BatteryMetricsService.swift", 249 | "rule_id" : "trailing_newline", 250 | "line" : 26, 251 | "severity" : "Warning", 252 | "type" : "Trailing Newline" 253 | }, 254 | { 255 | "reason" : "Files should have a single trailing newline.", 256 | "character" : null, 257 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Services\/CounterService.swift", 258 | "rule_id" : "trailing_newline", 259 | "line" : 49, 260 | "severity" : "Warning", 261 | "type" : "Trailing Newline" 262 | }, 263 | { 264 | "reason" : "Files should have a single trailing newline.", 265 | "character" : null, 266 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Services\/GeoTaggedSkateSessionRecorder.swift", 267 | "rule_id" : "trailing_newline", 268 | "line" : 60, 269 | "severity" : "Warning", 270 | "type" : "Trailing Newline" 271 | }, 272 | { 273 | "reason" : "TODOs and FIXMEs should be avoided.", 274 | "character" : 24, 275 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Services\/ReplayableSkateSessionRecorder.swift", 276 | "rule_id" : "todo", 277 | "line" : 99, 278 | "severity" : "Warning", 279 | "type" : "Todo" 280 | }, 281 | { 282 | "reason" : "Files should have a single trailing newline.", 283 | "character" : null, 284 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Services\/TrickPersistor.swift", 285 | "rule_id" : "trailing_newline", 286 | "line" : 58, 287 | "severity" : "Warning", 288 | "type" : "Trailing Newline" 289 | }, 290 | { 291 | "reason" : "Opening braces should be preceded by a single space and on the same line as the declaration.", 292 | "character" : 103, 293 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/Services\/TrickPersistor.swift", 294 | "rule_id" : "opening_brace", 295 | "line" : 38, 296 | "severity" : "Warning", 297 | "type" : "Opening Brace Spacing" 298 | }, 299 | { 300 | "reason" : "Files should have a single trailing newline.", 301 | "character" : null, 302 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/DeveloperSettings\/DeveloperSettingsView.swift", 303 | "rule_id" : "trailing_newline", 304 | "line" : 50, 305 | "severity" : "Warning", 306 | "type" : "Trailing Newline" 307 | }, 308 | { 309 | "reason" : "Files should have a single trailing newline.", 310 | "character" : null, 311 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/DeveloperSettings\/DeveloperSettingsViewController.swift", 312 | "rule_id" : "trailing_newline", 313 | "line" : 209, 314 | "severity" : "Warning", 315 | "type" : "Trailing Newline" 316 | }, 317 | { 318 | "reason" : "Files should have a single trailing newline.", 319 | "character" : null, 320 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/DeveloperSettings\/LoggerViewController.swift", 321 | "rule_id" : "trailing_newline", 322 | "line" : 39, 323 | "severity" : "Warning", 324 | "type" : "Trailing Newline" 325 | }, 326 | { 327 | "reason" : "Files should have a single trailing newline.", 328 | "character" : null, 329 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/DeveloperSettingsService.swift", 330 | "rule_id" : "trailing_newline", 331 | "line" : 85, 332 | "severity" : "Warning", 333 | "type" : "Trailing Newline" 334 | }, 335 | { 336 | "reason" : "Files should have a single trailing newline.", 337 | "character" : null, 338 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/NavigationController.swift", 339 | "rule_id" : "trailing_newline", 340 | "line" : 26, 341 | "severity" : "Warning", 342 | "type" : "Trailing Newline" 343 | }, 344 | { 345 | "reason" : "Files should have a single trailing newline.", 346 | "character" : null, 347 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/OnBoarding\/AvailableTrackers\/AvailableTrackersCell\/AvailableTrackerCell.swift", 348 | "rule_id" : "trailing_newline", 349 | "line" : 33, 350 | "severity" : "Warning", 351 | "type" : "Trailing Newline" 352 | }, 353 | { 354 | "reason" : "Files should have a single trailing newline.", 355 | "character" : null, 356 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/OnBoarding\/AvailableTrackers\/AvailableTrackersCell\/AvailableTrackerCellViewModel.swift", 357 | "rule_id" : "trailing_newline", 358 | "line" : 23, 359 | "severity" : "Warning", 360 | "type" : "Trailing Newline" 361 | }, 362 | { 363 | "reason" : "Files should have a single trailing newline.", 364 | "character" : null, 365 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/OnBoarding\/AvailableTrackers\/AvailableTrackersView.swift", 366 | "rule_id" : "trailing_newline", 367 | "line" : 60, 368 | "severity" : "Warning", 369 | "type" : "Trailing Newline" 370 | }, 371 | { 372 | "reason" : "Files should have a single trailing newline.", 373 | "character" : null, 374 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/OnBoarding\/AvailableTrackers\/AvailableTrackersViewModel.swift", 375 | "rule_id" : "trailing_newline", 376 | "line" : 63, 377 | "severity" : "Warning", 378 | "type" : "Trailing Newline" 379 | }, 380 | { 381 | "reason" : "Files should have a single trailing newline.", 382 | "character" : null, 383 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/OnBoarding\/ChargeBattery\/ChargeBatteryViewController.swift", 384 | "rule_id" : "trailing_newline", 385 | "line" : 75, 386 | "severity" : "Warning", 387 | "type" : "Trailing Newline" 388 | }, 389 | { 390 | "reason" : "Files should have a single trailing newline.", 391 | "character" : null, 392 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/OnBoarding\/TurningOnTracker\/TurningOnTrackerController.swift", 393 | "rule_id" : "trailing_newline", 394 | "line" : 82, 395 | "severity" : "Warning", 396 | "type" : "Trailing Newline" 397 | }, 398 | { 399 | "reason" : "Files should have a single trailing newline.", 400 | "character" : null, 401 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/OnBoarding\/Welcome\/WelcomeView.swift", 402 | "rule_id" : "trailing_newline", 403 | "line" : 66, 404 | "severity" : "Warning", 405 | "type" : "Trailing Newline" 406 | }, 407 | { 408 | "reason" : "Files should have a single trailing newline.", 409 | "character" : null, 410 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/OnBoarding\/Welcome\/WelcomeViewModel.swift", 411 | "rule_id" : "trailing_newline", 412 | "line" : 35, 413 | "severity" : "Warning", 414 | "type" : "Trailing Newline" 415 | }, 416 | { 417 | "reason" : "Files should have a single trailing newline.", 418 | "character" : null, 419 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/RecordTrick\/RecordTrickController.swift", 420 | "rule_id" : "trailing_newline", 421 | "line" : 415, 422 | "severity" : "Warning", 423 | "type" : "Trailing Newline" 424 | }, 425 | { 426 | "reason" : "File should contain 400 lines or less: currently contains 415", 427 | "character" : null, 428 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/RecordTrick\/RecordTrickController.swift", 429 | "rule_id" : "file_length", 430 | "line" : 415, 431 | "severity" : "Warning", 432 | "type" : "File Line Length" 433 | }, 434 | { 435 | "reason" : "Files should have a single trailing newline.", 436 | "character" : null, 437 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/RecordTrick\/RecordTrickStatsViewModel.swift", 438 | "rule_id" : "trailing_newline", 439 | "line" : 75, 440 | "severity" : "Warning", 441 | "type" : "Trailing Newline" 442 | }, 443 | { 444 | "reason" : "Function body should span 30 lines or less excluding comments and whitespace: currently spans 50 lines", 445 | "character" : 12, 446 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/RecordTrick\/RecordTrickViewModel.swift", 447 | "rule_id" : "function_body_length", 448 | "line" : 88, 449 | "severity" : "Warning", 450 | "type" : "Function Body Length" 451 | }, 452 | { 453 | "reason" : "Struct-scoped constants are preferred over legacy global constants.", 454 | "character" : 27, 455 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/Session\/SessionDetail\/SessionDetailNavigationTitle.swift", 456 | "rule_id" : "legacy_constant", 457 | "line" : 25, 458 | "severity" : "Warning", 459 | "type" : "Legacy Constant" 460 | }, 461 | { 462 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 463 | "character" : 53, 464 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/Session\/SessionDetail\/SessionDetailNavigationTitle.swift", 465 | "rule_id" : "legacy_constructor", 466 | "line" : 45, 467 | "severity" : "Warning", 468 | "type" : "Legacy Constructor" 469 | }, 470 | { 471 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 472 | "character" : 56, 473 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/Session\/SessionDetail\/SessionDetailNavigationTitle.swift", 474 | "rule_id" : "legacy_constructor", 475 | "line" : 46, 476 | "severity" : "Warning", 477 | "type" : "Legacy Constructor" 478 | }, 479 | { 480 | "reason" : "Lines should not have trailing semicolons.", 481 | "character" : 48, 482 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/Session\/SessionDetail\/SessionDetailViewModel.swift", 483 | "rule_id" : "trailing_semicolon", 484 | "line" : 87, 485 | "severity" : "Warning", 486 | "type" : "Trailing Semicolon" 487 | }, 488 | { 489 | "reason" : "Lines should not have trailing semicolons.", 490 | "character" : 48, 491 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/Session\/SessionDetail\/SessionTrickList\/Cells\/SessionTrickViewModel.swift", 492 | "rule_id" : "trailing_semicolon", 493 | "line" : 105, 494 | "severity" : "Warning", 495 | "type" : "Trailing Semicolon" 496 | }, 497 | { 498 | "reason" : "Struct-scoped constants are preferred over legacy global constants.", 499 | "character" : 24, 500 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/Session\/SessionTrickAnalizer\/SessionTrickAnalizerViewController.swift", 501 | "rule_id" : "legacy_constant", 502 | "line" : 155, 503 | "severity" : "Warning", 504 | "type" : "Legacy Constant" 505 | }, 506 | { 507 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 508 | "character" : 26, 509 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/TrackerButton.swift", 510 | "rule_id" : "legacy_constructor", 511 | "line" : 15, 512 | "severity" : "Warning", 513 | "type" : "Legacy Constructor" 514 | }, 515 | { 516 | "reason" : "Documented declarations should be valid.", 517 | "character" : 13, 518 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/TrackerSettings\/AddTracker\/AddTrackerCell\/TrackerPickerViewModel.swift", 519 | "rule_id" : "valid_docs", 520 | "line" : 75, 521 | "severity" : "Warning", 522 | "type" : "Valid Docs" 523 | }, 524 | { 525 | "reason" : "Function body should span 30 lines or less excluding comments and whitespace: currently spans 34 lines", 526 | "character" : 12, 527 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/TrackerSettings\/AddTracker\/AddTrackerViewModel.swift", 528 | "rule_id" : "function_body_length", 529 | "line" : 58, 530 | "severity" : "Warning", 531 | "type" : "Function Body Length" 532 | }, 533 | { 534 | "reason" : "Closing brace with closing parenthesis should not have any whitespaces in the middle.", 535 | "character" : 21, 536 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/TrackerSettings\/AddTracker\/AddTrackerViewModel.swift", 537 | "rule_id" : "closing_brace", 538 | "line" : 79, 539 | "severity" : "Warning", 540 | "type" : "Closing Brace Spacing" 541 | }, 542 | { 543 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 544 | "character" : 39, 545 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/TrackerSettings\/TrackerCell\/PairedTrackerCell.swift", 546 | "rule_id" : "legacy_constructor", 547 | "line" : 68, 548 | "severity" : "Warning", 549 | "type" : "Legacy Constructor" 550 | }, 551 | { 552 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 553 | "character" : 42, 554 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/TrackerSettings\/TrackerCell\/PairedTrackerCell.swift", 555 | "rule_id" : "legacy_constructor", 556 | "line" : 70, 557 | "severity" : "Warning", 558 | "type" : "Legacy Constructor" 559 | }, 560 | { 561 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 562 | "character" : 40, 563 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIComponents\/TrackerSettings\/TrackerSettingsController.swift", 564 | "rule_id" : "legacy_constructor", 565 | "line" : 86, 566 | "severity" : "Warning", 567 | "type" : "Legacy Constructor" 568 | }, 569 | { 570 | "reason" : "Files should have a single trailing newline.", 571 | "character" : null, 572 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UIDeviceExtension.swift", 573 | "rule_id" : "trailing_newline", 574 | "line" : 17, 575 | "severity" : "Warning", 576 | "type" : "Trailing Newline" 577 | }, 578 | { 579 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 580 | "character" : 38, 581 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UIImageExtension.swift", 582 | "rule_id" : "legacy_constructor", 583 | "line" : 63, 584 | "severity" : "Warning", 585 | "type" : "Legacy Constructor" 586 | }, 587 | { 588 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 589 | "character" : 36, 590 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UIImageExtension.swift", 591 | "rule_id" : "legacy_constructor", 592 | "line" : 64, 593 | "severity" : "Warning", 594 | "type" : "Legacy Constructor" 595 | }, 596 | { 597 | "reason" : "Files should have a single trailing newline.", 598 | "character" : null, 599 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UINavigationItemExtension.swift", 600 | "rule_id" : "trailing_newline", 601 | "line" : 15, 602 | "severity" : "Warning", 603 | "type" : "Trailing Newline" 604 | }, 605 | { 606 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 607 | "character" : 23, 608 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UITabBarExtension.swift", 609 | "rule_id" : "legacy_constructor", 610 | "line" : 31, 611 | "severity" : "Warning", 612 | "type" : "Legacy Constructor" 613 | }, 614 | { 615 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 616 | "character" : 38, 617 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UITabBarExtension.swift", 618 | "rule_id" : "legacy_constructor", 619 | "line" : 33, 620 | "severity" : "Warning", 621 | "type" : "Legacy Constructor" 622 | }, 623 | { 624 | "reason" : "Files should have a single trailing newline.", 625 | "character" : null, 626 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UITextViewExtension.swift", 627 | "rule_id" : "trailing_newline", 628 | "line" : 18, 629 | "severity" : "Warning", 630 | "type" : "Trailing Newline" 631 | }, 632 | { 633 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 634 | "character" : 40, 635 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UIViewExtension.swift", 636 | "rule_id" : "legacy_constructor", 637 | "line" : 24, 638 | "severity" : "Warning", 639 | "type" : "Legacy Constructor" 640 | }, 641 | { 642 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 643 | "character" : 40, 644 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UIViewExtension.swift", 645 | "rule_id" : "legacy_constructor", 646 | "line" : 30, 647 | "severity" : "Warning", 648 | "type" : "Legacy Constructor" 649 | }, 650 | { 651 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 652 | "character" : 40, 653 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UIViewExtension.swift", 654 | "rule_id" : "legacy_constructor", 655 | "line" : 37, 656 | "severity" : "Warning", 657 | "type" : "Legacy Constructor" 658 | }, 659 | { 660 | "reason" : "Swift constructors are preferred over legacy convenience functions.", 661 | "character" : 40, 662 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UIViewExtension.swift", 663 | "rule_id" : "legacy_constructor", 664 | "line" : 43, 665 | "severity" : "Warning", 666 | "type" : "Legacy Constructor" 667 | }, 668 | { 669 | "reason" : "Documented declarations should be valid.", 670 | "character" : 5, 671 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/UIExtensions\/UIViewExtension.swift", 672 | "rule_id" : "valid_docs", 673 | "line" : 53, 674 | "severity" : "Warning", 675 | "type" : "Valid Docs" 676 | }, 677 | { 678 | "reason" : "Files should have a single trailing newline.", 679 | "character" : null, 680 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/VirtualComponents\/VirtualBatteryService.swift", 681 | "rule_id" : "trailing_newline", 682 | "line" : 42, 683 | "severity" : "Warning", 684 | "type" : "Trailing Newline" 685 | }, 686 | { 687 | "reason" : "Files should have a single trailing newline.", 688 | "character" : null, 689 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/Syrmo\/VirtualComponents\/VirtualSkateRecorder.swift", 690 | "rule_id" : "trailing_newline", 691 | "line" : 137, 692 | "severity" : "Warning", 693 | "type" : "Trailing Newline" 694 | }, 695 | { 696 | "reason" : "Files should have a single trailing newline.", 697 | "character" : null, 698 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Extensions\/StringExtensionSpec.swift", 699 | "rule_id" : "trailing_newline", 700 | "line" : 34, 701 | "severity" : "Warning", 702 | "type" : "Trailing Newline" 703 | }, 704 | { 705 | "reason" : "Files should have a single trailing newline.", 706 | "character" : null, 707 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Mocks\/Services\/MockApplicationState.swift", 708 | "rule_id" : "trailing_newline", 709 | "line" : 26, 710 | "severity" : "Warning", 711 | "type" : "Trailing Newline" 712 | }, 713 | { 714 | "reason" : "Files should have a single trailing newline.", 715 | "character" : null, 716 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Mocks\/Services\/MockBatteryService.swift", 717 | "rule_id" : "trailing_newline", 718 | "line" : 34, 719 | "severity" : "Warning", 720 | "type" : "Trailing Newline" 721 | }, 722 | { 723 | "reason" : "Closing brace with closing parenthesis should not have any whitespaces in the middle.", 724 | "character" : 118, 725 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Services\/SkateSessionBuilderSpec.swift", 726 | "rule_id" : "closing_brace", 727 | "line" : 51, 728 | "severity" : "Warning", 729 | "type" : "Closing Brace Spacing" 730 | }, 731 | { 732 | "reason" : "Closing brace with closing parenthesis should not have any whitespaces in the middle.", 733 | "character" : 118, 734 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Services\/SkateSessionBuilderSpec.swift", 735 | "rule_id" : "closing_brace", 736 | "line" : 52, 737 | "severity" : "Warning", 738 | "type" : "Closing Brace Spacing" 739 | }, 740 | { 741 | "reason" : "Closing brace with closing parenthesis should not have any whitespaces in the middle.", 742 | "character" : 91, 743 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Services\/SkateSessionBuilderSpec.swift", 744 | "rule_id" : "closing_brace", 745 | "line" : 70, 746 | "severity" : "Warning", 747 | "type" : "Closing Brace Spacing" 748 | }, 749 | { 750 | "reason" : "Files should have a single trailing newline.", 751 | "character" : null, 752 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Services\/TrackerConnectionLostNotificationServiceSpec.swift", 753 | "rule_id" : "trailing_newline", 754 | "line" : 95, 755 | "severity" : "Warning", 756 | "type" : "Trailing Newline" 757 | }, 758 | { 759 | "reason" : "Type name should be between 3 and 40 characters long: 'TrackerConnectionLostNotificationServiceSpec'", 760 | "character" : 14, 761 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Services\/TrackerConnectionLostNotificationServiceSpec.swift", 762 | "rule_id" : "type_name", 763 | "line" : 15, 764 | "severity" : "Warning", 765 | "type" : "Type Name" 766 | }, 767 | { 768 | "reason" : "Function body should span 30 lines or less excluding comments and whitespace: currently spans 40 lines", 769 | "character" : 21, 770 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/Services\/TrackerConnectionLostNotificationServiceSpec.swift", 771 | "rule_id" : "function_body_length", 772 | "line" : 17, 773 | "severity" : "Warning", 774 | "type" : "Function Body Length" 775 | }, 776 | { 777 | "reason" : "Files should have a single trailing newline.", 778 | "character" : null, 779 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/UIComponents\/OnBoarding\/AvailableTrackers\/AvailableTrackersViewModelSpec.swift", 780 | "rule_id" : "trailing_newline", 781 | "line" : 78, 782 | "severity" : "Warning", 783 | "type" : "Trailing Newline" 784 | }, 785 | { 786 | "reason" : "Function body should span 30 lines or less excluding comments and whitespace: currently spans 34 lines", 787 | "character" : 21, 788 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/UIComponents\/OnBoarding\/AvailableTrackers\/AvailableTrackersViewModelSpec.swift", 789 | "rule_id" : "function_body_length", 790 | "line" : 16, 791 | "severity" : "Warning", 792 | "type" : "Function Body Length" 793 | }, 794 | { 795 | "reason" : "Files should have a single trailing newline.", 796 | "character" : null, 797 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/UIComponents\/RecordTrick\/RecordTrickViewModelSpec.swift", 798 | "rule_id" : "trailing_newline", 799 | "line" : 603, 800 | "severity" : "Warning", 801 | "type" : "Trailing Newline" 802 | }, 803 | { 804 | "reason" : "File should contain 400 lines or less: currently contains 603", 805 | "character" : null, 806 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/SyrmoTests\/UIComponents\/RecordTrick\/RecordTrickViewModelSpec.swift", 807 | "rule_id" : "file_length", 808 | "line" : 603, 809 | "severity" : "Warning", 810 | "type" : "File Line Length" 811 | }, 812 | { 813 | "reason" : "Files should have a single trailing newline.", 814 | "character" : null, 815 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/ViewModel\/BindingAnalyzer.swift", 816 | "rule_id" : "trailing_newline", 817 | "line" : 102, 818 | "severity" : "Warning", 819 | "type" : "Trailing Newline" 820 | }, 821 | { 822 | "reason" : "Opening braces should be preceded by a single space and on the same line as the declaration.", 823 | "character" : 40, 824 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/ViewModel\/BindingAnalyzer.swift", 825 | "rule_id" : "opening_brace", 826 | "line" : 49, 827 | "severity" : "Warning", 828 | "type" : "Opening Brace Spacing" 829 | }, 830 | { 831 | "reason" : "TODOs and FIXMEs should be avoided.", 832 | "character" : 4, 833 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/ViewModel\/Extensions\/LazyProperties.swift", 834 | "rule_id" : "todo", 835 | "line" : 12, 836 | "severity" : "Warning", 837 | "type" : "Todo" 838 | }, 839 | { 840 | "reason" : "TODOs and FIXMEs should be avoided.", 841 | "character" : 8, 842 | "file" : "\/Users\/guidomb\/Documents\/projects\/syrmo\/syrmo-ios\/ViewModel\/Extensions\/UIKit+ReactiveCocoa.swift", 843 | "rule_id" : "todo", 844 | "line" : 62, 845 | "severity" : "Warning", 846 | "type" : "Todo" 847 | } 848 | ] 849 | -------------------------------------------------------------------------------- /spec/linterbot_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Linterbot do 4 | it 'has a version number' do 5 | expect(Linterbot::VERSION).not_to be nil 6 | end 7 | 8 | it 'does something useful' do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'linterbot' 3 | --------------------------------------------------------------------------------