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
| @@ -0,0 +1,56 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "forwardable"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module ThemeCheck
         | 
| 5 | 
            +
              class HtmlNode
         | 
| 6 | 
            +
                extend Forwardable
         | 
| 7 | 
            +
                attr_reader :template
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def_delegators :@value, :content, :attributes
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(value, template)
         | 
| 12 | 
            +
                  @value = value
         | 
| 13 | 
            +
                  @template = template
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def literal?
         | 
| 17 | 
            +
                  @value.name == "text"
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def element?
         | 
| 21 | 
            +
                  @value.element?
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def children
         | 
| 25 | 
            +
                  @value.children.map { |child| HtmlNode.new(child, template) }
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def parent
         | 
| 29 | 
            +
                  HtmlNode.new(@value.parent, template)
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def name
         | 
| 33 | 
            +
                  if @value.name == "#document-fragment"
         | 
| 34 | 
            +
                    "document"
         | 
| 35 | 
            +
                  else
         | 
| 36 | 
            +
                    @value.name
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def value
         | 
| 41 | 
            +
                  if literal?
         | 
| 42 | 
            +
                    @value.content
         | 
| 43 | 
            +
                  else
         | 
| 44 | 
            +
                    @value
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def markup
         | 
| 49 | 
            +
                  @value.to_html
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def line_number
         | 
| 53 | 
            +
                  @value.line
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
              end
         | 
| 56 | 
            +
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "nokogumbo"
         | 
| 3 | 
            +
            require "forwardable"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module ThemeCheck
         | 
| 6 | 
            +
              class HtmlVisitor
         | 
| 7 | 
            +
                attr_reader :checks
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize(checks)
         | 
| 10 | 
            +
                  @checks = checks
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def visit_template(template)
         | 
| 14 | 
            +
                  doc = parse(template)
         | 
| 15 | 
            +
                  visit(HtmlNode.new(doc, template))
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                private
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def parse(template)
         | 
| 21 | 
            +
                  Nokogiri::HTML5.fragment(template.source)
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def visit(node)
         | 
| 25 | 
            +
                  call_checks(:on_element, node) if node.element?
         | 
| 26 | 
            +
                  call_checks(:"on_#{node.name}", node)
         | 
| 27 | 
            +
                  node.children.each { |child| visit(child) }
         | 
| 28 | 
            +
                  unless node.literal?
         | 
| 29 | 
            +
                    call_checks(:"after_#{node.name}", node)
         | 
| 30 | 
            +
                    call_checks(:after_element, node) if node.element?
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def call_checks(method, *args)
         | 
| 35 | 
            +
                  checks.call(method, *args)
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -20,6 +20,10 @@ module ThemeCheck | |
| 20 20 | 
             
                  @relative_pathname ||= Pathname.new(@relative_path)
         | 
| 21 21 | 
             
                end
         | 
| 22 22 |  | 
| 23 | 
            +
                def source
         | 
| 24 | 
            +
                  @source ||= @storage.read(@relative_path)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 23 27 | 
             
                def content
         | 
| 24 28 | 
             
                  load!
         | 
| 25 29 | 
             
                  @content
         | 
| @@ -34,12 +38,20 @@ module ThemeCheck | |
| 34 38 | 
             
                  relative_path.sub_ext('').to_s
         | 
| 35 39 | 
             
                end
         | 
| 36 40 |  | 
| 41 | 
            +
                def json?
         | 
| 42 | 
            +
                  true
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def liquid?
         | 
| 46 | 
            +
                  false
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 37 49 | 
             
                private
         | 
| 38 50 |  | 
| 39 51 | 
             
                def load!
         | 
| 40 52 | 
             
                  return if @loaded
         | 
| 41 53 |  | 
| 42 | 
            -
                  @content = JSON.parse( | 
| 54 | 
            +
                  @content = JSON.parse(source)
         | 
| 43 55 | 
             
                rescue JSON::ParserError => e
         | 
| 44 56 | 
             
                  @parser_error = e
         | 
| 45 57 | 
             
                ensure
         | 
| @@ -4,11 +4,12 @@ require_relative "language_server/constants" | |
| 4 4 | 
             
            require_relative "language_server/handler"
         | 
| 5 5 | 
             
            require_relative "language_server/server"
         | 
| 6 6 | 
             
            require_relative "language_server/tokens"
         | 
| 7 | 
            -
            require_relative "language_server/ | 
| 7 | 
            +
            require_relative "language_server/variable_lookup_finder"
         | 
| 8 8 | 
             
            require_relative "language_server/completion_helper"
         | 
| 9 9 | 
             
            require_relative "language_server/completion_provider"
         | 
| 10 10 | 
             
            require_relative "language_server/completion_engine"
         | 
| 11 11 | 
             
            require_relative "language_server/document_link_engine"
         | 
| 12 | 
            +
            require_relative "language_server/diagnostics_tracker"
         | 
| 12 13 |  | 
| 13 14 | 
             
            Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
         | 
| 14 15 | 
             
              require file
         | 
| @@ -12,7 +12,7 @@ module ThemeCheck | |
| 12 12 |  | 
| 13 13 | 
             
                  def completions(relative_path, line, col)
         | 
| 14 14 | 
             
                    buffer = @storage.read(relative_path)
         | 
| 15 | 
            -
                    cursor =  | 
| 15 | 
            +
                    cursor = from_row_column_to_index(buffer, line, col)
         | 
| 16 16 | 
             
                    token = find_token(buffer, cursor)
         | 
| 17 17 | 
             
                    return [] if token.nil?
         | 
| 18 18 |  | 
| @@ -4,18 +4,20 @@ module ThemeCheck | |
| 4 4 | 
             
              module LanguageServer
         | 
| 5 5 | 
             
                class ObjectCompletionProvider < CompletionProvider
         | 
| 6 6 | 
             
                  def completions(content, cursor)
         | 
| 7 | 
            -
                    return [] unless  | 
| 8 | 
            -
                     | 
| 7 | 
            +
                    return [] unless (variable_lookup = variable_lookup_at_cursor(content, cursor))
         | 
| 8 | 
            +
                    return [] unless variable_lookup.lookups.empty?
         | 
| 9 | 
            +
                    return [] if content[cursor - 1] == "."
         | 
| 9 10 | 
             
                    ShopifyLiquid::Object.labels
         | 
| 10 | 
            -
                      .select { |w| w.start_with?(partial) }
         | 
| 11 | 
            +
                      .select { |w| w.start_with?(partial(variable_lookup)) }
         | 
| 11 12 | 
             
                      .map { |object| object_to_completion(object) }
         | 
| 12 13 | 
             
                  end
         | 
| 13 14 |  | 
| 14 | 
            -
                  def  | 
| 15 | 
            -
                     | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 15 | 
            +
                  def variable_lookup_at_cursor(content, cursor)
         | 
| 16 | 
            +
                    VariableLookupFinder.lookup(content, cursor)
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def partial(variable_lookup)
         | 
| 20 | 
            +
                    variable_lookup.name || ''
         | 
| 19 21 | 
             
                  end
         | 
| 20 22 |  | 
| 21 23 | 
             
                  private
         | 
| @@ -4,7 +4,11 @@ module ThemeCheck | |
| 4 4 | 
             
              module LanguageServer
         | 
| 5 5 | 
             
                PARTIAL_RENDER = %r{
         | 
| 6 6 | 
             
                  \{\%-?\s*render\s+'(?<partial>[^']*)'|
         | 
| 7 | 
            -
                  \{\%-?\s*render\s+"(?<partial>[^"]*)"
         | 
| 7 | 
            +
                  \{\%-?\s*render\s+"(?<partial>[^"]*)"|
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  # in liquid tags the whole line is white space until render
         | 
| 10 | 
            +
                  ^\s*render\s+'(?<partial>[^']*)'|
         | 
| 11 | 
            +
                  ^\s*render\s+"(?<partial>[^"]*)"
         | 
| 8 12 | 
             
                }mix
         | 
| 9 13 | 
             
              end
         | 
| 10 14 | 
             
            end
         | 
| @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ThemeCheck
         | 
| 4 | 
            +
              module LanguageServer
         | 
| 5 | 
            +
                class DiagnosticsTracker
         | 
| 6 | 
            +
                  def initialize
         | 
| 7 | 
            +
                    @previously_reported_files = Set.new
         | 
| 8 | 
            +
                    @single_files_offenses = {}
         | 
| 9 | 
            +
                    @first_run = true
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def first_run?
         | 
| 13 | 
            +
                    @first_run
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def build_diagnostics(offenses, analyzed_files: nil)
         | 
| 17 | 
            +
                    reported_files = Set.new
         | 
| 18 | 
            +
                    new_single_file_offenses = {}
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    offenses.group_by(&:template).each do |template, template_offenses|
         | 
| 21 | 
            +
                      next unless template
         | 
| 22 | 
            +
                      reported_offenses = template_offenses
         | 
| 23 | 
            +
                      previous_offenses = @single_files_offenses[template.path]
         | 
| 24 | 
            +
                      if analyzed_files.nil? || analyzed_files.include?(template.path)
         | 
| 25 | 
            +
                        # We re-analyzed the file, so we know the template_offenses are update to date.
         | 
| 26 | 
            +
                        reported_single_file_offenses = reported_offenses.select(&:single_file?)
         | 
| 27 | 
            +
                        if reported_single_file_offenses.any?
         | 
| 28 | 
            +
                          new_single_file_offenses[template.path] = reported_single_file_offenses
         | 
| 29 | 
            +
                        end
         | 
| 30 | 
            +
                      elsif previous_offenses
         | 
| 31 | 
            +
                        # Merge in the previous ones, if some
         | 
| 32 | 
            +
                        reported_offenses |= previous_offenses
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                      yield template.path, reported_offenses
         | 
| 35 | 
            +
                      reported_files << template.path
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    @single_files_offenses.each do |path, _|
         | 
| 39 | 
            +
                      # Already reported above, skip
         | 
| 40 | 
            +
                      next if reported_files.include?(path)
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                      if analyzed_files.nil? || analyzed_files.include?(path)
         | 
| 43 | 
            +
                        # We re-analyzed this file, if it was not reported, all offenses in it got fixed
         | 
| 44 | 
            +
                        yield path, []
         | 
| 45 | 
            +
                        new_single_file_offenses[path] = nil
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                      # NOTE: No need to re-report previous offenses as LSP should keep them around until
         | 
| 48 | 
            +
                      # we clear them.
         | 
| 49 | 
            +
                      reported_files << path
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    # Publish diagnostics with empty array if all issues on a previously reported template
         | 
| 53 | 
            +
                    # have been fixed.
         | 
| 54 | 
            +
                    (@previously_reported_files - reported_files).each do |path|
         | 
| 55 | 
            +
                      yield path, []
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    @previously_reported_files = reported_files
         | 
| 59 | 
            +
                    @single_files_offenses.merge!(new_single_file_offenses)
         | 
| 60 | 
            +
                    @first_run = false
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
            end
         | 
| @@ -14,12 +14,12 @@ module ThemeCheck | |
| 14 14 | 
             
                    buffer = @storage.read(relative_path)
         | 
| 15 15 | 
             
                    return [] unless buffer
         | 
| 16 16 | 
             
                    matches(buffer, PARTIAL_RENDER).map do |match|
         | 
| 17 | 
            -
                      start_line, start_character =  | 
| 17 | 
            +
                      start_line, start_character = from_index_to_row_column(
         | 
| 18 18 | 
             
                        buffer,
         | 
| 19 19 | 
             
                        match.begin(:partial),
         | 
| 20 20 | 
             
                      )
         | 
| 21 21 |  | 
| 22 | 
            -
                      end_line, end_character =  | 
| 22 | 
            +
                      end_line, end_character = from_index_to_row_column(
         | 
| 23 23 | 
             
                        buffer,
         | 
| 24 24 | 
             
                        match.end(:partial)
         | 
| 25 25 | 
             
                      )
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "benchmark"
         | 
| 2 3 |  | 
| 3 4 | 
             
            module ThemeCheck
         | 
| 4 5 | 
             
              module LanguageServer
         | 
| @@ -19,21 +20,21 @@ module ThemeCheck | |
| 19 20 |  | 
| 20 21 | 
             
                  def initialize(server)
         | 
| 21 22 | 
             
                    @server = server
         | 
| 22 | 
            -
                    @ | 
| 23 | 
            +
                    @diagnostics_tracker = DiagnosticsTracker.new
         | 
| 23 24 | 
             
                  end
         | 
| 24 25 |  | 
| 25 26 | 
             
                  def on_initialize(id, params)
         | 
| 26 | 
            -
                    @root_path = params["rootPath"]
         | 
| 27 | 
            +
                    @root_path = path_from_uri(params["rootUri"]) || params["rootPath"]
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    # Tell the client we don't support anything if there's no rootPath
         | 
| 30 | 
            +
                    return send_response(id, { capabilities: {} }) if @root_path.nil?
         | 
| 27 31 | 
             
                    @storage = in_memory_storage(@root_path)
         | 
| 28 32 | 
             
                    @completion_engine = CompletionEngine.new(@storage)
         | 
| 29 33 | 
             
                    @document_link_engine = DocumentLinkEngine.new(@storage)
         | 
| 30 34 | 
             
                    # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
         | 
| 31 | 
            -
                    send_response(
         | 
| 32 | 
            -
                       | 
| 33 | 
            -
             | 
| 34 | 
            -
                        capabilities: CAPABILITIES,
         | 
| 35 | 
            -
                      }
         | 
| 36 | 
            -
                    )
         | 
| 35 | 
            +
                    send_response(id, {
         | 
| 36 | 
            +
                      capabilities: CAPABILITIES,
         | 
| 37 | 
            +
                    })
         | 
| 37 38 | 
             
                  end
         | 
| 38 39 |  | 
| 39 40 | 
             
                  def on_exit(_id, _params)
         | 
| @@ -52,6 +53,7 @@ module ThemeCheck | |
| 52 53 | 
             
                  end
         | 
| 53 54 |  | 
| 54 55 | 
             
                  def on_text_document_did_open(_id, params)
         | 
| 56 | 
            +
                    return unless @diagnostics_tracker.first_run?
         | 
| 55 57 | 
             
                    relative_path = relative_path_from_text_document_uri(params)
         | 
| 56 58 | 
             
                    @storage.write(relative_path, text_document_text(params))
         | 
| 57 59 | 
             
                    analyze_and_send_offenses(text_document_uri(params))
         | 
| @@ -63,20 +65,14 @@ module ThemeCheck | |
| 63 65 |  | 
| 64 66 | 
             
                  def on_text_document_document_link(id, params)
         | 
| 65 67 | 
             
                    relative_path = relative_path_from_text_document_uri(params)
         | 
| 66 | 
            -
                    send_response(
         | 
| 67 | 
            -
                      id: id,
         | 
| 68 | 
            -
                      result: document_links(relative_path)
         | 
| 69 | 
            -
                    )
         | 
| 68 | 
            +
                    send_response(id, document_links(relative_path))
         | 
| 70 69 | 
             
                  end
         | 
| 71 70 |  | 
| 72 71 | 
             
                  def on_text_document_completion(id, params)
         | 
| 73 72 | 
             
                    relative_path = relative_path_from_text_document_uri(params)
         | 
| 74 73 | 
             
                    line = params.dig('position', 'line')
         | 
| 75 74 | 
             
                    col = params.dig('position', 'character')
         | 
| 76 | 
            -
                    send_response(
         | 
| 77 | 
            -
                      id: id,
         | 
| 78 | 
            -
                      result: completions(relative_path, line, col)
         | 
| 79 | 
            -
                    )
         | 
| 75 | 
            +
                    send_response(id, completions(relative_path, line, col))
         | 
| 80 76 | 
             
                  end
         | 
| 81 77 |  | 
| 82 78 | 
             
                  private
         | 
| @@ -99,7 +95,11 @@ module ThemeCheck | |
| 99 95 | 
             
                  end
         | 
| 100 96 |  | 
| 101 97 | 
             
                  def text_document_uri(params)
         | 
| 102 | 
            -
                    params.dig('textDocument', 'uri') | 
| 98 | 
            +
                    path_from_uri(params.dig('textDocument', 'uri'))
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  def path_from_uri(uri)
         | 
| 102 | 
            +
                    uri&.sub('file://', '')
         | 
| 103 103 | 
             
                  end
         | 
| 104 104 |  | 
| 105 105 | 
             
                  def relative_path_from_text_document_uri(params)
         | 
| @@ -126,17 +126,32 @@ module ThemeCheck | |
| 126 126 | 
             
                      ignored_patterns: config.ignored_patterns
         | 
| 127 127 | 
             
                    )
         | 
| 128 128 | 
             
                    theme = ThemeCheck::Theme.new(storage)
         | 
| 129 | 
            -
             | 
| 130 | 
            -
                    offenses = analyze(theme, config)
         | 
| 131 | 
            -
                    log("Found #{theme.all.size} templates, and #{offenses.size} offenses")
         | 
| 132 | 
            -
                    send_diagnostics(offenses)
         | 
| 133 | 
            -
                  end
         | 
| 134 | 
            -
             | 
| 135 | 
            -
                  def analyze(theme, config)
         | 
| 136 129 | 
             
                    analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
         | 
| 137 | 
            -
             | 
| 138 | 
            -
                     | 
| 139 | 
            -
             | 
| 130 | 
            +
             | 
| 131 | 
            +
                    if @diagnostics_tracker.first_run?
         | 
| 132 | 
            +
                      # Analyze the full theme on first run
         | 
| 133 | 
            +
                      log("Checking #{config.root}")
         | 
| 134 | 
            +
                      offenses = nil
         | 
| 135 | 
            +
                      time = Benchmark.measure do
         | 
| 136 | 
            +
                        offenses = analyzer.analyze_theme
         | 
| 137 | 
            +
                      end
         | 
| 138 | 
            +
                      log("Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s")
         | 
| 139 | 
            +
                      send_diagnostics(offenses)
         | 
| 140 | 
            +
                    else
         | 
| 141 | 
            +
                      # Analyze selected files
         | 
| 142 | 
            +
                      relative_path = Pathname.new(@storage.relative_path(absolute_path))
         | 
| 143 | 
            +
                      file = theme[relative_path]
         | 
| 144 | 
            +
                      # Skip if not a theme file
         | 
| 145 | 
            +
                      if file
         | 
| 146 | 
            +
                        log("Checking #{relative_path}")
         | 
| 147 | 
            +
                        offenses = nil
         | 
| 148 | 
            +
                        time = Benchmark.measure do
         | 
| 149 | 
            +
                          offenses = analyzer.analyze_files([file])
         | 
| 150 | 
            +
                        end
         | 
| 151 | 
            +
                        log("Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s")
         | 
| 152 | 
            +
                        send_diagnostics(offenses, [absolute_path])
         | 
| 153 | 
            +
                      end
         | 
| 154 | 
            +
                    end
         | 
| 140 155 | 
             
                  end
         | 
| 141 156 |  | 
| 142 157 | 
             
                  def completions(relative_path, line, col)
         | 
| @@ -147,33 +162,18 @@ module ThemeCheck | |
| 147 162 | 
             
                    @document_link_engine.document_links(relative_path)
         | 
| 148 163 | 
             
                  end
         | 
| 149 164 |  | 
| 150 | 
            -
                  def send_diagnostics(offenses)
         | 
| 151 | 
            -
                     | 
| 152 | 
            -
             | 
| 153 | 
            -
                    offenses.group_by(&:template).each do |template, template_offenses|
         | 
| 154 | 
            -
                      next unless template
         | 
| 155 | 
            -
                      send_diagnostic(template.path, template_offenses)
         | 
| 156 | 
            -
                      reported_files << template.path
         | 
| 157 | 
            -
                    end
         | 
| 158 | 
            -
             | 
| 159 | 
            -
                    # Publish diagnostics with empty array if all issues on a previously reported template
         | 
| 160 | 
            -
                    # have been solved.
         | 
| 161 | 
            -
                    (@previously_reported_files - reported_files).each do |path|
         | 
| 162 | 
            -
                      send_diagnostic(path, [])
         | 
| 165 | 
            +
                  def send_diagnostics(offenses, analyzed_files = nil)
         | 
| 166 | 
            +
                    @diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
         | 
| 167 | 
            +
                      send_diagnostic(path, diagnostic_offenses)
         | 
| 163 168 | 
             
                    end
         | 
| 164 | 
            -
             | 
| 165 | 
            -
                    @previously_reported_files = reported_files
         | 
| 166 169 | 
             
                  end
         | 
| 167 170 |  | 
| 168 171 | 
             
                  def send_diagnostic(path, offenses)
         | 
| 169 172 | 
             
                    # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
         | 
| 170 | 
            -
                     | 
| 171 | 
            -
                       | 
| 172 | 
            -
                       | 
| 173 | 
            -
             | 
| 174 | 
            -
                        diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
         | 
| 175 | 
            -
                      },
         | 
| 176 | 
            -
                    )
         | 
| 173 | 
            +
                    send_notification('textDocument/publishDiagnostics', {
         | 
| 174 | 
            +
                      uri: "file://#{path}",
         | 
| 175 | 
            +
                      diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
         | 
| 176 | 
            +
                    })
         | 
| 177 177 | 
             
                  end
         | 
| 178 178 |  | 
| 179 179 | 
             
                  def offense_to_diagnostic(offense)
         | 
| @@ -220,11 +220,24 @@ module ThemeCheck | |
| 220 220 | 
             
                    }
         | 
| 221 221 | 
             
                  end
         | 
| 222 222 |  | 
| 223 | 
            -
                  def  | 
| 223 | 
            +
                  def send_message(message)
         | 
| 224 224 | 
             
                    message[:jsonrpc] = '2.0'
         | 
| 225 225 | 
             
                    @server.send_response(message)
         | 
| 226 226 | 
             
                  end
         | 
| 227 227 |  | 
| 228 | 
            +
                  def send_response(id, result = nil, error = nil)
         | 
| 229 | 
            +
                    message = { id: id }
         | 
| 230 | 
            +
                    message[:result] = result if result
         | 
| 231 | 
            +
                    message[:error] = error if error
         | 
| 232 | 
            +
                    send_message(message)
         | 
| 233 | 
            +
                  end
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                  def send_notification(method, params)
         | 
| 236 | 
            +
                    message = { method: method }
         | 
| 237 | 
            +
                    message[:params] = params
         | 
| 238 | 
            +
                    send_message(message)
         | 
| 239 | 
            +
                  end
         | 
| 240 | 
            +
             | 
| 228 241 | 
             
                  def log(message)
         | 
| 229 242 | 
             
                    @server.log(message)
         | 
| 230 243 | 
             
                  end
         |