lint_trappings 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +21 -0
  3. data/lib/lint_trappings.rb +17 -0
  4. data/lib/lint_trappings/application.rb +138 -0
  5. data/lib/lint_trappings/arguments_parser.rb +145 -0
  6. data/lib/lint_trappings/cli.rb +61 -0
  7. data/lib/lint_trappings/command/base.rb +36 -0
  8. data/lib/lint_trappings/command/display_documentation.rb +65 -0
  9. data/lib/lint_trappings/command/display_formatters.rb +14 -0
  10. data/lib/lint_trappings/command/display_help.rb +8 -0
  11. data/lib/lint_trappings/command/display_linters.rb +24 -0
  12. data/lib/lint_trappings/command/display_version.rb +14 -0
  13. data/lib/lint_trappings/command/scan.rb +19 -0
  14. data/lib/lint_trappings/configuration.rb +94 -0
  15. data/lib/lint_trappings/configuration_loader.rb +98 -0
  16. data/lib/lint_trappings/configuration_resolver.rb +49 -0
  17. data/lib/lint_trappings/document.rb +45 -0
  18. data/lib/lint_trappings/errors.rb +127 -0
  19. data/lib/lint_trappings/executable.rb +26 -0
  20. data/lib/lint_trappings/file_finder.rb +171 -0
  21. data/lib/lint_trappings/formatter/base.rb +67 -0
  22. data/lib/lint_trappings/formatter/checkstyle.rb +34 -0
  23. data/lib/lint_trappings/formatter/default.rb +99 -0
  24. data/lib/lint_trappings/formatter/json.rb +62 -0
  25. data/lib/lint_trappings/formatter_forwarder.rb +23 -0
  26. data/lib/lint_trappings/formatter_loader.rb +45 -0
  27. data/lib/lint_trappings/lint.rb +37 -0
  28. data/lib/lint_trappings/linter.rb +182 -0
  29. data/lib/lint_trappings/linter_configuration_validator.rb +42 -0
  30. data/lib/lint_trappings/linter_loader.rb +44 -0
  31. data/lib/lint_trappings/linter_plugin.rb +35 -0
  32. data/lib/lint_trappings/linter_selector.rb +120 -0
  33. data/lib/lint_trappings/location.rb +39 -0
  34. data/lib/lint_trappings/output.rb +118 -0
  35. data/lib/lint_trappings/preprocessor.rb +41 -0
  36. data/lib/lint_trappings/rake_task.rb +145 -0
  37. data/lib/lint_trappings/report.rb +58 -0
  38. data/lib/lint_trappings/runner.rb +161 -0
  39. data/lib/lint_trappings/spec.rb +12 -0
  40. data/lib/lint_trappings/spec/directory_helpers.rb +22 -0
  41. data/lib/lint_trappings/spec/indentation_helpers.rb +7 -0
  42. data/lib/lint_trappings/spec/matchers/report_lint_matcher.rb +169 -0
  43. data/lib/lint_trappings/spec/shared_contexts/linter_shared_context.rb +35 -0
  44. data/lib/lint_trappings/utils.rb +123 -0
  45. data/lib/lint_trappings/version.rb +4 -0
  46. metadata +117 -0
@@ -0,0 +1,26 @@
1
+ require 'lint_trappings/cli'
2
+
3
+ # Helper module for creating a LintTrappings-powered executable.
4
+ module LintTrappings::Executable
5
+ module_function
6
+
7
+ # Runs the command line interface.
8
+ #
9
+ # This should be called from your application executable, like so:
10
+ #
11
+ # @example
12
+ # #!/usr/bin/env ruby
13
+ # require 'my_linter_gem'
14
+ # require 'lint_trappings/executable'
15
+ # LintTrappings::Executable.run(MyLinter::Application, STDOUT, STDERR, ARGV)
16
+ #
17
+ # @param application_class [Class]
18
+ # @param stdout [IO] output stream
19
+ # @param arguments [Array<String>] command line arguments
20
+ def run(application_class, stdout, stderr, arguments)
21
+ output = LintTrappings::Output.new(stdout)
22
+ error_output = LintTrappings::Output.new(stderr)
23
+ application = application_class.new(output)
24
+ exit LintTrappings::Cli.new(application, error_output).run(arguments)
25
+ end
26
+ end
@@ -0,0 +1,171 @@
1
+ require 'find'
2
+
3
+ module LintTrappings
4
+ # Finds files that should be linted.
5
+ module FileFinder
6
+ class << self
7
+ # Return list of lintable files given the specified set of paths and glob
8
+ # pattern search criteria.
9
+ #
10
+ # The distinction between paths and globs is so files with a `*` in their
11
+ # name can still be matched if necessary without treating them as a glob
12
+ # pattern.
13
+ #
14
+ # @param options [Hash]
15
+ # @option options :included_paths [Array<String>] files/directories to include
16
+ # @option options :excluded_paths [Array<String>] files/directories to exclude
17
+ # @option options :included_patterns [Array<String>] glob patterns to include
18
+ # @option options :excluded_patterns [Array<String>] glob patterns to exclude
19
+ # @option options :file_extensions [Array<String>] extensions of files to
20
+ # include when searching directories specified by included_paths
21
+ #
22
+ # @raise [InvalidFilePathError] if included_paths/excluded_paths don't exist
23
+ # @raise [InvalidFilePatternError] if any included_pattern doesn't match any files
24
+ #
25
+ # @return [Array<String>] list of matching files in lexicographic order
26
+ def find(options)
27
+ included_paths = options.fetch(:included_paths, []).map { |p| normalize_path(p) }
28
+ excluded_paths = options.fetch(:excluded_paths, []).map { |p| normalize_path(p) }
29
+ included_patterns = options.fetch(:included_patterns, []).map { |p| normalize_path(p) }
30
+ excluded_patterns = options.fetch(:excluded_patterns, []).map { |p| normalize_path(p) }
31
+ allowed_extensions = options.fetch(:allowed_extensions)
32
+
33
+ included_files = expand_paths(included_paths, included_patterns, allowed_extensions)
34
+ matching_files = filter_files(included_files, excluded_paths, excluded_patterns)
35
+
36
+ matching_files.uniq.sort
37
+ end
38
+
39
+ private
40
+
41
+ # Expand included paths to include lintable files under paths which are
42
+ # directories.
43
+ #
44
+ # @param paths [Array<String>]
45
+ # @param patterns [Array<String>]
46
+ # @param allowed_extensions [Array<String>]
47
+ #
48
+ # @return [Array<String>]
49
+ def expand_paths(paths, patterns, allowed_extensions)
50
+ find_files_in_paths(paths, allowed_extensions) + find_matching_files(patterns)
51
+ end
52
+
53
+ # Exclude specified files from the list of included files, expanding the
54
+ # excluded paths as necessary.
55
+ #
56
+ # @param included_paths [Array<String>]
57
+ # @param excluded_paths [Array<String>]
58
+ # @param excluded_patterns [Array<String>]
59
+ #
60
+ # @return [Array<String>]
61
+ def filter_files(included_files, excluded_paths, excluded_patterns)
62
+ # Convert excluded paths to patterns so we don't need to actually hold
63
+ # all excluded files in memory
64
+ excluded_patterns = excluded_patterns.dup
65
+ excluded_paths.each do |file|
66
+ if File.directory?(file)
67
+ excluded_patterns << File.join(file, '**', '*')
68
+ elsif File.file?(file)
69
+ excluded_patterns << file
70
+ else
71
+ raise LintTrappings::InvalidFilePathError,
72
+ "Excluded path '#{path}' does not correspond to a valid file"
73
+ end
74
+ end
75
+
76
+ included_files.reject do |file|
77
+ LintTrappings::Utils.any_glob_matches?(excluded_patterns, file)
78
+ end
79
+ end
80
+
81
+ # Search the specified paths for lintable files.
82
+ #
83
+ # If path is a directory, searches the directory recursively.
84
+ #
85
+ # @param paths [Array<String>]
86
+ # @param allowed_extensions [Array<String>]
87
+ #
88
+ # @return [Array<String>]
89
+ def find_files_in_paths(paths, allowed_extensions)
90
+ files = []
91
+
92
+ paths.each do |path|
93
+ if File.directory?(path)
94
+ files += find_files_in_directory(path, allowed_extensions)
95
+ elsif File.file?(path)
96
+ files << path
97
+ else
98
+ raise LintTrappings::InvalidFilePathError,
99
+ "Path '#{path}' does not correspond to a valid file or directory"
100
+ end
101
+ end
102
+
103
+ files.uniq.map { |path| normalize_path(path) }
104
+ end
105
+
106
+ # Recursively search the specified directory for lintable files.
107
+ #
108
+ # @param directory [String]
109
+ # @param allowed_extensions [Array<String>]
110
+ #
111
+ # @return [Array<String>]
112
+ def find_files_in_directory(directory, allowed_extensions)
113
+ files = []
114
+
115
+ ::Find.find(directory) do |path|
116
+ files << path if lintable_file?(path, allowed_extensions)
117
+ end
118
+
119
+ files
120
+ end
121
+
122
+ # Find all files matching the specified glob patterns.
123
+ #
124
+ # @param patterns [Array<String>] glob patterns
125
+ #
126
+ # @return [Array<String>]
127
+ def find_matching_files(patterns)
128
+ files = []
129
+
130
+ patterns.each do |pattern|
131
+ matches = ::Dir.glob(pattern,
132
+ ::File::FNM_PATHNAME | # Wildcards don't match path separators
133
+ ::File::FNM_DOTMATCH) # `*` wildcard matches dotfiles
134
+
135
+ if matches.empty?
136
+ # One of the patterns specified does not match anything; raise a more
137
+ # descriptive exception so we know which one
138
+ raise LintTrappings::InvalidFilePatternError,
139
+ "Glob pattern '#{pattern}' does not match any file"
140
+ end
141
+
142
+ matches.each do |path|
143
+ files << path if File.file?(path)
144
+ end
145
+ end
146
+
147
+ files.flatten.uniq.map { |path| normalize_path(path) }
148
+ end
149
+
150
+ # Trim "./" from the front of relative paths.
151
+ #
152
+ # @param path [String]
153
+ #
154
+ # @return [String]
155
+ def normalize_path(path)
156
+ path.start_with?(".#{File::SEPARATOR}") ? path[2..-1] : path
157
+ end
158
+
159
+ # Whether a file should be treated as lintable.
160
+ #
161
+ # @param file [String]
162
+ # @param allowed_extensions [Array<String>]
163
+ #
164
+ # @return [Boolean]
165
+ def lintable_file?(file, allowed_extensions)
166
+ return false unless ::File.file?(file)
167
+ allowed_extensions.include?(::File.extname(file))
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,67 @@
1
+ module LintTrappings::Formatter
2
+ # Abstract lint formatter. Subclass and override {#display_report} to
3
+ # implement a custom formatter.
4
+ #
5
+ # @abstract
6
+ class Base
7
+ # @param application [LintTrappings::Application]
8
+ # @param config [LintTrappings::Configuration]
9
+ # @param options [Hash]
10
+ # @param output [LintTrappings::Output]
11
+ def initialize(application, config, options, output)
12
+ @application = application
13
+ @config = config
14
+ @options = options
15
+ @output = output
16
+
17
+ # Used in helpers to determine if severity should be displayed as failure
18
+ # or not
19
+ severities = @config.fetch('severities', error: 'fail', warning: 'warn')
20
+ @fail_severities = severities.select { |_severity, action| action == 'fail' }.keys
21
+ @warn_severities = severities.select { |_severity, action| action == 'warn' }.keys
22
+ end
23
+
24
+ # Called at the start of the run once runner has determined all files it
25
+ # will lint.
26
+ def started(_files_to_lint)
27
+ end
28
+
29
+ # Called at the beginning of a job for a linter run against a file.
30
+ #
31
+ # This can be called in parallel.
32
+ def job_started(_job)
33
+ end
34
+
35
+ # Called at the end of a job for a linter run against a file.
36
+ #
37
+ # This can be called in parallel.
38
+ def job_finished(_job, _lints)
39
+ end
40
+
41
+ # Called at the end of the run.
42
+ def finished(_report)
43
+ end
44
+
45
+ private
46
+
47
+ # @return [LintTrappings::Application]
48
+ attr_reader :application
49
+
50
+ # @return [LintTrappings::Configuration]
51
+ attr_reader :config
52
+
53
+ # @return [Hash]
54
+ attr_reader :options
55
+
56
+ # @return [LintTrappings::Output] stream to send output to
57
+ attr_reader :output
58
+
59
+ def failing_lint?(lint)
60
+ @fail_severities.include?(lint.severity)
61
+ end
62
+
63
+ def warning_lint?(lint)
64
+ @warn_severities.include?(lint.severity)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ require 'lint_trappings/formatter/base'
2
+ require 'stringio'
3
+
4
+ module LintTrappings::Formatter
5
+ # Outputs results in a Checkstyle-compatible format.
6
+ #
7
+ # @see http://checkstyle.sourceforge.net/
8
+ class Checkstyle < Base
9
+ def finished(report)
10
+ xml = StringIO.new
11
+ xml << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
12
+
13
+ xml << "<checkstyle version=\"1.5.6\">\n"
14
+ report.lints.group_by(&:path).each do |path, lints|
15
+ file_name_absolute = File.expand_path(path)
16
+ xml << " <file name=#{file_name_absolute.encode(xml: :attr)}>\n"
17
+
18
+ lints.each do |lint|
19
+ xml << " <error source=\"#{lint.linter.canonical_name if lint.linter}\" " \
20
+ "line=\"#{lint.location.line}\" " \
21
+ "column=\"#{lint.location.column}\" " \
22
+ "length=\"#{lint.location.length}\" " \
23
+ "severity=\"#{lint.severity}\" " \
24
+ "message=#{lint.message.encode(xml: :attr)} />\n"
25
+ end
26
+
27
+ xml << " </file>\n"
28
+ end
29
+ xml << "</checkstyle>\n"
30
+
31
+ output.print xml.string
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,99 @@
1
+ require 'lint_trappings/formatter/base'
2
+
3
+ module LintTrappings::Formatter
4
+ # Outputs lints in a simple format with the filename, line number, and lint
5
+ # message.
6
+ class Default < Base
7
+ def started(files_to_lint)
8
+ files = LintTrappings::Utils.pluralize('file', files_to_lint.count)
9
+ output.info "Scanning #{files_to_lint.count} #{files}..."
10
+ end
11
+
12
+ def job_finished(_job, lints)
13
+ @at_least_one_job_finished = true
14
+
15
+ if lints.any? { |lint| failing_lint?(lint) }
16
+ output.error 'F', false
17
+ elsif lints.any? { |lint| warning_lint?(lint) }
18
+ output.warning 'W', false
19
+ else
20
+ output.success '.', false
21
+ end
22
+ end
23
+
24
+ def finished(report)
25
+ # Ensure we have a newline after the last progress dot output
26
+ output.newline if @at_least_one_job_finished
27
+
28
+ report.lints.each do |lint|
29
+ print_location(lint)
30
+ print_message(lint)
31
+ end
32
+
33
+ output.newline
34
+
35
+ files = LintTrappings::Utils.pluralize('file', report.documents_inspected.count)
36
+ output.print "#{report.documents_inspected.count} #{files} inspected"
37
+
38
+ if report.failures?
39
+ failures = LintTrappings::Utils.pluralize('failure', report.failures.count)
40
+ output.print ', '
41
+ output.error "#{report.failures.count} #{failures} reported", false
42
+ end
43
+
44
+ if report.warnings?
45
+ warnings = LintTrappings::Utils.pluralize('warning', report.warnings.count)
46
+ output.print ', '
47
+ output.warning "#{report.warnings.count} #{warnings} reported", false
48
+ end
49
+
50
+ if report.success?
51
+ output.print ', '
52
+ output.success 'no issues reported', false
53
+ end
54
+
55
+ output.newline
56
+ end
57
+
58
+ private
59
+
60
+ def print_location(lint)
61
+ output.info lint.path, false
62
+ output.puts ':', false
63
+ output.bold lint.source_range.begin.line, false
64
+ output.print ':'
65
+ output.bold lint.source_range.begin.column, false
66
+ output.print ' '
67
+ end
68
+
69
+ def print_message(lint)
70
+ if failing_lint?(lint)
71
+ output.error severity_character(lint.severity), false
72
+ elsif warning_lint?(lint)
73
+ output.warning severity_character(lint.severity), false
74
+ end
75
+
76
+ if lint.linter
77
+ output.notice("[#{lint.linter.canonical_name}] ", false)
78
+ end
79
+
80
+ message = lint.message
81
+ if lint.exception
82
+ message << ' (specify --debug flag to see backtrace)' unless options[:debug]
83
+ end
84
+ output.puts message
85
+
86
+ print_exception(lint)
87
+ end
88
+
89
+ def print_exception(lint)
90
+ return unless lint.exception && options[:debug]
91
+ output.error "#{lint.exception.class}: #{lint.exception.message}"
92
+ output.error lint.exception.backtrace.join("\n")
93
+ end
94
+
95
+ def severity_character(severity)
96
+ "#{severity.to_s[0].capitalize} "
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,62 @@
1
+ require 'lint_trappings/formatter/base'
2
+ require 'json'
3
+
4
+ module LintTrappings::Formatter
5
+ # Outputs report as a JSON document.
6
+ class JSON < Base
7
+ def finished(report)
8
+ lints = report.lints
9
+ grouped = lints.group_by(&:path)
10
+
11
+ report_hash = {
12
+ metadata: metadata,
13
+ files: grouped.map { |path, lints_for_path| path_hash(path, lints_for_path) },
14
+ summary: {
15
+ offense_count: lints.count,
16
+ offending_file_count: grouped.count,
17
+ inspected_file_count: report.documents_inspected.count,
18
+ },
19
+ }
20
+
21
+ output.puts ::JSON.pretty_generate(report_hash)
22
+ end
23
+
24
+ private
25
+
26
+ def metadata
27
+ {
28
+ linter_version: @application.version,
29
+ ruby_engine: RUBY_ENGINE,
30
+ ruby_version: RUBY_VERSION,
31
+ ruby_patchlevel: RUBY_PATCHLEVEL.to_s,
32
+ ruby_platform: RUBY_PLATFORM,
33
+ }
34
+ end
35
+
36
+ def path_hash(path, lints)
37
+ {
38
+ path: path,
39
+ offenses: lints.map { |lint| lint_hash(lint) },
40
+ }
41
+ end
42
+
43
+ def lint_hash(lint)
44
+ {
45
+ severity: lint.severity,
46
+ message: lint.message,
47
+ line: lint.source_range.begin.line,
48
+ column: lint.source_range.begin.column,
49
+ source_range: {
50
+ begin: {
51
+ line: lint.source_range.begin.line,
52
+ column: lint.source_range.begin.column,
53
+ },
54
+ end: {
55
+ line: lint.source_range.end.line,
56
+ column: lint.source_range.end.column,
57
+ },
58
+ },
59
+ }
60
+ end
61
+ end
62
+ end