lint_trappings 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,12 @@
1
+ require 'lint_trappings'
2
+
3
+ # Stub declaration so nested modules can reference it
4
+ module LintTrappings::Spec; end
5
+
6
+ Dir[File.join(File.dirname(__FILE__), 'spec', '**', '*.rb')].each do |file|
7
+ require file
8
+ end
9
+
10
+ RSpec.configure do |config|
11
+ config.include LintTrappings::Matchers
12
+ end
@@ -0,0 +1,22 @@
1
+ require 'tmpdir'
2
+
3
+ # Helpers for creating temporary directories for testing.
4
+ module LintTrappings::Spec::DirectoryHelpers
5
+ module_function
6
+
7
+ # Creates a directory in a temporary directory which will automatically be
8
+ # destroyed at the end of the spec run. Any block passed to this will be
9
+ # executed with the created directory as the working directory.
10
+ #
11
+ # @return [String] The full path of the directory.
12
+ def directory(name = 'some-dir', &block)
13
+ tmpdir = Dir.mktmpdir.tap do |path|
14
+ Dir.chdir(path) do
15
+ Dir.mkdir(name)
16
+ Dir.chdir(name, &block) if block_given?
17
+ end
18
+ end
19
+
20
+ File.join(tmpdir, name)
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module LintTrappings::Spec::IndentationHelpers
2
+ # Strips off excess leading indentation from each line so we can use Heredocs
3
+ # for writing code without having the leading indentation count.
4
+ def normalize_indent(code)
5
+ LintTrappings::Utils.normalize_indent(code)
6
+ end
7
+ end
@@ -0,0 +1,169 @@
1
+ # rubocop:disable Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/LineLength
2
+
3
+ module LintTrappings::Matchers
4
+ # RSpec matcher that returns whether or not a linter reported lints matching
5
+ # the specified criteria.
6
+ #
7
+ # @example
8
+ #
9
+ # it { should report_lint line: 2 }
10
+ class ReportLintMatcher
11
+ ALLOWED_KEYWORD_ARGUMENTS = %i[line message]
12
+
13
+ def initialize(*args, **kwargs)
14
+ @options = kwargs
15
+
16
+ if args.any?
17
+ raise ArgumentError,
18
+ '`report_lint` was given more than one argument!' if args.length > 1
19
+
20
+ if @options[:line]
21
+ raise ArgumentError,
22
+ '`line` keyword argument cannot be specified when Range is given'
23
+ end
24
+
25
+ @range = args.first
26
+ if @range.is_a?(Array)
27
+ if @range.length != 2 ||
28
+ !@range.first.is_a?(Integer) ||
29
+ !@range.last.is_a?(Integer)
30
+ raise ArgumentError,
31
+ 'Location tuple must be an Array of two integers ' \
32
+ "representing line and column, but was #{@range.inspect}"
33
+ end
34
+
35
+ # Convert to an actual range by assuming it spans nothing
36
+ @range = @range..@range
37
+ elsif !@range.is_a?(Range)
38
+ raise ArgumentError, '`report_lint` must be given a Range e.g. [1, 2]..[3, 4]'
39
+ elsif !(@range.begin.is_a?(Array) && @range.end.is_a?(Array))
40
+ raise ArgumentError, 'Source range must have Array tuple endpoints'
41
+ end
42
+ else
43
+ # Otherwise no explicit range was specified, so verify the options
44
+ @options.keys.each do |key|
45
+ raise ArgumentError,
46
+ "Unknown keyword argument #{key}" unless ALLOWED_KEYWORD_ARGUMENTS.include?(key)
47
+ end
48
+
49
+ @line = @options[:line]
50
+ end
51
+
52
+ @message = @options[:message] if @options[:message]
53
+ end
54
+
55
+ def matches?(linter)
56
+ # We're cheating by accessing private values here, but it will allow us to
57
+ # present more-helpful error messages since we get access to so much more
58
+ # information by passing the linter instead of just a list of lints.
59
+ @linter = linter
60
+ @lints = linter.instance_variable_get(:@lints)
61
+
62
+ any_lint_matches?
63
+ end
64
+
65
+ def failure_message
66
+ output = 'expected that a lint would be reported'
67
+
68
+ if !any_range_matches?
69
+ output <<
70
+ if @line
71
+ " on line #{@line}"
72
+ elsif @range
73
+ " on #{range_to_str(@range)}"
74
+ end.to_s
75
+
76
+ output <<
77
+ case @lints.count
78
+ when 0
79
+ ', but none were'
80
+ when 1
81
+ if @line
82
+ ", but was reported on line #{@lints.first.source_range.begin.line}"
83
+ elsif @range
84
+ ", but was reported on #{range_to_str(@lints.first.source_range)}"
85
+ end.to_s
86
+ else
87
+ if @line
88
+ ", but lints were reported on the following lines instead:\n" +
89
+ @lints.map { |lint| lint.source_range.line }.sort.join(', ')
90
+ elsif @range
91
+ ", but lints were reported on the following ranges instead:\n" +
92
+ @lints.map { |lint| range_to_str(lint.source_range) }.join("\n")
93
+ end.to_s
94
+ end
95
+ elsif @message
96
+ matching_lints = lints_matching_range
97
+ output <<
98
+ if @message.is_a?(Regexp)
99
+ " with message matching pattern #{@message.inspect} "
100
+ else
101
+ " with message #{@message.inspect} "
102
+ end
103
+
104
+ output << "but got:\n" << matching_lints.map(&:message).join("\n")
105
+ end
106
+
107
+ output
108
+ end
109
+
110
+ def failure_message_when_negated
111
+ 'expected that a lint would NOT be reported'
112
+ end
113
+
114
+ def description
115
+ output = 'report a lint'
116
+ output <<
117
+ if @line
118
+ " on line #{@line}"
119
+ elsif @range
120
+ " on #{range_to_str(@range)}"
121
+ end.to_s
122
+
123
+ output
124
+ end
125
+
126
+ private
127
+
128
+ def lints_matching_range
129
+ @lints.select { |lint| range_matches?(lint) }
130
+ end
131
+
132
+ def any_lint_matches?
133
+ return true if !@line && !@range && @lints.any?
134
+ lints_matching_range.any? { |lint| message_matches?(lint) }
135
+ end
136
+
137
+ def any_range_matches?
138
+ @lints.any? do |lint|
139
+ range_matches?(lint)
140
+ end
141
+ end
142
+
143
+ def range_matches?(lint)
144
+ if @line
145
+ lint.source_range.begin.line == @line
146
+ else
147
+ lint.source_range == @range
148
+ end
149
+ end
150
+
151
+ def message_matches?(lint)
152
+ if @message.nil?
153
+ true
154
+ elsif @message.is_a?(Regexp)
155
+ lint.message =~ @message
156
+ elsif @message.is_a?(String)
157
+ lint.message == @message
158
+ end
159
+ end
160
+
161
+ def range_to_str(range)
162
+ "(L#{range.begin.line},C#{range.begin.column})..(L#{range.end.line},C#{range.end.column})"
163
+ end
164
+ end
165
+
166
+ def report_lint(*args, **kwargs)
167
+ ReportLintMatcher.new(*args, **kwargs)
168
+ end
169
+ end
@@ -0,0 +1,35 @@
1
+ # Makes writing tests for linters a lot DRYer by taking any `src` variable
2
+ # defined via `let` and normalizing it (removing indentation that would be
3
+ # inserted by using Heredocs) and setting up the subject to be the lints
4
+ # returned by Linter#run.
5
+ #
6
+ # Thus a typical test will look like:
7
+ #
8
+ # @example
9
+ # require 'spec_helper'
10
+ #
11
+ # RSpec.describe MyApp::Linter::MyLinter do
12
+ # include_context 'linter'
13
+ #
14
+ # context 'when source contains "foo"' do
15
+ # let(:src) { <<-SRC }
16
+ # This is some code
17
+ # with the word "foo" in it.
18
+ # SRC
19
+ #
20
+ # it { should report_lint line: 2 }
21
+ # end
22
+ # end
23
+ shared_context 'linter' do
24
+ let(:config) do
25
+ LINT_TRAP_APPLICATION_CLASS.base_configuration.for_linter(described_class)
26
+ end
27
+
28
+ subject do
29
+ linter = described_class.new(config)
30
+ document = LINT_TRAP_APPLICATION_CLASS.document_class
31
+ .new(normalize_indent(src), config)
32
+ linter.run(document)
33
+ linter
34
+ end
35
+ end
@@ -0,0 +1,123 @@
1
+ module LintTrappings
2
+ # Miscellaneus collection of helper functions.
3
+ module Utils
4
+ module_function
5
+
6
+ # Returns whether a glob pattern (or any of a list of patterns) matches the
7
+ # specified file.
8
+ #
9
+ # This is defined here so our file globbing options are consistent
10
+ # everywhere we perform globbing.
11
+ #
12
+ # @param glob [String, Array]
13
+ # @param file [String]
14
+ # @return [Boolean]
15
+ def any_glob_matches?(globs_or_glob, file)
16
+ Array(globs_or_glob).any? do |glob|
17
+ ::File.fnmatch?(glob, file,
18
+ ::File::FNM_PATHNAME | # Wildcards don't match path separators
19
+ ::File::FNM_DOTMATCH) # `*` wildcard matches dotfiles
20
+ end
21
+ end
22
+
23
+ # Find all consecutive items satisfying the given block of a minimum size,
24
+ # yielding each group of consecutive items to the provided block.
25
+ #
26
+ # @param items [Array]
27
+ # @param satisfies [Proc] function that takes an item and returns true/false
28
+ # @param min_consecutive [Fixnum] minimum number of consecutive items before
29
+ # yielding the group
30
+ # @yield Passes list of consecutive items all matching the criteria defined
31
+ # by the `satisfies` {Proc} to the provided block
32
+ # @yieldparam group [Array] List of consecutive items
33
+ # @yieldreturn [Boolean] block should return whether item matches criteria
34
+ # for inclusion
35
+ def for_consecutive_items(items, satisfies, min_consecutive = 2)
36
+ current_index = -1
37
+
38
+ while (current_index += 1) < items.count
39
+ next unless satisfies[items[current_index]]
40
+
41
+ count = count_consecutive(items, current_index, &satisfies)
42
+ next unless count >= min_consecutive
43
+
44
+ # Yield the chunk of consecutive items
45
+ yield items[current_index...(current_index + count)]
46
+
47
+ current_index += count # Skip this patch of consecutive items to find more
48
+ end
49
+ end
50
+
51
+ # Count the number of consecutive items satisfying the given {Proc}.
52
+ #
53
+ # @param items [Array]
54
+ # @param offset [Fixnum] index to start searching from
55
+ # @yield [item] Passes item to the provided block.
56
+ # @yieldparam item [Object] Item to evaluate as matching criteria for
57
+ # inclusion
58
+ # @yieldreturn [Boolean] whether to include the item
59
+ # @return [Integer]
60
+ def count_consecutive(items, offset = 0, &block)
61
+ count = 1
62
+ count += 1 while (offset + count < items.count) && block.call(items[offset + count])
63
+ count
64
+ end
65
+
66
+ # Convert a class name or CamelCase string into snake_case.
67
+ #
68
+ # @see stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
69
+ #
70
+ # @param str [String]
71
+ #
72
+ # @return [String]
73
+ def snake_case(str)
74
+ str.gsub(/::/, '/')
75
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
76
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
77
+ .tr('-', '_')
78
+ .downcase
79
+ end
80
+
81
+ # Converts a string containing underscores/hyphens/spaces into CamelCase.
82
+ #
83
+ # @param str [String]
84
+ #
85
+ # @return [String]
86
+ def camel_case(str)
87
+ str.split(/_|-| /).map { |part| part.sub(/^\w/, &:upcase) }.join
88
+ end
89
+
90
+ # Returns the plural of the word if necessary based on the given count.
91
+ #
92
+ # @param word [String]
93
+ # @param count [Integer]
94
+ #
95
+ # @return [String]
96
+ def pluralize(word, count)
97
+ count == 1 ? word : "#{word}s"
98
+ end
99
+
100
+ # Strips off excess leading indentation from each line so we can use Heredocs
101
+ # for writing code without having the leading indentation count.
102
+ def normalize_indent(code)
103
+ leading_indent = code[/^(\s*)/, 1]
104
+ code.lstrip.gsub(/\n#{leading_indent}/, "\n")
105
+ end
106
+
107
+ # Calls a block of code with a modified set of environment variables,
108
+ # restoring them once the code has executed.
109
+ #
110
+ # @param env [Hash] environment variables to set
111
+ def with_environment(env)
112
+ old_env = {}
113
+ env.each do |var, value|
114
+ old_env[var] = ENV[var.to_s]
115
+ ENV[var.to_s] = value
116
+ end
117
+
118
+ yield
119
+ ensure
120
+ old_env.each { |var, value| ENV[var.to_s] = value }
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,4 @@
1
+ # Defines the gem version.
2
+ module LintTrappings
3
+ VERSION = '0.1.0'
4
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lint_trappings
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shane da Silva
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parallel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: terminal-table
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
41
+ description: Framework for writing static analysis tools (a.k.a. linters)
42
+ email:
43
+ - shane@dasilva.io
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE.md
49
+ - lib/lint_trappings.rb
50
+ - lib/lint_trappings/application.rb
51
+ - lib/lint_trappings/arguments_parser.rb
52
+ - lib/lint_trappings/cli.rb
53
+ - lib/lint_trappings/command/base.rb
54
+ - lib/lint_trappings/command/display_documentation.rb
55
+ - lib/lint_trappings/command/display_formatters.rb
56
+ - lib/lint_trappings/command/display_help.rb
57
+ - lib/lint_trappings/command/display_linters.rb
58
+ - lib/lint_trappings/command/display_version.rb
59
+ - lib/lint_trappings/command/scan.rb
60
+ - lib/lint_trappings/configuration.rb
61
+ - lib/lint_trappings/configuration_loader.rb
62
+ - lib/lint_trappings/configuration_resolver.rb
63
+ - lib/lint_trappings/document.rb
64
+ - lib/lint_trappings/errors.rb
65
+ - lib/lint_trappings/executable.rb
66
+ - lib/lint_trappings/file_finder.rb
67
+ - lib/lint_trappings/formatter/base.rb
68
+ - lib/lint_trappings/formatter/checkstyle.rb
69
+ - lib/lint_trappings/formatter/default.rb
70
+ - lib/lint_trappings/formatter/json.rb
71
+ - lib/lint_trappings/formatter_forwarder.rb
72
+ - lib/lint_trappings/formatter_loader.rb
73
+ - lib/lint_trappings/lint.rb
74
+ - lib/lint_trappings/linter.rb
75
+ - lib/lint_trappings/linter_configuration_validator.rb
76
+ - lib/lint_trappings/linter_loader.rb
77
+ - lib/lint_trappings/linter_plugin.rb
78
+ - lib/lint_trappings/linter_selector.rb
79
+ - lib/lint_trappings/location.rb
80
+ - lib/lint_trappings/output.rb
81
+ - lib/lint_trappings/preprocessor.rb
82
+ - lib/lint_trappings/rake_task.rb
83
+ - lib/lint_trappings/report.rb
84
+ - lib/lint_trappings/runner.rb
85
+ - lib/lint_trappings/spec.rb
86
+ - lib/lint_trappings/spec/directory_helpers.rb
87
+ - lib/lint_trappings/spec/indentation_helpers.rb
88
+ - lib/lint_trappings/spec/matchers/report_lint_matcher.rb
89
+ - lib/lint_trappings/spec/shared_contexts/linter_shared_context.rb
90
+ - lib/lint_trappings/utils.rb
91
+ - lib/lint_trappings/version.rb
92
+ homepage: https://github.com/sds/lint-trappings
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '2'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.4.5.1
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Linter framework
116
+ test_files: []
117
+ has_rdoc: