theme-check 1.1.0 → 1.5.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.
- checksums.yaml +4 -4
- data/.github/workflows/theme-check.yml +5 -9
- data/.gitignore +1 -0
- data/CHANGELOG.md +50 -0
- data/CONTRIBUTING.md +1 -1
- data/RELEASING.md +34 -2
- data/bin/theme-check +29 -0
- data/bin/theme-check-language-server +29 -0
- data/config/default.yml +15 -1
- data/config/theme_app_extension.yml +15 -0
- data/data/shopify_liquid/objects.yml +1 -0
- data/docs/checks/app_block_valid_tags.md +40 -0
- data/docs/checks/asset_size_app_block_css.md +1 -1
- data/docs/checks/deprecate_lazysizes.md +0 -3
- data/docs/checks/deprecated_global_app_block_type.md +65 -0
- data/docs/checks/missing_template.md +25 -0
- data/docs/checks/pagination_size.md +44 -0
- data/docs/checks/template_length.md +1 -1
- data/docs/checks/undefined_object.md +5 -0
- data/lib/theme_check/analyzer.rb +1 -0
- data/lib/theme_check/check.rb +3 -3
- data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
- data/lib/theme_check/checks/asset_size_css.rb +3 -3
- data/lib/theme_check/checks/asset_size_javascript.rb +2 -2
- data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
- data/lib/theme_check/checks/default_locale.rb +3 -1
- data/lib/theme_check/checks/deprecate_bgsizes.rb +1 -1
- data/lib/theme_check/checks/deprecate_lazysizes.rb +7 -4
- data/lib/theme_check/checks/deprecated_global_app_block_type.rb +57 -0
- data/lib/theme_check/checks/img_lazy_loading.rb +1 -1
- data/lib/theme_check/checks/img_width_and_height.rb +3 -3
- data/lib/theme_check/checks/missing_template.rb +21 -5
- data/lib/theme_check/checks/pagination_size.rb +64 -0
- data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
- data/lib/theme_check/checks/remote_asset.rb +3 -3
- data/lib/theme_check/checks/space_inside_braces.rb +27 -7
- data/lib/theme_check/checks/template_length.rb +1 -1
- data/lib/theme_check/checks/undefined_object.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks.rb +11 -1
- data/lib/theme_check/cli.rb +18 -2
- data/lib/theme_check/corrector.rb +9 -0
- data/lib/theme_check/file_system_storage.rb +12 -0
- data/lib/theme_check/html_check.rb +0 -1
- data/lib/theme_check/html_node.rb +37 -16
- data/lib/theme_check/html_visitor.rb +17 -3
- data/lib/theme_check/json_check.rb +2 -2
- data/lib/theme_check/json_file.rb +11 -0
- data/lib/theme_check/json_printer.rb +27 -0
- data/lib/theme_check/language_server/constants.rb +18 -11
- data/lib/theme_check/language_server/document_link_engine.rb +3 -67
- data/lib/theme_check/language_server/document_link_provider.rb +71 -0
- data/lib/theme_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
- data/lib/theme_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
- data/lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
- data/lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
- data/lib/theme_check/language_server/handler.rb +17 -9
- data/lib/theme_check/language_server/server.rb +9 -0
- data/lib/theme_check/language_server/uri_helper.rb +37 -0
- data/lib/theme_check/language_server.rb +6 -0
- data/lib/theme_check/node.rb +6 -4
- data/lib/theme_check/offense.rb +56 -3
- data/lib/theme_check/parsing_helpers.rb +4 -3
- data/lib/theme_check/position.rb +98 -14
- data/lib/theme_check/regex_helpers.rb +5 -2
- data/lib/theme_check/theme.rb +3 -0
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +1 -0
- data/theme-check.gemspec +1 -1
- metadata +20 -6
- data/bin/liquid-server +0 -4
| @@ -8,7 +8,7 @@ module ThemeCheck | |
| 8 8 |  | 
| 9 9 | 
             
                def on_script(node)
         | 
| 10 10 | 
             
                  return unless node.attributes["src"]
         | 
| 11 | 
            -
                  return if node.attributes["defer"] || node.attributes["async"] || node.attributes["type"] | 
| 11 | 
            +
                  return if node.attributes["defer"] || node.attributes["async"] || node.attributes["type"] == "module"
         | 
| 12 12 |  | 
| 13 13 | 
             
                  add_offense("Missing async or defer attribute on script tag", node: node)
         | 
| 14 14 | 
             
                end
         | 
| @@ -14,7 +14,7 @@ module ThemeCheck | |
| 14 14 | 
             
                def on_element(node)
         | 
| 15 15 | 
             
                  return unless TAGS.include?(node.name)
         | 
| 16 16 |  | 
| 17 | 
            -
                  resource_url = node.attributes["src"] | 
| 17 | 
            +
                  resource_url = node.attributes["src"] || node.attributes["href"]
         | 
| 18 18 | 
             
                  return if resource_url.nil? || resource_url.empty?
         | 
| 19 19 |  | 
| 20 20 | 
             
                  # Ignore if URL is Liquid, taken care of by AssetUrlFilters check
         | 
| @@ -23,9 +23,9 @@ module ThemeCheck | |
| 23 23 | 
             
                  return if resource_url =~ RELATIVE_PATH
         | 
| 24 24 | 
             
                  return if url_hosted_by_shopify?(resource_url)
         | 
| 25 25 |  | 
| 26 | 
            -
                  # Ignore non-stylesheet  | 
| 26 | 
            +
                  # Ignore non-stylesheet link tags
         | 
| 27 27 | 
             
                  rel = node.attributes["rel"]
         | 
| 28 | 
            -
                  return if  | 
| 28 | 
            +
                  return if node.name == "link" && rel != "stylesheet"
         | 
| 29 29 |  | 
| 30 30 | 
             
                  add_offense(
         | 
| 31 31 | 
             
                    "Asset should be served by the Shopify CDN for better performance.",
         | 
| @@ -14,18 +14,38 @@ module ThemeCheck | |
| 14 14 | 
             
                  return unless node.markup
         | 
| 15 15 | 
             
                  return if :assign == node.type_name
         | 
| 16 16 |  | 
| 17 | 
            -
                  outside_of_strings(node.markup) do |chunk|
         | 
| 17 | 
            +
                  outside_of_strings(node.markup) do |chunk, chunk_start|
         | 
| 18 18 | 
             
                    chunk.scan(/([,:|]|==|<>|<=|>=|<|>|!=)  +/) do |_match|
         | 
| 19 | 
            -
                      add_offense( | 
| 19 | 
            +
                      add_offense(
         | 
| 20 | 
            +
                        "Too many spaces after '#{Regexp.last_match(1)}'",
         | 
| 21 | 
            +
                        node: node,
         | 
| 22 | 
            +
                        markup: Regexp.last_match(0),
         | 
| 23 | 
            +
                        node_markup_offset: chunk_start + Regexp.last_match.begin(0)
         | 
| 24 | 
            +
                      )
         | 
| 20 25 | 
             
                    end
         | 
| 21 26 | 
             
                    chunk.scan(/([,:|]|==|<>|<=|>=|<\b|>\b|!=)(\S|\z)/) do |_match|
         | 
| 22 | 
            -
                      add_offense( | 
| 27 | 
            +
                      add_offense(
         | 
| 28 | 
            +
                        "Space missing after '#{Regexp.last_match(1)}'",
         | 
| 29 | 
            +
                        node: node,
         | 
| 30 | 
            +
                        markup: Regexp.last_match(0),
         | 
| 31 | 
            +
                        node_markup_offset: chunk_start + Regexp.last_match.begin(0),
         | 
| 32 | 
            +
                      )
         | 
| 23 33 | 
             
                    end
         | 
| 24 34 | 
             
                    chunk.scan(/  (\||==|<>|<=|>=|<|>|!=)+/) do |_match|
         | 
| 25 | 
            -
                      add_offense( | 
| 35 | 
            +
                      add_offense(
         | 
| 36 | 
            +
                        "Too many spaces before '#{Regexp.last_match(1)}'",
         | 
| 37 | 
            +
                        node: node,
         | 
| 38 | 
            +
                        markup: Regexp.last_match(0),
         | 
| 39 | 
            +
                        node_markup_offset: chunk_start + Regexp.last_match.begin(0)
         | 
| 40 | 
            +
                      )
         | 
| 26 41 | 
             
                    end
         | 
| 27 42 | 
             
                    chunk.scan(/(\A|\S)(?<match>\||==|<>|<=|>=|<|\b>|!=)/) do |_match|
         | 
| 28 | 
            -
                      add_offense( | 
| 43 | 
            +
                      add_offense(
         | 
| 44 | 
            +
                        "Space missing before '#{Regexp.last_match(1)}'",
         | 
| 45 | 
            +
                        node: node,
         | 
| 46 | 
            +
                        markup: Regexp.last_match(0),
         | 
| 47 | 
            +
                        node_markup_offset: chunk_start + Regexp.last_match.begin(0)
         | 
| 48 | 
            +
                      )
         | 
| 29 49 | 
             
                    end
         | 
| 30 50 | 
             
                  end
         | 
| 31 51 | 
             
                end
         | 
| @@ -51,13 +71,13 @@ module ThemeCheck | |
| 51 71 | 
             
                end
         | 
| 52 72 |  | 
| 53 73 | 
             
                def on_variable(node)
         | 
| 54 | 
            -
                  return if @ignore
         | 
| 74 | 
            +
                  return if @ignore || node.markup.empty?
         | 
| 55 75 | 
             
                  if node.markup[0] != " "
         | 
| 56 76 | 
             
                    add_offense("Space missing after '{{'", node: node) do |corrector|
         | 
| 57 77 | 
             
                      corrector.insert_before(node, " ")
         | 
| 58 78 | 
             
                    end
         | 
| 59 79 | 
             
                  end
         | 
| 60 | 
            -
                  if node.markup[-1] != " "
         | 
| 80 | 
            +
                  if node.markup[-1] != " " && node.markup[-1] != "\n"
         | 
| 61 81 | 
             
                    add_offense("Space missing before '}}'", node: node) do |corrector|
         | 
| 62 82 | 
             
                      corrector.insert_after(node, " ")
         | 
| 63 83 | 
             
                    end
         | 
| @@ -5,7 +5,7 @@ module ThemeCheck | |
| 5 5 | 
             
                category :liquid
         | 
| 6 6 | 
             
                doc docs_url(__FILE__)
         | 
| 7 7 |  | 
| 8 | 
            -
                def initialize(max_length:  | 
| 8 | 
            +
                def initialize(max_length: 600, exclude_schema: true, exclude_stylesheet: true, exclude_javascript: true)
         | 
| 9 9 | 
             
                  @max_length = max_length
         | 
| 10 10 | 
             
                  @exclude_schema = exclude_schema
         | 
| 11 11 | 
             
                  @exclude_stylesheet = exclude_stylesheet
         | 
    
        data/lib/theme_check/checks.rb
    CHANGED
    
    | @@ -29,8 +29,18 @@ module ThemeCheck | |
| 29 29 | 
             
                def call_check_method(check, method, *args)
         | 
| 30 30 | 
             
                  return unless check.respond_to?(method) && !check.ignored?
         | 
| 31 31 |  | 
| 32 | 
            -
                   | 
| 32 | 
            +
                  # If you want to use binding.pry in unit tests, define the
         | 
| 33 | 
            +
                  # THEME_CHECK_DEBUG environment variable. e.g.
         | 
| 34 | 
            +
                  #
         | 
| 35 | 
            +
                  #   $ export THEME_CHECK_DEBUG=true
         | 
| 36 | 
            +
                  #   $ bundle exec rake tests:in_memory
         | 
| 37 | 
            +
                  #
         | 
| 38 | 
            +
                  if ENV['THEME_CHECK_DEBUG']
         | 
| 33 39 | 
             
                    check.send(method, *args)
         | 
| 40 | 
            +
                  else
         | 
| 41 | 
            +
                    Timeout.timeout(CHECK_METHOD_TIMEOUT) do
         | 
| 42 | 
            +
                      check.send(method, *args)
         | 
| 43 | 
            +
                    end
         | 
| 34 44 | 
             
                  end
         | 
| 35 45 | 
             
                rescue Liquid::Error
         | 
| 36 46 | 
             
                  # Pass-through Liquid errors
         | 
    
        data/lib/theme_check/cli.rb
    CHANGED
    
    | @@ -5,6 +5,8 @@ module ThemeCheck | |
| 5 5 | 
             
              class Cli
         | 
| 6 6 | 
             
                class Abort < StandardError; end
         | 
| 7 7 |  | 
| 8 | 
            +
                FORMATS = [:text, :json]
         | 
| 9 | 
            +
             | 
| 8 10 | 
             
                attr_accessor :path
         | 
| 9 11 |  | 
| 10 12 | 
             
                def initialize
         | 
| @@ -15,6 +17,7 @@ module ThemeCheck | |
| 15 17 | 
             
                  @auto_correct = false
         | 
| 16 18 | 
             
                  @config_path = nil
         | 
| 17 19 | 
             
                  @fail_level = :error
         | 
| 20 | 
            +
                  @format = :text
         | 
| 18 21 | 
             
                end
         | 
| 19 22 |  | 
| 20 23 | 
             
                def option_parser(parser = OptionParser.new, help: true)
         | 
| @@ -29,6 +32,10 @@ module ThemeCheck | |
| 29 32 | 
             
                    "Use the config provided, overriding .theme-check.yml if present",
         | 
| 30 33 | 
             
                    "Use :theme_app_extension to use default checks for theme app extensions"
         | 
| 31 34 | 
             
                  ) { |path| @config_path = path }
         | 
| 35 | 
            +
                  @option_parser.on(
         | 
| 36 | 
            +
                    "-o", "--output FORMAT", FORMATS,
         | 
| 37 | 
            +
                    "The output format to use. (text|json, default: text)"
         | 
| 38 | 
            +
                  ) { |format| @format = format.to_sym }
         | 
| 32 39 | 
             
                  @option_parser.on(
         | 
| 33 40 | 
             
                    "-c", "--category CATEGORY", Check::CATEGORIES, "Only run this category of checks",
         | 
| 34 41 | 
             
                    "Runs checks matching all categories when specified more than once"
         | 
| @@ -166,7 +173,7 @@ module ThemeCheck | |
| 166 173 | 
             
                end
         | 
| 167 174 |  | 
| 168 175 | 
             
                def check
         | 
| 169 | 
            -
                  puts "Checking #{@config.root} ..."
         | 
| 176 | 
            +
                  STDERR.puts "Checking #{@config.root} ..."
         | 
| 170 177 | 
             
                  storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
         | 
| 171 178 | 
             
                  theme = ThemeCheck::Theme.new(storage)
         | 
| 172 179 | 
             
                  if theme.all.empty?
         | 
| @@ -175,10 +182,19 @@ module ThemeCheck | |
| 175 182 | 
             
                  analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
         | 
| 176 183 | 
             
                  analyzer.analyze_theme
         | 
| 177 184 | 
             
                  analyzer.correct_offenses
         | 
| 178 | 
            -
                   | 
| 185 | 
            +
                  output_with_format(theme, analyzer)
         | 
| 179 186 | 
             
                  raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
         | 
| 180 187 | 
             
                    offense.check.severity_value <= Check.severity_value(@fail_level)
         | 
| 181 188 | 
             
                  end
         | 
| 182 189 | 
             
                end
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                def output_with_format(theme, analyzer)
         | 
| 192 | 
            +
                  case @format
         | 
| 193 | 
            +
                  when :text
         | 
| 194 | 
            +
                    ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
         | 
| 195 | 
            +
                  when :json
         | 
| 196 | 
            +
                    ThemeCheck::JsonPrinter.new.print(analyzer.offenses)
         | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
                end
         | 
| 183 199 | 
             
              end
         | 
| 184 200 | 
             
            end
         | 
| @@ -27,5 +27,14 @@ module ThemeCheck | |
| 27 27 | 
             
                  line.insert(node.range[0], insert_before)
         | 
| 28 28 | 
             
                  line.insert(node.range[1] + 1 + insert_before.length, insert_after)
         | 
| 29 29 | 
             
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def create(theme, relative_path, content)
         | 
| 32 | 
            +
                  theme.storage.write(relative_path, content)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def create_default_locale_json(theme)
         | 
| 36 | 
            +
                  theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
         | 
| 37 | 
            +
                  theme.default_locale_json.update_contents('{}')
         | 
| 38 | 
            +
                end
         | 
| 30 39 | 
             
              end
         | 
| 31 40 | 
             
            end
         | 
| @@ -20,6 +20,9 @@ module ThemeCheck | |
| 20 20 | 
             
                end
         | 
| 21 21 |  | 
| 22 22 | 
             
                def write(relative_path, content)
         | 
| 23 | 
            +
                  reset_memoizers unless file_exists?(relative_path)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  file(relative_path).dirname.mkpath unless file(relative_path).dirname.directory?
         | 
| 23 26 | 
             
                  file(relative_path).write(content)
         | 
| 24 27 | 
             
                end
         | 
| 25 28 |  | 
| @@ -36,6 +39,15 @@ module ThemeCheck | |
| 36 39 |  | 
| 37 40 | 
             
                private
         | 
| 38 41 |  | 
| 42 | 
            +
                def file_exists?(relative_path)
         | 
| 43 | 
            +
                  !!@files[relative_path]
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def reset_memoizers
         | 
| 47 | 
            +
                  @file_array = nil
         | 
| 48 | 
            +
                  @directories = nil
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 39 51 | 
             
                def glob(pattern)
         | 
| 40 52 | 
             
                  @root.glob(pattern).reject do |path|
         | 
| 41 53 | 
             
                    relative_path = path.relative_path_from(@root)
         | 
| @@ -4,13 +4,14 @@ require "forwardable" | |
| 4 4 | 
             
            module ThemeCheck
         | 
| 5 5 | 
             
              class HtmlNode
         | 
| 6 6 | 
             
                extend Forwardable
         | 
| 7 | 
            -
                 | 
| 7 | 
            +
                include RegexHelpers
         | 
| 8 | 
            +
                attr_reader :template, :parent
         | 
| 8 9 |  | 
| 9 | 
            -
                 | 
| 10 | 
            -
             | 
| 11 | 
            -
                def initialize(value, template)
         | 
| 10 | 
            +
                def initialize(value, template, placeholder_values = [], parent = nil)
         | 
| 12 11 | 
             
                  @value = value
         | 
| 13 12 | 
             
                  @template = template
         | 
| 13 | 
            +
                  @placeholder_values = placeholder_values
         | 
| 14 | 
            +
                  @parent = parent
         | 
| 14 15 | 
             
                end
         | 
| 15 16 |  | 
| 16 17 | 
             
                def literal?
         | 
| @@ -22,35 +23,55 @@ module ThemeCheck | |
| 22 23 | 
             
                end
         | 
| 23 24 |  | 
| 24 25 | 
             
                def children
         | 
| 25 | 
            -
                  @ | 
| 26 | 
            +
                  @children ||= @value
         | 
| 27 | 
            +
                    .children
         | 
| 28 | 
            +
                    .map { |child| HtmlNode.new(child, template, @placeholder_values, self) }
         | 
| 26 29 | 
             
                end
         | 
| 27 30 |  | 
| 28 | 
            -
                def  | 
| 29 | 
            -
                   | 
| 31 | 
            +
                def attributes
         | 
| 32 | 
            +
                  @attributes ||= @value.attributes
         | 
| 33 | 
            +
                    .map { |k, v| [replace_placeholders(k), replace_placeholders(v.value)] }
         | 
| 34 | 
            +
                    .to_h
         | 
| 30 35 | 
             
                end
         | 
| 31 36 |  | 
| 32 | 
            -
                def  | 
| 33 | 
            -
                   | 
| 34 | 
            -
                    "document"
         | 
| 35 | 
            -
                  else
         | 
| 36 | 
            -
                    @value.name
         | 
| 37 | 
            -
                  end
         | 
| 37 | 
            +
                def content
         | 
| 38 | 
            +
                  @content ||= replace_placeholders(@value.content)
         | 
| 38 39 | 
             
                end
         | 
| 39 40 |  | 
| 41 | 
            +
                # @value is not forwarded because we _need_ to replace the
         | 
| 42 | 
            +
                # placeholders for the HtmlNode to make sense.
         | 
| 40 43 | 
             
                def value
         | 
| 41 44 | 
             
                  if literal?
         | 
| 42 | 
            -
                     | 
| 45 | 
            +
                    content
         | 
| 43 46 | 
             
                  else
         | 
| 44 | 
            -
                     | 
| 47 | 
            +
                    markup
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def name
         | 
| 52 | 
            +
                  if @value.name == "#document-fragment"
         | 
| 53 | 
            +
                    "document"
         | 
| 54 | 
            +
                  else
         | 
| 55 | 
            +
                    @value.name
         | 
| 45 56 | 
             
                  end
         | 
| 46 57 | 
             
                end
         | 
| 47 58 |  | 
| 48 59 | 
             
                def markup
         | 
| 49 | 
            -
                  @value.to_html
         | 
| 60 | 
            +
                  @markup ||= replace_placeholders(@value.to_html)
         | 
| 50 61 | 
             
                end
         | 
| 51 62 |  | 
| 52 63 | 
             
                def line_number
         | 
| 53 64 | 
             
                  @value.line
         | 
| 54 65 | 
             
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                private
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def replace_placeholders(string)
         | 
| 70 | 
            +
                  # Replace all {%#{i}####%} with the actual content.
         | 
| 71 | 
            +
                  string.gsub(LIQUID_TAG) do |match|
         | 
| 72 | 
            +
                    key = /\d+/.match(match)[0]
         | 
| 73 | 
            +
                    @placeholder_values[key.to_i]
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
                end
         | 
| 55 76 | 
             
              end
         | 
| 56 77 | 
             
            end
         | 
| @@ -1,18 +1,20 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
            require " | 
| 2 | 
            +
            require "nokogiri"
         | 
| 3 3 | 
             
            require "forwardable"
         | 
| 4 4 |  | 
| 5 5 | 
             
            module ThemeCheck
         | 
| 6 6 | 
             
              class HtmlVisitor
         | 
| 7 | 
            +
                include RegexHelpers
         | 
| 7 8 | 
             
                attr_reader :checks
         | 
| 8 9 |  | 
| 9 10 | 
             
                def initialize(checks)
         | 
| 10 11 | 
             
                  @checks = checks
         | 
| 12 | 
            +
                  @placeholder_values = []
         | 
| 11 13 | 
             
                end
         | 
| 12 14 |  | 
| 13 15 | 
             
                def visit_template(template)
         | 
| 14 16 | 
             
                  doc = parse(template)
         | 
| 15 | 
            -
                  visit(HtmlNode.new(doc, template))
         | 
| 17 | 
            +
                  visit(HtmlNode.new(doc, template, @placeholder_values))
         | 
| 16 18 | 
             
                rescue ArgumentError => e
         | 
| 17 19 | 
             
                  call_checks(:on_parse_error, e, template)
         | 
| 18 20 | 
             
                end
         | 
| @@ -20,7 +22,19 @@ module ThemeCheck | |
| 20 22 | 
             
                private
         | 
| 21 23 |  | 
| 22 24 | 
             
                def parse(template)
         | 
| 23 | 
            -
                   | 
| 25 | 
            +
                  parseable_source = +template.source.clone
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  # Replace all liquid tags with {%#{i}######%} to prevent the HTML
         | 
| 28 | 
            +
                  # parser from freaking out. We transparently replace those placeholders in
         | 
| 29 | 
            +
                  # HtmlNode.
         | 
| 30 | 
            +
                  matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
         | 
| 31 | 
            +
                    value = m[0]
         | 
| 32 | 
            +
                    @placeholder_values.push(value)
         | 
| 33 | 
            +
                    key = (@placeholder_values.size - 1).to_s
         | 
| 34 | 
            +
                    parseable_source[m.begin(0)...m.end(0)] = "{%#{key.ljust(m.end(0) - m.begin(0) - 4, '#')}%}"
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400)
         | 
| 24 38 | 
             
                end
         | 
| 25 39 |  | 
| 26 40 | 
             
                def visit(node)
         | 
| @@ -4,8 +4,8 @@ module ThemeCheck | |
| 4 4 | 
             
              class JsonCheck < Check
         | 
| 5 5 | 
             
                extend ChecksTracking
         | 
| 6 6 |  | 
| 7 | 
            -
                def add_offense(message, markup: nil, line_number: nil, template: nil)
         | 
| 8 | 
            -
                  offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, template: template)
         | 
| 7 | 
            +
                def add_offense(message, markup: nil, line_number: nil, template: nil, &block)
         | 
| 8 | 
            +
                  offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, template: template, correction: block)
         | 
| 9 9 | 
             
                end
         | 
| 10 10 | 
             
              end
         | 
| 11 11 | 
             
            end
         | 
| @@ -20,6 +20,17 @@ module ThemeCheck | |
| 20 20 | 
             
                  @parser_error
         | 
| 21 21 | 
             
                end
         | 
| 22 22 |  | 
| 23 | 
            +
                def update_contents(new_content = '{}')
         | 
| 24 | 
            +
                  @content = new_content
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def write
         | 
| 28 | 
            +
                  if source != @content
         | 
| 29 | 
            +
                    @storage.write(@relative_path, content)
         | 
| 30 | 
            +
                    @source = content
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 23 34 | 
             
                def json?
         | 
| 24 35 | 
             
                  true
         | 
| 25 36 | 
             
                end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'json'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module ThemeCheck
         | 
| 5 | 
            +
              class JsonPrinter
         | 
| 6 | 
            +
                def print(offenses)
         | 
| 7 | 
            +
                  json = offenses_by_path(offenses)
         | 
| 8 | 
            +
                  puts JSON.dump(json)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def offenses_by_path(offenses)
         | 
| 12 | 
            +
                  offenses
         | 
| 13 | 
            +
                    .map(&:to_h)
         | 
| 14 | 
            +
                    .group_by { |offense| offense[:path] }
         | 
| 15 | 
            +
                    .map do |(path, path_offenses)|
         | 
| 16 | 
            +
                      {
         | 
| 17 | 
            +
                        path: path,
         | 
| 18 | 
            +
                        offenses: path_offenses.map { |offense| offense.filter { |k, _v| k != :path } },
         | 
| 19 | 
            +
                        errorCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:error] },
         | 
| 20 | 
            +
                        suggestionCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:suggestion] },
         | 
| 21 | 
            +
                        styleCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:style] },
         | 
| 22 | 
            +
                      }
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                    .sort_by { |o| o[:path] }
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -2,21 +2,28 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module ThemeCheck
         | 
| 4 4 | 
             
              module LanguageServer
         | 
| 5 | 
            -
                 | 
| 6 | 
            -
                   | 
| 7 | 
            -
             | 
| 5 | 
            +
                def self.partial_tag(tag)
         | 
| 6 | 
            +
                  %r{
         | 
| 7 | 
            +
                    \{\%-?\s*#{tag}\s+'(?<partial>[^']*)'|
         | 
| 8 | 
            +
                    \{\%-?\s*#{tag}\s+"(?<partial>[^"]*)"|
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    # in liquid tags the whole line is white space until the tag
         | 
| 11 | 
            +
                    ^\s*#{tag}\s+'(?<partial>[^']*)'|
         | 
| 12 | 
            +
                    ^\s*#{tag}\s+"(?<partial>[^"]*)"
         | 
| 13 | 
            +
                  }mix
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                PARTIAL_RENDER = partial_tag('render')
         | 
| 17 | 
            +
                PARTIAL_INCLUDE = partial_tag('include')
         | 
| 18 | 
            +
                PARTIAL_SECTION = partial_tag('section')
         | 
| 8 19 |  | 
| 9 | 
            -
                  # in liquid tags the whole line is white space until render
         | 
| 10 | 
            -
                  ^\s*render\s+'(?<partial>[^']*)'|
         | 
| 11 | 
            -
                  ^\s*render\s+"(?<partial>[^"]*)"
         | 
| 12 | 
            -
                }mix
         | 
| 13 20 | 
             
                ASSET_INCLUDE = %r{
         | 
| 14 | 
            -
                  \{ | 
| 15 | 
            -
                  \{ | 
| 21 | 
            +
                  \{\{-?\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
         | 
| 22 | 
            +
                  \{\{-?\s*"(?<partial>[^"]*)"\s*\|\s*asset_url|
         | 
| 16 23 |  | 
| 17 24 | 
             
                  # in liquid tags the whole line is white space until the asset partial
         | 
| 18 | 
            -
                  ^\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
         | 
| 19 | 
            -
                  ^\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
         | 
| 25 | 
            +
                  ^\s*(?:echo|assign[^=]*\=)\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
         | 
| 26 | 
            +
                  ^\s*(?:echo|assign[^=]*\=)\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
         | 
| 20 27 | 
             
                }mix
         | 
| 21 28 | 
             
              end
         | 
| 22 29 | 
             
            end
         |