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,14 @@
1
+ module LintTrappings::Command
2
+ # Displays all available formatters.
3
+ class DisplayFormatters < Base
4
+ def run
5
+ formatter_names = LintTrappings::Formatter::Base.descendants.map do |formatter_class|
6
+ formatter_class.name.split('::').last.sub(/Formatter$/, '').downcase
7
+ end
8
+
9
+ formatter_names.sort.each do |formatter_name|
10
+ output.puts " - #{formatter_name}"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module LintTrappings::Command
2
+ # Outputs help documentation.
3
+ class DisplayHelp < Base
4
+ def run
5
+ output.puts options[:help_message]
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ require 'set'
2
+
3
+ module LintTrappings::Command
4
+ # Displays all available linters and whether or not they are enabled.
5
+ class DisplayLinters < Base
6
+ def run
7
+ LintTrappings::LinterLoader.new(application, config).load(options)
8
+
9
+ linter_selector = LintTrappings::LinterSelector.new(config, options)
10
+ all_linter_names = linter_selector.all_linter_classes.map(&:canonical_name)
11
+ enabled_linter_names = linter_selector.enabled_linter_classes.map(&:canonical_name).to_set
12
+
13
+ all_linter_names.sort.each do |linter_name|
14
+ output.print(' - ')
15
+ output.bold(linter_name, false)
16
+ if enabled_linter_names.include?(linter_name)
17
+ output.success(' enabled')
18
+ else
19
+ output.error(' disabled')
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module LintTrappings::Command
2
+ # Outputs application version information.
3
+ class DisplayVersion < Base
4
+ def run
5
+ output.bold(application.executable_name, false)
6
+ output.info(application.version)
7
+
8
+ if options[:verbose]
9
+ output.bold('Ruby version: ', false)
10
+ output.info(RUBY_VERSION)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module LintTrappings::Command
2
+ # Scan for lints, outputting report of results using the specified formatter.
3
+ class Scan < Base
4
+ def run
5
+ LintTrappings::LinterLoader.new(application, config).load(options)
6
+
7
+ runner = LintTrappings::Runner.new(application, config, output)
8
+ report = runner.run(options)
9
+
10
+ if report.failures?
11
+ raise LintTrappings::ScanFailed,
12
+ 'High severity lints were reported!'
13
+ elsif report.warnings?
14
+ raise LintTrappings::ScanWarned,
15
+ 'Warnings were reported.'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,94 @@
1
+ require 'forwardable'
2
+
3
+ module LintTrappings
4
+ # Stores runtime configuration for the application.
5
+ class Configuration
6
+ # The path of this configuration, if it was loaded from a file.
7
+ #
8
+ # Used in certain scenarios to determine the absolute path of a file
9
+ # specified in a configuration (i.e. getting the path relative to the
10
+ # location of the configuration file itself).
11
+ #
12
+ # @return [String]
13
+ attr_accessor :path
14
+
15
+ # Creates a configuration from the given options hash.
16
+ #
17
+ # @param options [Hash]
18
+ def initialize(options = {})
19
+ @hash = options.dup
20
+ end
21
+
22
+ # Compares this configuration with another.
23
+ #
24
+ # @param other [LintTrappings::Configuration]
25
+ #
26
+ # @return [true,false] whether the given configuration is equivalent
27
+ def ==(other)
28
+ super || @hash == other.hash
29
+ end
30
+
31
+ def fetch(*args, &block)
32
+ @hash.fetch(*args, &block)
33
+ end
34
+
35
+ def [](key)
36
+ @hash[key]
37
+ end
38
+
39
+ def delete(key)
40
+ @hash.delete(key)
41
+ end
42
+
43
+ # Merges the given configuration with this one.
44
+ #
45
+ # The provided configuration will either add to or replace any options
46
+ # defined in this configuration.
47
+ #
48
+ # @param config [LintTrappings::Configuration]
49
+ #
50
+ # @return [LintTrappings::Configuration]
51
+ def merge(config)
52
+ merged_hash = smart_merge(@hash, config.hash)
53
+ self.class.new(merged_hash)
54
+ end
55
+
56
+ def for_linter(linterish)
57
+ linter_name =
58
+ case linterish
59
+ when Class, LintTrappings::Linter
60
+ linterish.canonical_name
61
+ else
62
+ linterish.to_s
63
+ end
64
+
65
+ conf = @hash.fetch('linters', {}).fetch(linter_name, {}).dup
66
+ conf['severity'] ||= @hash.fetch('default_severity', :error)
67
+ conf['severity'] = conf['severity'].to_sym
68
+ conf
69
+ end
70
+
71
+ protected
72
+
73
+ # Internal hash storing the configuration.
74
+ attr_reader :hash
75
+
76
+ private
77
+
78
+ # Merge two hashes such that nested hashes are merged rather than replaced.
79
+ #
80
+ # @param parent [Hash]
81
+ # @param child [Hash]
82
+ # @return [Hash]
83
+ def smart_merge(parent, child)
84
+ parent.merge(child) do |_key, old, new|
85
+ case old
86
+ when Hash
87
+ smart_merge(old, new)
88
+ else
89
+ new
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,98 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module LintTrappings
5
+ # Manages configuration file loading.
6
+ class ConfigurationLoader
7
+ def initialize(application)
8
+ @application = application
9
+ end
10
+
11
+ # Load configuration file given the current working directory the
12
+ # application is running within.
13
+ #
14
+ # @param options [Hash]
15
+ # @option options :working_directory [String] directory to start searching
16
+ # from
17
+ #
18
+ # @raise [NoConfigurationFileError] if no configuration file was found
19
+ #
20
+ # @return [LintTrappings::Configuration]
21
+ def load(options = {})
22
+ config_file_names = @application.configuration_file_names
23
+ working_directory = options.fetch(:working_directory, Dir.pwd)
24
+
25
+ directory = File.expand_path(working_directory)
26
+ config_file = possible_config_files(config_file_names, directory).find(&:file?)
27
+
28
+ if config_file
29
+ load_file(config_file.to_path)
30
+ else
31
+ raise NoConfigurationFileError,
32
+ "No configuration file #{config_file_names.join('/')} found in " \
33
+ 'working directory or any parent directory'
34
+ end
35
+ end
36
+
37
+ # Loads a configuration, ensuring it extends the base configuration.
38
+ #
39
+ # @param path [String]
40
+ #
41
+ # @raise [LintTrappings::ConfigurationParseError] YAML file could not be parsed
42
+ # @raise [LintTrappings::NoConfigurationFileError] specified file not found
43
+ #
44
+ # @return [LintTrappings::Configuration]
45
+ def load_file(path)
46
+ load_from_file(path)
47
+ rescue ::Psych::SyntaxError => error
48
+ raise ConfigurationParseError,
49
+ "Unable to parse configuration from '#{file}': #{error}",
50
+ error.backtrace
51
+ rescue Errno::ENOENT => error
52
+ raise NoConfigurationFileError,
53
+ "Unable to load configuration from '#{file}': #{error}"
54
+ end
55
+
56
+ private
57
+
58
+ def configuration_class
59
+ @application.base_configuration.class
60
+ end
61
+
62
+ # Parses and loads a configuration from the given file.
63
+ #
64
+ # @param path [String]
65
+ #
66
+ # @return [LintTrappings::Configuration]
67
+ def load_from_file(path)
68
+ hash =
69
+ if yaml = YAML.load_file(path)
70
+ yaml.to_hash
71
+ else
72
+ {}
73
+ end
74
+
75
+ configuration_class.new(hash).tap do |config|
76
+ config.path = path
77
+ end
78
+ end
79
+
80
+ # Returns an enumerator for the possible configuration file paths given
81
+ # the context of the specified working directory.
82
+ #
83
+ # @param config_file_name [String]
84
+ # @param directory [String]
85
+ #
86
+ # @return [Array<Pathname>]
87
+ def possible_config_files(config_file_names, directory)
88
+ Enumerator.new do |y|
89
+ Pathname.new(directory)
90
+ .enum_for(:ascend)
91
+ .each do |path|
92
+ config_file_names.each { |config_name| y << (path + config_name) }
93
+ end
94
+ config_file_names.each { |config_name| y << Pathname.new(config_name) }
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,49 @@
1
+ require 'lint_trappings/linter_plugin'
2
+
3
+ module LintTrappings
4
+ # Resolves a configuration to its final representation.
5
+ #
6
+ # This does the dirty work of loading and merging a configuration with the
7
+ # configurations it extends via the `extends` option or `linter_gems` option.
8
+ class ConfigurationResolver
9
+ # @param loader [LintTrappings::ConfigurationLoader]
10
+ def initialize(loader)
11
+ @loader = loader
12
+ end
13
+
14
+ # Resolves the given configuration, returning a configuration with all
15
+ # external configuration files merged into one {Configuration}.
16
+ #
17
+ # @param conf [LintTrappings::Configuration]
18
+ def resolve(conf, options)
19
+ configs_to_extend = Array(conf.delete('extends')).map do |extend_path|
20
+ # If the path is relative, expand it relative to the path of this config
21
+ config_path = File.expand_path(extend_path, conf.path)
22
+
23
+ # Recursively resolve this configuration (it may have `extends` of its own)
24
+ resolve(@loader.load_file(config_path))
25
+ end
26
+
27
+ # Load any configurations included by plugins
28
+ require_paths = Array(conf.delete('linter_plugins')) + options.fetch(:linter_plugins, [])
29
+ configs_to_extend += require_paths.map do |require_path|
30
+ plugin = LinterPlugin.new(require_path)
31
+ plugin.load
32
+ resolve(@loader.load_file(plugin.config_file_path))
33
+ end
34
+
35
+ conf = extend_configs(configs_to_extend, conf) if configs_to_extend.any?
36
+ conf
37
+ end
38
+
39
+ private
40
+
41
+ # Extend the given configurations with the specified config, merging into a
42
+ # single config.
43
+ def extend_configs(configs_to_extend, config)
44
+ configs_to_extend[1..-1].inject(configs_to_extend.first) do |merged, config_to_extend|
45
+ merged.merge(config_to_extend)
46
+ end.merge(config)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ module LintTrappings
2
+ # Represents a parsed document and its associated metadata.
3
+ #
4
+ # Implementors should implement the {#process_source} method.
5
+ #
6
+ # @abstract
7
+ class Document
8
+ # @return [LintTrappings::Configuration] configuration used to parse template
9
+ attr_reader :config
10
+
11
+ # @return [String, nil] path of the file that was parsed, or nil if it was
12
+ # parsed directory from a string
13
+ attr_reader :path
14
+
15
+ # @return [String] original source code
16
+ attr_reader :source
17
+
18
+ # @return [Array<String>] original source code as an array of lines
19
+ attr_reader :source_lines
20
+
21
+ # Parses the specified Slim code into a {Document}.
22
+ #
23
+ # @param source [String] Source code to parse
24
+ # @param config [LintTrappings::Configuration]
25
+ # @param options [Hash]
26
+ # @option options :path [String] path of document that was parsed
27
+ def initialize(source, config, options = {})
28
+ @config = config
29
+ @path = options[:path]
30
+ @source = source
31
+ @source_lines = @source.split("\n")
32
+
33
+ process_source(source)
34
+ end
35
+
36
+ private
37
+
38
+ # Processes the source code of the document, initializing the document.
39
+ #
40
+ # @raise [LintTrappings::ParseError] if there was a problem parsing the document
41
+ def process_source(_source)
42
+ raise NotImplementedError, 'Must implement #process_source'
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,127 @@
1
+ # Collection of errors that can be raised by the framework.
2
+ module LintTrappings
3
+ # Abstract error. Separates LintTrappings errors from other kinds of
4
+ # errors in the exception hierarchy.
5
+ #
6
+ # @abstract
7
+ class LintTrappingsError < StandardError
8
+ # Returns the status code that should be output if this error goes
9
+ # unhandled.
10
+ #
11
+ # Ideally these should resemble exit codes from the sysexits documentation
12
+ # where it makes sense.
13
+ def self.exit_status(*args)
14
+ if args.any?
15
+ @exit_status = args.first
16
+ else
17
+ if @exit_status
18
+ @exit_status
19
+ else
20
+ ancestors[1..-1].each do |ancestor|
21
+ return 70 if ancestor == LintTrappingsError # No exit status defined
22
+ return ancestor.exit_status if ancestor.exit_status
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def exit_status
29
+ self.class.exit_status
30
+ end
31
+ end
32
+
33
+ # Superclass of all configuration-related errors.
34
+ # @abstract
35
+ class ConfigurationError < LintTrappingsError
36
+ exit_status 78 # EX_CONFIG
37
+ end
38
+
39
+ # Raised when a LintTrappings::Application subclass does not set a value for a
40
+ # required configuration attribute.
41
+ class ApplicationConfigurationError < ConfigurationError; end
42
+
43
+ # Raised when a configuration file could not be parsed.
44
+ class ConfigurationParseError < ConfigurationError; end
45
+
46
+ # Raised when configuration file was not found.
47
+ class NoConfigurationFileError < ConfigurationError; end
48
+
49
+ # Raised when a linter's configuration does not match its specification.
50
+ class LinterConfigurationError < ConfigurationError; end
51
+
52
+ # Superclass of all usage-related errors
53
+ # @abstract
54
+ class UsageError < LintTrappingsError
55
+ exit_status 64 # EX_USAGE
56
+ end
57
+
58
+ # Raised when there was a problem loading a formatter.
59
+ class FormatterLoadError < UsageError; end
60
+
61
+ # Raised when invalid/incompatible command line options are specified.
62
+ class InvalidCliOptionError < UsageError; end
63
+
64
+ # Raised when invalid command was specified.
65
+ class InvalidCommandError < UsageError; end
66
+
67
+ # Raised when an invalid file path is specified.
68
+ class InvalidFilePathError < UsageError; end
69
+
70
+ # Raised when an invalid file glob pattern is specified.
71
+ class InvalidFilePatternError < UsageError; end
72
+
73
+ # Raised when an invalid option specification is specified for a linter.
74
+ class InvalidOptionSpecificationError < ConfigurationError; end
75
+
76
+ # Raised when a linter raises an unexpected error.
77
+ class LinterError < LintTrappingsError; end
78
+
79
+ # Raised when an error occurs loading a linter file.
80
+ class LinterLoadError < ConfigurationError; end
81
+
82
+ # Raised when an external preprocessor command returns a non-zero exit status.
83
+ class PreprocessorError < LintTrappingsError
84
+ exit_status 84
85
+ end
86
+
87
+ # Raised when a report contains lints which qualify as warnings, but does not
88
+ # contain failures.
89
+ class ScanWarned < LintTrappingsError
90
+ exit_status 0 # Don't fail for warnings
91
+ end
92
+
93
+ # Raised when a report contains lints which qualify as failures.
94
+ class ScanFailed < LintTrappingsError
95
+ exit_status 2
96
+ end
97
+
98
+ # Raised when running with options that would result in no linters being
99
+ # enabled.
100
+ class NoLintersError < ConfigurationError; end
101
+
102
+ # Raised when there was a problem parsing a document.
103
+ class ParseError < LintTrappingsError
104
+ # @return [String] path to the file that failed to parse
105
+ attr_reader :path
106
+
107
+ # @return [Range<LintTrappings::Location>] source range of the parse error
108
+ attr_reader :source_range
109
+
110
+ def initialize(options)
111
+ @message = options[:message]
112
+ @path = options[:path]
113
+
114
+ if @source_range = options[:source_range]
115
+ @line = @source_range.begin.line
116
+ @column = @source_range.begin.column
117
+ end
118
+ end
119
+
120
+ def message
121
+ msg = @message
122
+ msg << " on line #{@line}" if @line
123
+ msg << ", column #{@column}" if @column
124
+ msg
125
+ end
126
+ end
127
+ end