From 5b1f863e39d17e9d7c672c48f65bfd22483a7c2e Mon Sep 17 00:00:00 2001 From: Ryan Quanz Date: Sun, 29 Mar 2026 23:26:21 -0400 Subject: [PATCH] perf: add fast string pre-checks to skip unnecessary parsing Add cheap string checks before expensive RubyNode.parse and Tag.from_node calls in several linters: - NoJavascriptTagHelper: skip parsing unless source contains 'javascript_tag' - RequireInputAutocomplete: skip parsing unless source matches a form helper name pattern; skip Tag.from_node for non-input tags - RequireScriptNonce: skip parsing unless source matches a tag helper pattern; skip Tag.from_node for non-script tags - AllowedScriptType: skip Tag.from_node for non-script tags These linters previously parsed every ERB snippet or created Tag objects for every HTML tag, even when the vast majority could not possibly match. --- lib/erb_lint/linters/allowed_script_type.rb | 5 ++++- lib/erb_lint/linters/no_javascript_tag_helper.rb | 3 +++ lib/erb_lint/linters/require_input_autocomplete.rb | 10 ++++++++++ lib/erb_lint/linters/require_script_nonce.rb | 10 ++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/erb_lint/linters/allowed_script_type.rb b/lib/erb_lint/linters/allowed_script_type.rb index 64a55c2b..1fff3ff2 100644 --- a/lib/erb_lint/linters/allowed_script_type.rb +++ b/lib/erb_lint/linters/allowed_script_type.rb @@ -23,9 +23,12 @@ class ConfigSchema < LinterConfig def run(processed_source) parser = processed_source.parser parser.nodes_with_type(:tag).each do |tag_node| + # Fast path: check tag name from AST node directly before creating Tag object + tag_name_node = tag_node.to_a[1] + next unless tag_name_node&.loc&.source == "script" + tag = BetterHtml::Tree::Tag.from_node(tag_node) next if tag.closing? - next unless tag.name == "script" if @config.disallow_inline_scripts? name_node = tag_node.to_a[1] diff --git a/lib/erb_lint/linters/no_javascript_tag_helper.rb b/lib/erb_lint/linters/no_javascript_tag_helper.rb index 3c25dd6c..28efcdef 100644 --- a/lib/erb_lint/linters/no_javascript_tag_helper.rb +++ b/lib/erb_lint/linters/no_javascript_tag_helper.rb @@ -25,6 +25,9 @@ def run(processed_source) source = code_node.loc.source + # Fast path: skip expensive parsing if source can't contain the target method + next unless source.include?("javascript_tag") + ruby_node = begin BetterHtml::TestHelper::RubyNode.parse(source) diff --git a/lib/erb_lint/linters/require_input_autocomplete.rb b/lib/erb_lint/linters/require_input_autocomplete.rb index 4a779e5f..3d747c9e 100644 --- a/lib/erb_lint/linters/require_input_autocomplete.rb +++ b/lib/erb_lint/linters/require_input_autocomplete.rb @@ -52,6 +52,10 @@ def run(processed_source) def find_html_input_tags(parser) parser.nodes_with_type(:tag).each do |tag_node| + # Fast path: check tag name from AST node directly before creating Tag object + tag_name_node = tag_node.to_a[1] + next unless tag_name_node&.loc&.source == "input" + tag = BetterHtml::Tree::Tag.from_node(tag_node) autocomplete_attribute = tag.attributes["autocomplete"] @@ -82,10 +86,16 @@ def html_type_requires_autocomplete_attribute?(type_attribute) type_present && HTML_INPUT_TYPES_REQUIRING_AUTOCOMPLETE.include?(type_attribute.value) end + FORM_HELPER_NAMES_PATTERN = Regexp.union(FORM_HELPERS_REQUIRING_AUTOCOMPLETE.map(&:to_s)).freeze + def find_rails_helper_input_tags(parser) parser.ast.descendants(:erb).each do |erb_node| indicator_node, _, code_node, _ = *erb_node source = code_node.loc.source + + # Fast path: skip expensive parsing if source can't contain any form helper name + next unless source.match?(FORM_HELPER_NAMES_PATTERN) + ruby_node = extract_ruby_node(source) send_node = ruby_node&.descendants(:send)&.first diff --git a/lib/erb_lint/linters/require_script_nonce.rb b/lib/erb_lint/linters/require_script_nonce.rb index e575dfbb..76d3a590 100644 --- a/lib/erb_lint/linters/require_script_nonce.rb +++ b/lib/erb_lint/linters/require_script_nonce.rb @@ -21,6 +21,10 @@ def run(processed_source) def find_html_script_tags(parser) parser.nodes_with_type(:tag).each do |tag_node| + # Fast path: check tag name from AST node directly before creating Tag object + tag_name_node = tag_node.to_a[1] + next unless tag_name_node&.loc&.source == "script" + tag = BetterHtml::Tree::Tag.from_node(tag_node) nonce_attribute = tag.attributes["nonce"] @@ -52,10 +56,16 @@ def html_javascript_type_attribute?(tag) type_attribute.value_node.to_a[1] != "application/javascript" end + TAG_HELPER_PATTERN = /javascript_tag|javascript_include_tag|javascript_pack_tag/ + def find_rails_helper_script_tags(parser) parser.ast.descendants(:erb).each do |erb_node| indicator_node, _, code_node, _ = *erb_node source = code_node.loc.source + + # Fast path: skip expensive parsing if source can't contain any tag helper name + next unless source.match?(TAG_HELPER_PATTERN) + ruby_node = extract_ruby_node(source) send_node = ruby_node&.descendants(:send)&.first