From 8b21a6808f5a9b18bd8b436b07b5b8b84b5a7397 Mon Sep 17 00:00:00 2001 From: Cameron Dutro Date: Fri, 13 May 2022 11:26:14 -0700 Subject: [PATCH] Add a linter for calls to `super` in templates (#1153) --- .changeset/popular-vans-sleep.md | 5 ++ Gemfile.lock | 2 - .../linters/super_in_component_templates.rb | 65 +++++++++++++++++ .../super_in_component_templates_test.rb | 73 +++++++++++++++++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 .changeset/popular-vans-sleep.md create mode 100644 lib/primer/view_components/linters/super_in_component_templates.rb create mode 100644 test/linters/super_in_component_templates_test.rb diff --git a/.changeset/popular-vans-sleep.md b/.changeset/popular-vans-sleep.md new file mode 100644 index 0000000000..7b74a77e9c --- /dev/null +++ b/.changeset/popular-vans-sleep.md @@ -0,0 +1,5 @@ +--- +'@primer/view-components': patch +--- + +Add a linter for calls to super in templates diff --git a/Gemfile.lock b/Gemfile.lock index 5832d59272..0abe795c1b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -116,8 +116,6 @@ GEM nokogiri (1.12.5) mini_portile2 (~> 2.6.1) racc (~> 1.4) - nokogiri (1.12.5-x86_64-darwin) - racc (~> 1.4) octicons (17.1.0) parallel (1.21.0) parser (3.0.2.0) diff --git a/lib/primer/view_components/linters/super_in_component_templates.rb b/lib/primer/view_components/linters/super_in_component_templates.rb new file mode 100644 index 0000000000..3340a84f9a --- /dev/null +++ b/lib/primer/view_components/linters/super_in_component_templates.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require_relative "helpers/rubocop_helpers" + +module ERBLint + module Linters + # Replaces calls to `super` with calls to `render_parent`. + class SuperInComponentTemplates < Linter + include ERBLint::LinterRegistry + include Helpers::RubocopHelpers + + def run(processed_source) + processed_source.ast.descendants(:erb).each do |erb_node| + indicator_node, _, code_node = *erb_node + code = code_node.children.first + ast = erb_ast(code) + next unless ast + + super_call_nodes = find_super_call_nodes(ast) + next if super_call_nodes.empty? + + indicator, = *indicator_node + indicator ||= "" + + # +2 to account for the leading "<%" characters + code_start_pos = erb_node.location.begin_pos + indicator.size + 2 + + super_call_nodes.each do |super_call_node| + orig_loc = code_node.location + super_call_loc = super_call_node.location.expression + + new_loc = orig_loc.with( + begin_pos: super_call_loc.begin_pos + code_start_pos, + end_pos: super_call_loc.end_pos + code_start_pos + ) + + add_offense( + new_loc, + "Avoid calling `super` in component templates. Call `render_parent` instead", + "render_parent" + ) + end + end + end + + def autocorrect(_, offense) + return unless offense.context + + lambda do |corrector| + corrector.replace(offense.source_range, offense.context) + end + end + + private + + def find_super_call_nodes(ast) + return [ast] if ast.type == :zsuper + + ast.each_child_node.flat_map do |child_ast| + find_super_call_nodes(child_ast) + end + end + end + end +end diff --git a/test/linters/super_in_component_templates_test.rb b/test/linters/super_in_component_templates_test.rb new file mode 100644 index 0000000000..e8e80f6246 --- /dev/null +++ b/test/linters/super_in_component_templates_test.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "linter_test_case" + +class SuperInComponentTemplatesTest < LinterTestCase + def test_identifies_super_calls + @file = "
<% super %>
" + @linter.run(processed_source) + + refute_empty @linter.offenses + end + + def test_identifies_echoed_super_calls + @file = "
<%= super %>
" + @linter.run(processed_source) + + refute_empty @linter.offenses + end + + def test_replaces_super_with_render_parent + @file = <<~ERB +
+ <% super %> +
+ ERB + + expected = <<~ERB +
+ <% render_parent %> +
+ ERB + + assert_equal expected, corrected_content + end + + def test_replaces_echoed_super_with_render_parent + @file = <<~ERB +
+ <%= super %> +
+ ERB + + expected = <<~ERB +
+ <%= render_parent %> +
+ ERB + + assert_equal expected, corrected_content + end + + def test_replaces_super_when_nested + @file = <<~ERB +
+ <% 3.times { |_i| super } %> +
+ ERB + + expected = <<~ERB +
+ <% 3.times { |_i| render_parent } %> +
+ ERB + + assert_equal expected, corrected_content + end + + private + + def linter_class + ERBLint::Linters::SuperInComponentTemplates + end +end