theme-check 0.8.0 → 0.9.1
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 +3 -0
- data/CHANGELOG.md +44 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +4 -1
- data/RELEASING.md +5 -3
- data/config/default.yml +42 -1
- data/data/shopify_liquid/tags.yml +3 -0
- data/data/shopify_translation_keys.yml +1 -0
- data/docs/checks/asset_url_filters.md +56 -0
- data/docs/checks/content_for_header_modification.md +42 -0
- data/docs/checks/nested_snippet.md +1 -1
- data/docs/checks/parser_blocking_script_tag.md +53 -0
- data/docs/checks/space_inside_braces.md +28 -0
- data/exe/theme-check-language-server +1 -2
- data/lib/theme_check.rb +13 -1
- data/lib/theme_check/analyzer.rb +79 -13
- data/lib/theme_check/bug.rb +20 -0
- data/lib/theme_check/check.rb +36 -7
- data/lib/theme_check/checks.rb +47 -8
- data/lib/theme_check/checks/asset_url_filters.rb +46 -0
- data/lib/theme_check/checks/content_for_header_modification.rb +41 -0
- data/lib/theme_check/checks/img_width_and_height.rb +18 -49
- data/lib/theme_check/checks/missing_enable_comment.rb +4 -4
- data/lib/theme_check/checks/missing_template.rb +1 -0
- data/lib/theme_check/checks/nested_snippet.rb +1 -1
- data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -38
- data/lib/theme_check/checks/parser_blocking_script_tag.rb +20 -0
- data/lib/theme_check/checks/remote_asset.rb +21 -79
- data/lib/theme_check/checks/space_inside_braces.rb +8 -2
- data/lib/theme_check/checks/template_length.rb +3 -0
- data/lib/theme_check/checks/valid_html_translation.rb +1 -0
- data/lib/theme_check/config.rb +2 -0
- data/lib/theme_check/disabled_check.rb +41 -0
- data/lib/theme_check/disabled_checks.rb +33 -29
- data/lib/theme_check/exceptions.rb +32 -0
- data/lib/theme_check/html_check.rb +7 -0
- data/lib/theme_check/html_node.rb +56 -0
- data/lib/theme_check/html_visitor.rb +38 -0
- data/lib/theme_check/json_file.rb +13 -1
- data/lib/theme_check/language_server.rb +2 -1
- data/lib/theme_check/language_server/completion_engine.rb +1 -1
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -8
- data/lib/theme_check/language_server/constants.rb +5 -1
- data/lib/theme_check/language_server/diagnostics_tracker.rb +64 -0
- data/lib/theme_check/language_server/document_link_engine.rb +2 -2
- data/lib/theme_check/language_server/handler.rb +63 -50
- data/lib/theme_check/language_server/server.rb +1 -1
- data/lib/theme_check/language_server/variable_lookup_finder.rb +295 -0
- data/lib/theme_check/liquid_check.rb +1 -4
- data/lib/theme_check/node.rb +12 -0
- data/lib/theme_check/offense.rb +30 -46
- data/lib/theme_check/position.rb +77 -0
- data/lib/theme_check/position_helper.rb +37 -0
- data/lib/theme_check/remote_asset_file.rb +3 -0
- data/lib/theme_check/shopify_liquid/tag.rb +13 -0
- data/lib/theme_check/template.rb +8 -0
- data/lib/theme_check/theme.rb +7 -2
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check/visitor.rb +4 -14
- metadata +19 -4
- data/lib/theme_check/language_server/position_helper.rb +0 -27
    
        data/lib/theme_check.rb
    CHANGED
    
    | @@ -1,10 +1,14 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 | 
             
            require "liquid"
         | 
| 3 3 |  | 
| 4 | 
            +
            require_relative "theme_check/version"
         | 
| 5 | 
            +
            require_relative "theme_check/bug"
         | 
| 6 | 
            +
            require_relative "theme_check/exceptions"
         | 
| 4 7 | 
             
            require_relative "theme_check/analyzer"
         | 
| 5 8 | 
             
            require_relative "theme_check/check"
         | 
| 6 9 | 
             
            require_relative "theme_check/checks_tracking"
         | 
| 7 10 | 
             
            require_relative "theme_check/cli"
         | 
| 11 | 
            +
            require_relative "theme_check/disabled_check"
         | 
| 8 12 | 
             
            require_relative "theme_check/disabled_checks"
         | 
| 9 13 | 
             
            require_relative "theme_check/liquid_check"
         | 
| 10 14 | 
             
            require_relative "theme_check/locale_diff"
         | 
| @@ -14,6 +18,8 @@ require_relative "theme_check/regex_helpers" | |
| 14 18 | 
             
            require_relative "theme_check/json_check"
         | 
| 15 19 | 
             
            require_relative "theme_check/json_file"
         | 
| 16 20 | 
             
            require_relative "theme_check/json_helpers"
         | 
| 21 | 
            +
            require_relative "theme_check/position_helper"
         | 
| 22 | 
            +
            require_relative "theme_check/position"
         | 
| 17 23 | 
             
            require_relative "theme_check/language_server"
         | 
| 18 24 | 
             
            require_relative "theme_check/checks"
         | 
| 19 25 | 
             
            require_relative "theme_check/config"
         | 
| @@ -30,6 +36,12 @@ require_relative "theme_check/template" | |
| 30 36 | 
             
            require_relative "theme_check/theme"
         | 
| 31 37 | 
             
            require_relative "theme_check/visitor"
         | 
| 32 38 | 
             
            require_relative "theme_check/corrector"
         | 
| 33 | 
            -
            require_relative "theme_check/ | 
| 39 | 
            +
            require_relative "theme_check/html_node"
         | 
| 40 | 
            +
            require_relative "theme_check/html_visitor"
         | 
| 41 | 
            +
            require_relative "theme_check/html_check"
         | 
| 34 42 |  | 
| 35 43 | 
             
            Dir[__dir__ + "/theme_check/checks/*.rb"].each { |file| require file }
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            # UTF-8 is the default internal and external encoding, like in Rails & Shopify.
         | 
| 46 | 
            +
            Encoding.default_external = Encoding::UTF_8
         | 
| 47 | 
            +
            Encoding.default_internal = Encoding::UTF_8
         | 
    
        data/lib/theme_check/analyzer.rb
    CHANGED
    
    | @@ -1,53 +1,119 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 | 
             
            module ThemeCheck
         | 
| 3 3 | 
             
              class Analyzer
         | 
| 4 | 
            -
                attr_reader :offenses
         | 
| 5 | 
            -
             | 
| 6 4 | 
             
                def initialize(theme, checks = Check.all.map(&:new), auto_correct = false)
         | 
| 7 5 | 
             
                  @theme = theme
         | 
| 8 | 
            -
                  @offenses = []
         | 
| 9 6 | 
             
                  @auto_correct = auto_correct
         | 
| 10 7 |  | 
| 11 8 | 
             
                  @liquid_checks = Checks.new
         | 
| 12 9 | 
             
                  @json_checks = Checks.new
         | 
| 10 | 
            +
                  @html_checks = Checks.new
         | 
| 13 11 |  | 
| 14 12 | 
             
                  checks.each do |check|
         | 
| 15 13 | 
             
                    check.theme = @theme
         | 
| 16 | 
            -
                    check.offenses = @offenses
         | 
| 17 14 |  | 
| 18 15 | 
             
                    case check
         | 
| 19 16 | 
             
                    when LiquidCheck
         | 
| 20 17 | 
             
                      @liquid_checks << check
         | 
| 21 18 | 
             
                    when JsonCheck
         | 
| 22 19 | 
             
                      @json_checks << check
         | 
| 20 | 
            +
                    when HtmlCheck
         | 
| 21 | 
            +
                      @html_checks << check
         | 
| 23 22 | 
             
                    end
         | 
| 24 23 | 
             
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 25 |  | 
| 26 | 
            -
             | 
| 26 | 
            +
                def offenses
         | 
| 27 | 
            +
                  @liquid_checks.flat_map(&:offenses) +
         | 
| 28 | 
            +
                    @json_checks.flat_map(&:offenses) +
         | 
| 29 | 
            +
                    @html_checks.flat_map(&:offenses)
         | 
| 27 30 | 
             
                end
         | 
| 28 31 |  | 
| 29 32 | 
             
                def analyze_theme
         | 
| 30 | 
            -
                   | 
| 31 | 
            -
             | 
| 33 | 
            +
                  reset
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  liquid_visitor = Visitor.new(@liquid_checks, @disabled_checks)
         | 
| 36 | 
            +
                  html_visitor = HtmlVisitor.new(@html_checks)
         | 
| 37 | 
            +
                  @theme.liquid.each do |template|
         | 
| 38 | 
            +
                    liquid_visitor.visit_template(template)
         | 
| 39 | 
            +
                    html_visitor.visit_template(template)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 32 42 | 
             
                  @theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                   | 
| 35 | 
            -
             | 
| 43 | 
            +
             | 
| 44 | 
            +
                  finish
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def analyze_files(files)
         | 
| 48 | 
            +
                  reset
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  # Call all checks that run on the whole theme
         | 
| 51 | 
            +
                  liquid_visitor = Visitor.new(@liquid_checks.whole_theme, @disabled_checks)
         | 
| 52 | 
            +
                  html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
         | 
| 53 | 
            +
                  @theme.liquid.each do |template|
         | 
| 54 | 
            +
                    liquid_visitor.visit_template(template)
         | 
| 55 | 
            +
                    html_visitor.visit_template(template)
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                  @theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  # Call checks that run on a single files, only on specified file
         | 
| 60 | 
            +
                  liquid_visitor = Visitor.new(@liquid_checks.single_file, @disabled_checks)
         | 
| 61 | 
            +
                  html_visitor = HtmlVisitor.new(@html_checks.single_file)
         | 
| 62 | 
            +
                  files.each do |file|
         | 
| 63 | 
            +
                    if file.liquid?
         | 
| 64 | 
            +
                      liquid_visitor.visit_template(file)
         | 
| 65 | 
            +
                      html_visitor.visit_template(file)
         | 
| 66 | 
            +
                    elsif file.json?
         | 
| 67 | 
            +
                      @json_checks.single_file.call(:on_file, file)
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  finish
         | 
| 36 72 | 
             
                end
         | 
| 37 73 |  | 
| 38 74 | 
             
                def uncorrectable_offenses
         | 
| 39 75 | 
             
                  unless @auto_correct
         | 
| 40 | 
            -
                    return  | 
| 76 | 
            +
                    return offenses
         | 
| 41 77 | 
             
                  end
         | 
| 42 78 |  | 
| 43 | 
            -
                   | 
| 79 | 
            +
                  offenses.select { |offense| !offense.correctable? }
         | 
| 44 80 | 
             
                end
         | 
| 45 81 |  | 
| 46 82 | 
             
                def correct_offenses
         | 
| 47 83 | 
             
                  if @auto_correct
         | 
| 48 | 
            -
                     | 
| 84 | 
            +
                    offenses.each(&:correct)
         | 
| 49 85 | 
             
                    @theme.liquid.each(&:write)
         | 
| 50 86 | 
             
                  end
         | 
| 51 87 | 
             
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                private
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def reset
         | 
| 92 | 
            +
                  @disabled_checks = DisabledChecks.new
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  @liquid_checks.each do |check|
         | 
| 95 | 
            +
                    check.offenses.clear
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  @html_checks.each do |check|
         | 
| 99 | 
            +
                    check.offenses.clear
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  @json_checks.each do |check|
         | 
| 103 | 
            +
                    check.offenses.clear
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def finish
         | 
| 108 | 
            +
                  @liquid_checks.call(:on_end)
         | 
| 109 | 
            +
                  @html_checks.call(:on_end)
         | 
| 110 | 
            +
                  @json_checks.call(:on_end)
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  @disabled_checks.remove_disabled_offenses(@liquid_checks)
         | 
| 113 | 
            +
                  @disabled_checks.remove_disabled_offenses(@json_checks)
         | 
| 114 | 
            +
                  @disabled_checks.remove_disabled_offenses(@html_checks)
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  offenses
         | 
| 117 | 
            +
                end
         | 
| 52 118 | 
             
              end
         | 
| 53 119 | 
             
            end
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'theme_check/version'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module ThemeCheck
         | 
| 5 | 
            +
              BUG_POSTAMBLE = <<~EOS
         | 
| 6 | 
            +
                Theme Check Version: #{VERSION}
         | 
| 7 | 
            +
                Ruby Version: #{RUBY_VERSION}
         | 
| 8 | 
            +
                Platform: #{RUBY_PLATFORM}
         | 
| 9 | 
            +
                Muffin mode: activated
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                ------------------------
         | 
| 12 | 
            +
                Whoops! It looks like you found a bug in Theme Check.
         | 
| 13 | 
            +
                Please report it at https://github.com/Shopify/theme-check/issues, and include the message above.
         | 
| 14 | 
            +
                Or cross your fingers real hard, and try again.
         | 
| 15 | 
            +
              EOS
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              def self.bug(message)
         | 
| 18 | 
            +
                abort(message + BUG_POSTAMBLE)
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
            end
         | 
    
        data/lib/theme_check/check.rb
    CHANGED
    
    | @@ -6,8 +6,8 @@ module ThemeCheck | |
| 6 6 | 
             
                include JsonHelpers
         | 
| 7 7 |  | 
| 8 8 | 
             
                attr_accessor :theme
         | 
| 9 | 
            -
                attr_accessor : | 
| 10 | 
            -
                 | 
| 9 | 
            +
                attr_accessor :options, :ignored_patterns
         | 
| 10 | 
            +
                attr_writer :offenses
         | 
| 11 11 |  | 
| 12 12 | 
             
                SEVERITIES = [
         | 
| 13 13 | 
             
                  :error,
         | 
| @@ -19,6 +19,7 @@ module ThemeCheck | |
| 19 19 | 
             
                  :liquid,
         | 
| 20 20 | 
             
                  :translation,
         | 
| 21 21 | 
             
                  :performance,
         | 
| 22 | 
            +
                  :html,
         | 
| 22 23 | 
             
                  :json,
         | 
| 23 24 | 
             
                  :performance,
         | 
| 24 25 | 
             
                ]
         | 
| @@ -67,6 +68,21 @@ module ThemeCheck | |
| 67 68 | 
             
                    end
         | 
| 68 69 | 
             
                    defined?(@can_disable) ? @can_disable : true
         | 
| 69 70 | 
             
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def single_file(single_file = nil)
         | 
| 73 | 
            +
                    unless single_file.nil?
         | 
| 74 | 
            +
                      @single_file = single_file
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                    defined?(@single_file) ? @single_file : !method_defined?(:on_end)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def offenses
         | 
| 81 | 
            +
                  @offenses ||= []
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
         | 
| 85 | 
            +
                  offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
         | 
| 70 86 | 
             
                end
         | 
| 71 87 |  | 
| 72 88 | 
             
                def severity
         | 
| @@ -89,10 +105,6 @@ module ThemeCheck | |
| 89 105 | 
             
                  @ignored = true
         | 
| 90 106 | 
             
                end
         | 
| 91 107 |  | 
| 92 | 
            -
                def unignore!
         | 
| 93 | 
            -
                  @ignored = false
         | 
| 94 | 
            -
                end
         | 
| 95 | 
            -
             | 
| 96 108 | 
             
                def ignored?
         | 
| 97 109 | 
             
                  defined?(@ignored) && @ignored
         | 
| 98 110 | 
             
                end
         | 
| @@ -101,9 +113,26 @@ module ThemeCheck | |
| 101 113 | 
             
                  self.class.can_disable
         | 
| 102 114 | 
             
                end
         | 
| 103 115 |  | 
| 116 | 
            +
                def single_file?
         | 
| 117 | 
            +
                  self.class.single_file
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                def whole_theme?
         | 
| 121 | 
            +
                  !single_file?
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                def ==(other)
         | 
| 125 | 
            +
                  other.is_a?(Check) && code_name == other.code_name
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 104 128 | 
             
                def to_s
         | 
| 105 129 | 
             
                  s = +"#{code_name}:\n"
         | 
| 106 | 
            -
                  properties = { | 
| 130 | 
            +
                  properties = {
         | 
| 131 | 
            +
                    severity: severity,
         | 
| 132 | 
            +
                    categories: categories,
         | 
| 133 | 
            +
                    doc: doc,
         | 
| 134 | 
            +
                    ignored_patterns: ignored_patterns,
         | 
| 135 | 
            +
                  }.merge(options)
         | 
| 107 136 | 
             
                  properties.each_pair do |name, value|
         | 
| 108 137 | 
             
                    s << "  #{name}: #{value}\n" if value
         | 
| 109 138 | 
             
                  end
         | 
    
        data/lib/theme_check/checks.rb
    CHANGED
    
    | @@ -1,22 +1,61 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "pp"
         | 
| 3 | 
            +
            require "timeout"
         | 
| 4 | 
            +
             | 
| 2 5 | 
             
            module ThemeCheck
         | 
| 3 6 | 
             
              class Checks < Array
         | 
| 7 | 
            +
                CHECK_METHOD_TIMEOUT = 5 # sec
         | 
| 8 | 
            +
             | 
| 4 9 | 
             
                def call(method, *args)
         | 
| 5 10 | 
             
                  each do |check|
         | 
| 6 | 
            -
                     | 
| 7 | 
            -
                      check.send(method, *args)
         | 
| 8 | 
            -
                    end
         | 
| 11 | 
            +
                    call_check_method(check, method, *args)
         | 
| 9 12 | 
             
                  end
         | 
| 10 13 | 
             
                end
         | 
| 11 14 |  | 
| 12 | 
            -
                def  | 
| 13 | 
            -
                  self.class.new( | 
| 15 | 
            +
                def disableable
         | 
| 16 | 
            +
                  @disableable ||= self.class.new(select(&:can_disable?))
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def whole_theme
         | 
| 20 | 
            +
                  @whole_theme ||= self.class.new(select(&:whole_theme?))
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def single_file
         | 
| 24 | 
            +
                  @single_file ||= self.class.new(select(&:single_file?))
         | 
| 14 25 | 
             
                end
         | 
| 15 26 |  | 
| 16 | 
            -
                 | 
| 17 | 
            -
             | 
| 27 | 
            +
                private
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def call_check_method(check, method, *args)
         | 
| 30 | 
            +
                  return unless check.respond_to?(method) && !check.ignored?
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  Timeout.timeout(CHECK_METHOD_TIMEOUT) do
         | 
| 33 | 
            +
                    check.send(method, *args)
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                rescue Liquid::Error
         | 
| 36 | 
            +
                  # Pass-through Liquid errors
         | 
| 37 | 
            +
                  raise
         | 
| 38 | 
            +
                rescue => e
         | 
| 39 | 
            +
                  node = args.first
         | 
| 40 | 
            +
                  template = node.respond_to?(:template) ? node.template.relative_path : "?"
         | 
| 41 | 
            +
                  markup = node.respond_to?(:markup) ? node.markup : ""
         | 
| 42 | 
            +
                  node_class = node.respond_to?(:value) ? node.value.class : "?"
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  ThemeCheck.bug(<<~EOS)
         | 
| 45 | 
            +
                    Exception while running `#{check.code_name}##{method}`:
         | 
| 46 | 
            +
                    ```
         | 
| 47 | 
            +
                    #{e.class}: #{e.message}
         | 
| 48 | 
            +
                      #{e.backtrace.join("\n  ")}
         | 
| 49 | 
            +
                    ```
         | 
| 18 50 |  | 
| 19 | 
            -
             | 
| 51 | 
            +
                    Template: `#{template}`
         | 
| 52 | 
            +
                    Node: `#{node_class}`
         | 
| 53 | 
            +
                    Markup:
         | 
| 54 | 
            +
                    ```
         | 
| 55 | 
            +
                    #{markup}
         | 
| 56 | 
            +
                    ```
         | 
| 57 | 
            +
                    Check options: `#{check.options.pretty_inspect}`
         | 
| 58 | 
            +
                  EOS
         | 
| 20 59 | 
             
                end
         | 
| 21 60 | 
             
              end
         | 
| 22 61 | 
             
            end
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            module ThemeCheck
         | 
| 3 | 
            +
              class AssetUrlFilters < LiquidCheck
         | 
| 4 | 
            +
                severity :suggestion
         | 
| 5 | 
            +
                categories :liquid, :performance
         | 
| 6 | 
            +
                doc docs_url(__FILE__)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                HTML_FILTERS = [
         | 
| 9 | 
            +
                  'stylesheet_tag',
         | 
| 10 | 
            +
                  'script_tag',
         | 
| 11 | 
            +
                  'img_tag',
         | 
| 12 | 
            +
                ]
         | 
| 13 | 
            +
                ASSET_URL_FILTERS = [
         | 
| 14 | 
            +
                  'asset_url',
         | 
| 15 | 
            +
                  'asset_img_url',
         | 
| 16 | 
            +
                  'file_img_url',
         | 
| 17 | 
            +
                  'file_url',
         | 
| 18 | 
            +
                  'global_asset_url',
         | 
| 19 | 
            +
                  'img_url',
         | 
| 20 | 
            +
                  'payment_type_img_url',
         | 
| 21 | 
            +
                  'shopify_asset_url',
         | 
| 22 | 
            +
                ]
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def on_variable(node)
         | 
| 25 | 
            +
                  record_variable_offense(node)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                private
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def record_variable_offense(variable_node)
         | 
| 31 | 
            +
                  # We flag HTML tags with URLs not hosted by Shopify
         | 
| 32 | 
            +
                  return if !html_resource_drop?(variable_node) || variable_hosted_by_shopify?(variable_node)
         | 
| 33 | 
            +
                  add_offense("Use one of the asset_url filters to serve assets", node: variable_node)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def html_resource_drop?(variable_node)
         | 
| 37 | 
            +
                  variable_node.value.filters
         | 
| 38 | 
            +
                    .any? { |(filter_name, *_filter_args)| HTML_FILTERS.include?(filter_name) }
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def variable_hosted_by_shopify?(variable_node)
         | 
| 42 | 
            +
                  variable_node.value.filters
         | 
| 43 | 
            +
                    .any? { |(filter_name, *_filter_args)| ASSET_URL_FILTERS.include?(filter_name) }
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
            end
         | 
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            module ThemeCheck
         | 
| 3 | 
            +
              class ContentForHeaderModification < LiquidCheck
         | 
| 4 | 
            +
                severity :error
         | 
| 5 | 
            +
                category :liquid
         | 
| 6 | 
            +
                doc docs_url(__FILE__)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize
         | 
| 9 | 
            +
                  @in_assign = false
         | 
| 10 | 
            +
                  @in_capture = false
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def on_variable(node)
         | 
| 14 | 
            +
                  return unless node.value.name.is_a?(Liquid::VariableLookup)
         | 
| 15 | 
            +
                  return unless node.value.name.name == "content_for_header"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  if @in_assign || @in_capture || node.value.filters.any?
         | 
| 18 | 
            +
                    add_offense(
         | 
| 19 | 
            +
                      "Do not rely on the content of `content_for_header`",
         | 
| 20 | 
            +
                      node: node,
         | 
| 21 | 
            +
                    )
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def on_assign(_node)
         | 
| 26 | 
            +
                  @in_assign = true
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def after_assign(_node)
         | 
| 30 | 
            +
                  @in_assign = false
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def on_capture(_node)
         | 
| 34 | 
            +
                  @in_capture = true
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def after_capture(_node)
         | 
| 38 | 
            +
                  @in_capture = false
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -1,41 +1,21 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 | 
             
            module ThemeCheck
         | 
| 3 3 | 
             
              # Reports errors when trying to use parser-blocking script tags
         | 
| 4 | 
            -
              class ImgWidthAndHeight <  | 
| 5 | 
            -
                include RegexHelpers
         | 
| 4 | 
            +
              class ImgWidthAndHeight < HtmlCheck
         | 
| 6 5 | 
             
                severity :error
         | 
| 7 | 
            -
                categories : | 
| 6 | 
            +
                categories :html, :performance
         | 
| 8 7 | 
             
                doc docs_url(__FILE__)
         | 
| 9 8 |  | 
| 10 | 
            -
                # Not implemented with lookbehinds and lookaheads because performance was shit!
         | 
| 11 | 
            -
                IMG_TAG = %r{<img#{HTML_ATTRIBUTES}/?>}oxim
         | 
| 12 | 
            -
                SRC_ATTRIBUTE = /\s(src)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
         | 
| 13 | 
            -
                WIDTH_ATTRIBUTE = /\s(width)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
         | 
| 14 | 
            -
                HEIGHT_ATTRIBUTE = /\s(height)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                FIELDS = [WIDTH_ATTRIBUTE, HEIGHT_ATTRIBUTE]
         | 
| 17 9 | 
             
                ENDS_IN_CSS_UNIT = /(cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)$/i
         | 
| 18 10 |  | 
| 19 | 
            -
                def  | 
| 20 | 
            -
                   | 
| 21 | 
            -
                   | 
| 22 | 
            -
                  record_offenses
         | 
| 23 | 
            -
                end
         | 
| 11 | 
            +
                def on_img(node)
         | 
| 12 | 
            +
                  width = node.attributes["width"]&.value
         | 
| 13 | 
            +
                  height = node.attributes["height"]&.value
         | 
| 24 14 |  | 
| 25 | 
            -
             | 
| 15 | 
            +
                  record_units_in_field_offenses("width", width, node: node)
         | 
| 16 | 
            +
                  record_units_in_field_offenses("height", height, node: node)
         | 
| 26 17 |  | 
| 27 | 
            -
             | 
| 28 | 
            -
                  matches(@source, IMG_TAG).each do |img_match|
         | 
| 29 | 
            -
                    next unless img_match[0] =~ SRC_ATTRIBUTE
         | 
| 30 | 
            -
                    record_missing_field_offenses(img_match)
         | 
| 31 | 
            -
                    record_units_in_field_offenses(img_match)
         | 
| 32 | 
            -
                  end
         | 
| 33 | 
            -
                end
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                def record_missing_field_offenses(img_match)
         | 
| 36 | 
            -
                  width = WIDTH_ATTRIBUTE.match(img_match[0])
         | 
| 37 | 
            -
                  height = HEIGHT_ATTRIBUTE.match(img_match[0])
         | 
| 38 | 
            -
                  return if width && height
         | 
| 18 | 
            +
                  return if node.attributes["src"].nil? || (width && height)
         | 
| 39 19 | 
             
                  missing_width = width.nil?
         | 
| 40 20 | 
             
                  missing_height = height.nil?
         | 
| 41 21 | 
             
                  error_message = if missing_width && missing_height
         | 
| @@ -46,29 +26,18 @@ module ThemeCheck | |
| 46 26 | 
             
                    "Missing height attribute"
         | 
| 47 27 | 
             
                  end
         | 
| 48 28 |  | 
| 49 | 
            -
                  add_offense(
         | 
| 50 | 
            -
                    error_message,
         | 
| 51 | 
            -
                    node: @node,
         | 
| 52 | 
            -
                    markup: img_match[0],
         | 
| 53 | 
            -
                    line_number: @source[0...img_match.begin(0)].count("\n") + 1
         | 
| 54 | 
            -
                  )
         | 
| 29 | 
            +
                  add_offense(error_message, node: node)
         | 
| 55 30 | 
             
                end
         | 
| 56 31 |  | 
| 57 | 
            -
                 | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
                     | 
| 64 | 
            -
                     | 
| 65 | 
            -
             | 
| 66 | 
            -
                      "The #{field_match[1]} attribute does not take units. Replace with \"#{value_without_units}\".",
         | 
| 67 | 
            -
                      node: @node,
         | 
| 68 | 
            -
                      markup: value,
         | 
| 69 | 
            -
                      line_number: @source[0...start].count("\n") + 1
         | 
| 70 | 
            -
                    )
         | 
| 71 | 
            -
                  end
         | 
| 32 | 
            +
                private
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def record_units_in_field_offenses(attribute, value, node:)
         | 
| 35 | 
            +
                  return unless value =~ ENDS_IN_CSS_UNIT
         | 
| 36 | 
            +
                  value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
         | 
| 37 | 
            +
                  add_offense(
         | 
| 38 | 
            +
                    "The #{attribute} attribute does not take units. Replace with \"#{value_without_units}\".",
         | 
| 39 | 
            +
                    node: node,
         | 
| 40 | 
            +
                  )
         | 
| 72 41 | 
             
                end
         | 
| 73 42 | 
             
              end
         | 
| 74 43 | 
             
            end
         |