lib/rubocop/cop/sorbet/sigils/enforce_sigil_order.rb



# frozen_string_literal: true

require 'rubocop'

module RuboCop
  module Cop
    module Sorbet
      # This cop checks that the Sorbet sigil comes as the first magic comment in the file.
      #
      # The expected order for magic comments is: typed, (en)?coding, warn_indent then frozen_string_literal.
      #
      # For example, the following bad ordering:
      #
      # ```ruby
      # # frozen_string_literal: true
      # # typed: true
      # class Foo; end
      # ```
      #
      # Will be corrected as:
      #
      # ```ruby
      # # typed: true
      # # frozen_string_literal: true
      # class Foo; end
      # ```
      #
      # Only `typed`, `(en)?coding`, `warn_indent` and `frozen_string_literal` magic comments are considered,
      # other comments or magic comments are left in the same place.
      class EnforceSigilOrder < ValidSigil
        def investigate(processed_source)
          return if processed_source.tokens.empty?

          tokens = extract_magic_comments(processed_source)
          return if tokens.empty?

          check_magic_comments_order(tokens)
        end

        def autocorrect(_node)
          lambda do |corrector|
            tokens = extract_magic_comments(processed_source)

            # Get the magic comments tokens in their expected order
            expected = PREFERRED_ORDER.keys.map do |re|
              tokens.select { |token| re.match?(token.text) }
            end.flatten

            tokens.each_with_index do |token, index|
              corrector.replace(token.pos, expected[index].text)
            end
          end
        end

        protected

        CODING_REGEX = /#\s+(en)?coding:(?:\s+([\w]+))?/
        INDENT_REGEX = /#\s+warn_indent:(?:\s+([\w]+))?/
        FROZEN_REGEX = /#\s+frozen_string_literal:(?:\s+([\w]+))?/

        PREFERRED_ORDER = {
          SIGIL_REGEX => 'typed',
          CODING_REGEX => 'encoding',
          INDENT_REGEX => 'warn_indent',
          FROZEN_REGEX => 'frozen_string_literal',
        }.freeze

        MAGIC_REGEX = Regexp.union(*PREFERRED_ORDER.keys)

        # extraction

        # Get all the tokens in `processed_source` that match `MAGIC_REGEX`
        def extract_magic_comments(processed_source)
          processed_source.tokens
            .take_while { |token| token.type == :tCOMMENT }
            .select { |token| MAGIC_REGEX.match?(token.text) }
        end

        # checks

        def check_magic_comments_order(tokens)
          # Get the current magic comments order
          order = tokens.map do |token|
            PREFERRED_ORDER.keys.find { |re| re.match?(token.text) }
          end.compact.uniq

          # Get the expected magic comments order based on the one used in the actual source
          expected = PREFERRED_ORDER.keys.select do |re|
            tokens.any? { |token| re.match?(token.text) }
          end.uniq

          if order != expected
            tokens.each do |token|
              add_offense(
                token,
                location: token.pos,
                message: "Magic comments should be in the following order: #{PREFERRED_ORDER.values.join(', ')}."
              )
            end
          end
        end
      end
    end
  end
end