slim_lint_standard 0.0.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +21 -0
  3. data/bin/slim-lint-standard +7 -0
  4. data/config/default.yml +109 -0
  5. data/lib/slim_lint/atom.rb +129 -0
  6. data/lib/slim_lint/capture_map.rb +19 -0
  7. data/lib/slim_lint/cli.rb +167 -0
  8. data/lib/slim_lint/configuration.rb +111 -0
  9. data/lib/slim_lint/configuration_loader.rb +86 -0
  10. data/lib/slim_lint/constants.rb +10 -0
  11. data/lib/slim_lint/document.rb +78 -0
  12. data/lib/slim_lint/engine.rb +41 -0
  13. data/lib/slim_lint/exceptions.rb +20 -0
  14. data/lib/slim_lint/file_finder.rb +88 -0
  15. data/lib/slim_lint/filter.rb +126 -0
  16. data/lib/slim_lint/filters/attribute_processor.rb +46 -0
  17. data/lib/slim_lint/filters/auto_indenter.rb +39 -0
  18. data/lib/slim_lint/filters/control_processor.rb +46 -0
  19. data/lib/slim_lint/filters/do_inserter.rb +39 -0
  20. data/lib/slim_lint/filters/end_inserter.rb +74 -0
  21. data/lib/slim_lint/filters/interpolation.rb +73 -0
  22. data/lib/slim_lint/filters/multi_flattener.rb +32 -0
  23. data/lib/slim_lint/filters/splat_processor.rb +20 -0
  24. data/lib/slim_lint/filters/static_merger.rb +47 -0
  25. data/lib/slim_lint/lint.rb +70 -0
  26. data/lib/slim_lint/linter/avoid_multiline_expressions.rb +41 -0
  27. data/lib/slim_lint/linter/comment_control_statement.rb +26 -0
  28. data/lib/slim_lint/linter/consecutive_control_statements.rb +26 -0
  29. data/lib/slim_lint/linter/control_statement_spacing.rb +32 -0
  30. data/lib/slim_lint/linter/dynamic_output_spacing.rb +77 -0
  31. data/lib/slim_lint/linter/embedded_engines.rb +18 -0
  32. data/lib/slim_lint/linter/empty_control_statement.rb +15 -0
  33. data/lib/slim_lint/linter/empty_lines.rb +24 -0
  34. data/lib/slim_lint/linter/file_length.rb +18 -0
  35. data/lib/slim_lint/linter/line_length.rb +18 -0
  36. data/lib/slim_lint/linter/redundant_div.rb +21 -0
  37. data/lib/slim_lint/linter/rubocop.rb +131 -0
  38. data/lib/slim_lint/linter/standard.rb +69 -0
  39. data/lib/slim_lint/linter/tab.rb +20 -0
  40. data/lib/slim_lint/linter/tag_case.rb +15 -0
  41. data/lib/slim_lint/linter/trailing_blank_lines.rb +19 -0
  42. data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
  43. data/lib/slim_lint/linter.rb +93 -0
  44. data/lib/slim_lint/linter_registry.rb +37 -0
  45. data/lib/slim_lint/linter_selector.rb +87 -0
  46. data/lib/slim_lint/logger.rb +103 -0
  47. data/lib/slim_lint/matcher/anything.rb +11 -0
  48. data/lib/slim_lint/matcher/base.rb +21 -0
  49. data/lib/slim_lint/matcher/capture.rb +32 -0
  50. data/lib/slim_lint/matcher/nothing.rb +13 -0
  51. data/lib/slim_lint/options.rb +110 -0
  52. data/lib/slim_lint/parser.rb +584 -0
  53. data/lib/slim_lint/rake_task.rb +125 -0
  54. data/lib/slim_lint/report.rb +25 -0
  55. data/lib/slim_lint/reporter/checkstyle_reporter.rb +42 -0
  56. data/lib/slim_lint/reporter/default_reporter.rb +40 -0
  57. data/lib/slim_lint/reporter/emacs_reporter.rb +40 -0
  58. data/lib/slim_lint/reporter/json_reporter.rb +50 -0
  59. data/lib/slim_lint/reporter.rb +44 -0
  60. data/lib/slim_lint/ruby_extract_engine.rb +30 -0
  61. data/lib/slim_lint/ruby_extractor.rb +175 -0
  62. data/lib/slim_lint/ruby_parser.rb +32 -0
  63. data/lib/slim_lint/runner.rb +82 -0
  64. data/lib/slim_lint/sexp.rb +134 -0
  65. data/lib/slim_lint/sexp_visitor.rb +150 -0
  66. data/lib/slim_lint/source_location.rb +45 -0
  67. data/lib/slim_lint/utils.rb +84 -0
  68. data/lib/slim_lint/version.rb +6 -0
  69. data/lib/slim_lint.rb +55 -0
  70. metadata +218 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6dcf68f7cde5910a4f87dc7f7c1409976006ffde7a554601cb2ff0fad4240dad
4
+ data.tar.gz: c1b6f4332831e572aa1f3adbb010a254ad752c222531849b2891cd740013a997
5
+ SHA512:
6
+ metadata.gz: ef7ee3e8871a0eb7218a7994aefe7922411435c75f112ea85998cc9bfb3be4228ebb0bdf6ebc2ba8de308cad2c03975cbbbf2d65b6f1425998ee358e25ba5252
7
+ data.tar.gz: acaf35418fea782e1e8f1fe1612c0a19668d90c6b3befa00afb638e1998a1b2baead8d32770a94ca2ee0e4c7e0d2077c123912bd684110decff3c58130e545a2
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ `slim-lint-standard` is released under the MIT license.
2
+
3
+ > Copyright (c) 2015 Shane da Silva. http://shane.io
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 THE
21
+ > SOFTWARE.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'slim_lint/cli'
5
+
6
+ logger = SlimLint::Logger.new(STDOUT)
7
+ exit SlimLint::CLI.new(logger).run(ARGV)
@@ -0,0 +1,109 @@
1
+ # Default application configuration that all configurations inherit from.
2
+ #
3
+ # This is an opinionated list of which hooks are valuable to run and what their
4
+ # out of the box settings should be.
5
+
6
+ # Whether to ignore frontmatter at the beginning of Slim documents for
7
+ # frameworks such as Jekyll/Middleman
8
+ skip_frontmatter: false
9
+
10
+ linters:
11
+ AvoidMultilineExpressions:
12
+ enabled: true
13
+
14
+ CommentControlStatement:
15
+ enabled: true
16
+
17
+ ConsecutiveControlStatements:
18
+ enabled: true
19
+ max_consecutive: 2
20
+
21
+ ControlStatementSpacing:
22
+ enabled: true
23
+ space_after: always
24
+
25
+ DynamicOutputSpacing:
26
+ enabled: true
27
+ space_before: always
28
+ space_after: always
29
+
30
+ EmbeddedEngines:
31
+ enabled: false
32
+ forbidden_engines: []
33
+
34
+ EmptyControlStatement:
35
+ enabled: true
36
+
37
+ EmptyLines:
38
+ enabled: true
39
+
40
+ FileLength:
41
+ enabled: false
42
+ max: 300
43
+
44
+ LineLength:
45
+ enabled: false
46
+ max: 80
47
+
48
+ RedundantDiv:
49
+ enabled: true
50
+
51
+ RuboCop:
52
+ enabled: false
53
+ # These cops are incredibly noisy since the Ruby we extract from Slim
54
+ # templates isn't well-formatted, so we ignore them.
55
+ # WARNING: If you define this list in your own .slim-lint.yml file, you'll
56
+ # be overriding the list defined here.
57
+ # TODO: Narrow this list to essentials.
58
+ ignored_cops:
59
+ - Layout/ArgumentAlignment
60
+ - Layout/ArrayAlignment
61
+ - Layout/BlockAlignment
62
+ - Layout/ClosingParenthesisIndentation
63
+ - Layout/EmptyLineAfterGuardClause
64
+ - Layout/EndAlignment
65
+ - Layout/FirstArgumentIndentation
66
+ - Layout/FirstArrayElementIndentation
67
+ - Layout/FirstHashElementIndentation
68
+ - Layout/FirstParameterIndentation
69
+ - Layout/HashAlignment
70
+ - Layout/IndentationConsistency
71
+ - Layout/IndentationWidth
72
+ - Layout/InitialIndentation
73
+ - Layout/LineEndStringConcatenationIndentation
74
+ - Layout/LineLength
75
+ - Layout/MultilineArrayBraceLayout
76
+ - Layout/MultilineAssignmentLayout
77
+ - Layout/MultilineHashBraceLayout
78
+ - Layout/MultilineMethodCallBraceLayout
79
+ - Layout/MultilineMethodCallIndentation
80
+ - Layout/MultilineMethodDefinitionBraceLayout
81
+ - Layout/MultilineOperationIndentation
82
+ - Layout/ParameterAlignment
83
+ - Layout/TrailingEmptyLines
84
+ - Layout/TrailingWhitespace
85
+ - Lint/Void
86
+ - Metrics/BlockLength
87
+ - Metrics/BlockNesting
88
+ - Naming/FileName
89
+ - Style/FrozenStringLiteralComment
90
+ - Style/IdenticalConditionalBranches
91
+ - Style/IfUnlessModifier
92
+ - Style/Next
93
+ - Style/WhileUntilDo
94
+ - Style/WhileUntilModifier
95
+
96
+ Standard:
97
+ enabled: true
98
+
99
+ Tab:
100
+ enabled: true
101
+
102
+ TagCase:
103
+ enabled: true
104
+
105
+ TrailingBlankLines:
106
+ enabled: true
107
+
108
+ TrailingWhitespace:
109
+ enabled: true
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Represents an atomic, childless, literal value within an S-expression.
5
+ #
6
+ # This creates a light wrapper around literal values of S-expressions so we
7
+ # can make an {Atom} quack like a {Sexp} without being an {Sexp}.
8
+ class Atom
9
+ # Stores the line number of the code in the original document that this Atom
10
+ # came from.
11
+ attr_accessor :value, :start, :finish
12
+
13
+ # Creates an atom from the specified value.
14
+ #
15
+ # @param value [Object]
16
+ def initialize(value, pos:)
17
+ @value = value
18
+
19
+ @start = pos
20
+
21
+ if value.is_a?(String)
22
+ lines = value.count("\n")
23
+ chars = 0
24
+ chars = value.lines.last.size unless value.empty?
25
+ @finish = [pos[0] + lines, chars + (lines == 0 ? pos[1] : 0)]
26
+ end
27
+ end
28
+
29
+ def line
30
+ start[0] if start
31
+ end
32
+
33
+ def location
34
+ SourceLocation.new(
35
+ start_line: start[0],
36
+ start_column: start[1],
37
+ last_line: (finish || start)[0],
38
+ last_column: (finish || start)[1]
39
+ )
40
+ end
41
+
42
+ # Returns whether this atom is equivalent to another object.
43
+ #
44
+ # This defines a helper which unwraps the inner value of the atom to compare
45
+ # against a literal value, saving us having to do it ourselves everywhere
46
+ # else.
47
+ #
48
+ # @param other [Object]
49
+ # @return [Boolean]
50
+ def ==(other)
51
+ @value == (other.is_a?(Atom) ? other.instance_variable_get(:@value) : other)
52
+ end
53
+
54
+ # Returns whether this atom matches the given Sexp pattern.
55
+ #
56
+ # This exists solely to make an {Atom} quack like a {Sexp}, so we don't have
57
+ # to manually check the type when doing comparisons elsewhere.
58
+ #
59
+ # @param [Array, Object]
60
+ # @return [Boolean]
61
+ def match?(pattern)
62
+ # Delegate matching logic if we're comparing against a matcher
63
+ if pattern.is_a?(SlimLint::Matcher::Base)
64
+ return pattern.match?(@value)
65
+ end
66
+
67
+ @value == pattern
68
+ end
69
+
70
+ # Displays the string representation the value this {Atom} wraps.
71
+ #
72
+ # @return [String]
73
+ def to_s
74
+ @value.to_s
75
+ end
76
+
77
+ def to_array
78
+ @value
79
+ end
80
+
81
+ # Displays a string representation of this {Atom} suitable for debugging.
82
+ #
83
+ # @return [String]
84
+ def inspect
85
+ range = +""
86
+ range << start.join(":") if start
87
+ range << " => " if start && finish
88
+ range << finish.join(":") if finish
89
+
90
+ "A(#{range}) #{@value.inspect}"
91
+ end
92
+
93
+ # Redirect methods to the value this {Atom} wraps.
94
+ #
95
+ # Again, this is for convenience so we don't need to manually unwrap the
96
+ # value ourselves. It's pretty magical, but results in much DRYer code.
97
+ #
98
+ # @param method_sym [Symbol] method that was called
99
+ # @param args [Array]
100
+ # @yield block that was passed to the method
101
+ def method_missing(method_sym, *args, &block)
102
+ if @value.respond_to?(method_sym)
103
+ @value.send(method_sym, *args, &block)
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ # @param method_name [String,Symbol] method name
110
+ # @param args [Array]
111
+ def respond_to_missing?(method_name, *args)
112
+ @value.__send__(:respond_to_missing?, method_name, *args) || super
113
+ end
114
+
115
+ # Return whether this {Atom} or the value it wraps responds to the given
116
+ # message.
117
+ #
118
+ # @param method_sym [Symbol]
119
+ # @param include_private [Boolean]
120
+ # @return [Boolean]
121
+ def respond_to?(method_sym, include_private = false)
122
+ if super
123
+ true
124
+ else
125
+ @value.respond_to?(method_sym, include_private)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Holds the list of captures, providing a convenient interface for accessing
5
+ # the values and unwrapping them on your behalf.
6
+ class CaptureMap < Hash
7
+ # Returns the captured value with the specified name.
8
+ #
9
+ # @param capture_name [Symbol]
10
+ # @return [Object]
11
+ def [](capture_name)
12
+ if key?(capture_name)
13
+ super.value
14
+ else
15
+ raise ArgumentError, "Capture #{capture_name.inspect} does not exist!"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slim_lint"
4
+ require "slim_lint/options"
5
+
6
+ module SlimLint
7
+ # Command line application interface.
8
+ class CLI
9
+ # Exit codes
10
+ # @see https://man.openbsd.org/sysexits.3
11
+ EX_OK = 0
12
+ EX_USAGE = 64
13
+ EX_DATAERR = 65
14
+ EX_NOINPUT = 67
15
+ EX_SOFTWARE = 70
16
+ EX_CONFIG = 78
17
+
18
+ # Create a CLI that outputs to the specified logger.
19
+ #
20
+ # @param logger [SlimLint::Logger]
21
+ def initialize(logger)
22
+ @log = logger
23
+ end
24
+
25
+ # Parses the given command-line arguments and executes appropriate logic
26
+ # based on those arguments.
27
+ #
28
+ # @param args [Array<String>] command line arguments
29
+ # @return [Integer] exit status code
30
+ def run(args)
31
+ options = SlimLint::Options.new.parse(args)
32
+ act_on_options(options)
33
+ rescue => e
34
+ handle_exception(e)
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :log
40
+
41
+ # Given the provided options, execute the appropriate command.
42
+ #
43
+ # @return [Integer] exit status code
44
+ def act_on_options(options)
45
+ log.color_enabled = options.fetch(:color, log.tty?)
46
+
47
+ if options[:help]
48
+ print_help(options)
49
+ EX_OK
50
+ elsif options[:version] || options[:verbose_version]
51
+ print_version(options)
52
+ EX_OK
53
+ elsif options[:show_linters]
54
+ print_available_linters
55
+ EX_OK
56
+ elsif options[:show_reporters]
57
+ print_available_reporters
58
+ EX_OK
59
+ else
60
+ scan_for_lints(options)
61
+ end
62
+ end
63
+
64
+ # Outputs a message and returns an appropriate error code for the specified
65
+ # exception.
66
+ def handle_exception(exception)
67
+ case exception
68
+ when SlimLint::Exceptions::ConfigurationError
69
+ log.error exception.message
70
+ EX_CONFIG
71
+ when SlimLint::Exceptions::InvalidCLIOption
72
+ log.error exception.message
73
+ log.log "Run `#{APP_NAME}` --help for usage documentation"
74
+ EX_USAGE
75
+ when SlimLint::Exceptions::InvalidFilePath
76
+ log.error exception.message
77
+ EX_NOINPUT
78
+ when SlimLint::Exceptions::NoLintersError
79
+ log.error exception.message
80
+ EX_NOINPUT
81
+ else
82
+ print_unexpected_exception(exception)
83
+ EX_SOFTWARE
84
+ end
85
+ end
86
+
87
+ # Scans the files specified by the given options for lints.
88
+ #
89
+ # @return [Integer] exit status code
90
+ def scan_for_lints(options)
91
+ report = Runner.new.run(options)
92
+ print_report(report, options)
93
+ report.failed? ? EX_DATAERR : EX_OK
94
+ end
95
+
96
+ # Outputs a report of the linter run using the specified reporter.
97
+ def print_report(report, options)
98
+ reporter = options.fetch(:reporter,
99
+ SlimLint::Reporter::DefaultReporter).new(log)
100
+ reporter.display_report(report)
101
+ end
102
+
103
+ # Outputs a list of all currently available linters.
104
+ def print_available_linters
105
+ log.info "Available linters:"
106
+
107
+ linter_names = SlimLint::LinterRegistry.linters.map do |linter|
108
+ linter.name.split("::").last
109
+ end
110
+
111
+ linter_names.sort.each do |linter_name|
112
+ log.log " - #{linter_name}"
113
+ end
114
+ end
115
+
116
+ # Outputs a list of currently available reporters.
117
+ def print_available_reporters
118
+ log.info "Available reporters:"
119
+
120
+ reporter_names = SlimLint::Reporter.descendants.map do |reporter|
121
+ reporter.name.split("::").last.sub(/Reporter$/, "").downcase
122
+ end
123
+
124
+ reporter_names.sort.each do |reporter_name|
125
+ log.log " - #{reporter_name}"
126
+ end
127
+ end
128
+
129
+ # Outputs help documentation.
130
+ def print_help(options)
131
+ log.log options[:help]
132
+ end
133
+
134
+ # Outputs the application name and version.
135
+ def print_version(options)
136
+ log.log "#{SlimLint::APP_NAME} #{SlimLint::VERSION}"
137
+
138
+ if options[:verbose_version]
139
+ log.log "slim #{Gem.loaded_specs["slim"].version}"
140
+ log.log "rubocop #{Gem.loaded_specs["rubocop"].version}"
141
+ log.log RUBY_DESCRIPTION
142
+ end
143
+ end
144
+
145
+ # Outputs the backtrace of an exception with instructions on how to report
146
+ # the issue.
147
+ def print_unexpected_exception(exception)
148
+ log.bold_error exception.message
149
+ log.error exception.backtrace.join("\n")
150
+ log.warning "Report this bug at ", false
151
+ log.info SlimLint::BUG_REPORT_URL
152
+ log.newline
153
+ log.success "To help fix this issue, please include:"
154
+ log.log "- The above stack trace"
155
+ log.log "- slim-lint-standard version: ", false
156
+ log.info SlimLint::VERSION
157
+ log.log "- RuboCop version: ", false
158
+ log.info Gem.loaded_specs["rubocop"].version
159
+ if Gem.loaded_specs["standard"]
160
+ log.log "- Standard version: ", false
161
+ log.info Gem.loaded_specs["standard"].version
162
+ end
163
+ log.log "- Ruby version: ", false
164
+ log.info RUBY_VERSION
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Stores runtime configuration for the application.
5
+ #
6
+ # The purpose of this class is to validate and ensure all configurations
7
+ # satisfy some basic pre-conditions so other parts of the application don't
8
+ # have to check the configuration for errors. It should have no knowledge of
9
+ # how these configuration values are ultimately used.
10
+ class Configuration
11
+ # Internal hash storing the configuration.
12
+ attr_reader :hash
13
+
14
+ # Creates a configuration from the given options hash.
15
+ #
16
+ # @param options [Hash]
17
+ def initialize(options)
18
+ @hash = options
19
+ validate
20
+ end
21
+
22
+ # Access the configuration as if it were a hash.
23
+ #
24
+ # @param key [String]
25
+ # @return [Array,Hash,Number,String]
26
+ def [](key)
27
+ @hash[key]
28
+ end
29
+
30
+ # Compares this configuration with another.
31
+ #
32
+ # @param other [SlimLint::Configuration]
33
+ # @return [true,false] whether the given configuration is equivalent
34
+ def ==(other)
35
+ super || @hash == other.hash
36
+ end
37
+
38
+ # Returns a non-modifiable configuration for the specified linter.
39
+ #
40
+ # @param linter [SlimLint::Linter,Class]
41
+ def for_linter(linter)
42
+ linter_name =
43
+ case linter
44
+ when SlimLint::Linter
45
+ linter.name
46
+ when Class
47
+ linter.name.split("::").last
48
+ when String
49
+ linter.split("::").last
50
+ end
51
+
52
+ @hash["linters"].fetch(linter_name, {}).dup.freeze
53
+ end
54
+
55
+ # Merges the given configuration with this one, returning a new
56
+ # {Configuration}. The provided configuration will either add to or replace
57
+ # any options defined in this configuration.
58
+ #
59
+ # @param config [SlimLint::Configuration]
60
+ def merge(config)
61
+ self.class.new(smart_merge(@hash, config.hash))
62
+ end
63
+
64
+ private
65
+
66
+ # Merge two hashes such that nested hashes are merged rather than replaced.
67
+ #
68
+ # @param parent [Hash]
69
+ # @param child [Hash]
70
+ # @return [Hash]
71
+ def smart_merge(parent, child)
72
+ parent.merge(child) do |_key, old, new|
73
+ case old
74
+ when Hash
75
+ smart_merge(old, new)
76
+ else
77
+ new
78
+ end
79
+ end
80
+ end
81
+
82
+ # Validates the configuration for any invalid options, normalizing it where
83
+ # possible.
84
+ def validate
85
+ ensure_exclude_option_array_exists
86
+ ensure_linter_section_exists
87
+ ensure_linter_include_exclude_arrays_exist
88
+ end
89
+
90
+ # Ensures the `exclude` global option is an array.
91
+ def ensure_exclude_option_array_exists
92
+ @hash["exclude"] = Array(@hash["exclude"])
93
+ end
94
+
95
+ # Ensures the `linters` configuration section exists.
96
+ def ensure_linter_section_exists
97
+ @hash["linters"] ||= {}
98
+ end
99
+
100
+ # Ensure `include` and `exclude` options for linters are arrays
101
+ # (since users can specify a single string glob pattern for convenience)
102
+ def ensure_linter_include_exclude_arrays_exist
103
+ @hash["linters"].each_key do |linter_name|
104
+ %w[include exclude].each do |option|
105
+ linter_config = @hash["linters"][linter_name]
106
+ linter_config[option] = Array(linter_config[option])
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "yaml"
5
+
6
+ module SlimLint
7
+ # Manages configuration file loading.
8
+ class ConfigurationLoader
9
+ DEFAULT_CONFIG_PATH = File.join(SlimLint::HOME, "config", "default.yml").freeze
10
+ CONFIG_FILE_NAME = ".slim-lint.yml"
11
+
12
+ class << self
13
+ # Load configuration file given the current working directory the
14
+ # application is running within.
15
+ def load_applicable_config
16
+ directory = File.expand_path(Dir.pwd)
17
+ config_file = possible_config_files(directory).find(&:file?)
18
+
19
+ if config_file
20
+ load_file(config_file.to_path)
21
+ else
22
+ default_configuration
23
+ end
24
+ end
25
+
26
+ # Loads the built-in default configuration.
27
+ def default_configuration
28
+ @default_configuration ||= load_from_file(DEFAULT_CONFIG_PATH)
29
+ end
30
+
31
+ # Loads a configuration, ensuring it extends the default configuration.
32
+ #
33
+ # @param file [String]
34
+ # @return [SlimLint::Configuration]
35
+ def load_file(file)
36
+ config = load_from_file(file)
37
+
38
+ default_configuration.merge(config)
39
+ rescue Psych::SyntaxError, Errno::ENOENT => e
40
+ raise SlimLint::Exceptions::ConfigurationError,
41
+ "Unable to load configuration from '#{file}': #{e}",
42
+ e.backtrace
43
+ end
44
+
45
+ # Creates a configuration from the specified hash, ensuring it extends the
46
+ # default configuration.
47
+ #
48
+ # @param hash [Hash]
49
+ # @return [SlimLint::Configuration]
50
+ def load_hash(hash)
51
+ config = SlimLint::Configuration.new(hash)
52
+
53
+ default_configuration.merge(config)
54
+ end
55
+
56
+ private
57
+
58
+ # Parses and loads a configuration from the given file.
59
+ #
60
+ # @param file [String]
61
+ # @return [SlimLint::Configuration]
62
+ def load_from_file(file)
63
+ hash =
64
+ if (yaml = YAML.load_file(file))
65
+ yaml.to_hash
66
+ else
67
+ {}
68
+ end
69
+
70
+ SlimLint::Configuration.new(hash)
71
+ end
72
+
73
+ # Returns a list of possible configuration files given the context of the
74
+ # specified directory.
75
+ #
76
+ # @param directory [String]
77
+ # @return [Array<Pathname>]
78
+ def possible_config_files(directory)
79
+ files = Pathname.new(directory)
80
+ .enum_for(:ascend)
81
+ .map { |path| path + CONFIG_FILE_NAME }
82
+ files << Pathname.new(CONFIG_FILE_NAME)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Global application constants.
4
+ module SlimLint
5
+ HOME = File.expand_path(File.join(File.dirname(__FILE__), "..", "..")).freeze
6
+ APP_NAME = "slim-lint-standard"
7
+
8
+ REPO_URL = "https://github.com/pvande/slim-lint-standard"
9
+ BUG_REPORT_URL = "#{REPO_URL}/issues"
10
+ end