From 55d4c3f553493044ec2fc3a84e4a6a4ba9f98a83 Mon Sep 17 00:00:00 2001 From: Michka Popoff Date: Sun, 9 Aug 2026 17:24:27 +0200 Subject: [PATCH] Improve PythonVersions cop to reject hardcoded pythonX.Y assignment Enforce dynamic Python version detection in formula code by flagging hardcoded pythonX.Y strings assigned to python and autocorrecting to Language::Python.major_minor_version with libexec/bin/python. Generalize detection using a shared Python version regex and update PythonVersions specs accordingly. --- Library/Homebrew/rubocops/lines.rb | 25 ++++++- .../rubocops/text/python_versions_spec.rb | 70 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/Library/Homebrew/rubocops/lines.rb b/Library/Homebrew/rubocops/lines.rb index 4d6e09cb1fde1..7aae44b8a22c7 100644 --- a/Library/Homebrew/rubocops/lines.rb +++ b/Library/Homebrew/rubocops/lines.rb @@ -581,10 +581,33 @@ def audit_formula(formula_nodes) class PythonVersions < FormulaCop extend AutoCorrector + PYTHON_VERSION_REFERENCE_REGEX = /^python(@)?(\d\.\d+)$/ + + HARDCODED_PYTHON_ASSIGNMENT_MSG = + "`python = \"pythonX.Y\"` should use dynamic version detection: " \ + "`python = \"python\#{python_major_minor(libexec/\"bin/python)\")}\"`" + sig { override.params(formula_nodes: FormulaNodes).void } def audit_formula(formula_nodes) return if (body_node = formula_nodes.body_node).nil? + body_node.each_descendant(:lvasgn) do |assignment_node| + variable_name = assignment_node.children.first + next unless [:python, :python3].include?(variable_name) + + value = assignment_node.children.last + next unless value.is_a?(RuboCop::AST::StrNode) + next unless PYTHON_VERSION_REFERENCE_REGEX.match?(string_content(value)) + + offending_node(value) + problem HARDCODED_PYTHON_ASSIGNMENT_MSG do |corrector| + corrector.replace( + value.source_range, + "\"python\#{python_major_minor(libexec/\"bin/python)\")}\"", + ) + end + end + python_formula_node = find_every_method_call_by_name(body_node, :depends_on).find do |dep| string_content(parameters(dep).fetch(0)).start_with? "python@" end @@ -605,7 +628,7 @@ def audit_formula(formula_nodes) find_strings(body_node).each do |str| content = string_content(str) - next unless (match = content.match(/^python(@)?(\d\.\d+)$/)) + next unless (match = content.match(PYTHON_VERSION_REFERENCE_REGEX)) next if python_version == match[2] fix = if match[1] diff --git a/Library/Homebrew/test/rubocops/text/python_versions_spec.rb b/Library/Homebrew/test/rubocops/text/python_versions_spec.rb index 32618a9c6c14c..988e343661c3e 100644 --- a/Library/Homebrew/test/rubocops/text/python_versions_spec.rb +++ b/Library/Homebrew/test/rubocops/text/python_versions_spec.rb @@ -220,5 +220,75 @@ def install end RUBY end + + it "reports and corrects hardcoded `python = \"pythonX.Y\"` assignments" do + expect_offense(<<~'RUBY') + class Foo < Formula + depends_on "python@3.14" + + def install + python = "python3.12" + ^^^^^^^^^^^^ FormulaAudit/PythonVersions: `python = "pythonX.Y"` should use dynamic version detection: `python = "python#{python_major_minor(libexec/"bin/python)")}"` + end + end + RUBY + + expect_correction(<<~'RUBY') + class Foo < Formula + depends_on "python@3.14" + + def install + python = "python#{python_major_minor(libexec/"bin/python)")}" + end + end + RUBY + end + + it "reports no offenses for dynamic python assignments" do + expect_no_offenses(<<~'RUBY') + class Foo < Formula + depends_on "python@3.14" + + def install + python = "python#{python_major_minor(libexec/"bin/python)")}" + end + end + RUBY + end + + it "reports and corrects hardcoded `python3 = \"pythonX.Y\"` assignments" do + expect_offense(<<~'RUBY') + class Foo < Formula + depends_on "python@3.14" + + def install + python3 = "python3.12" + ^^^^^^^^^^^^ FormulaAudit/PythonVersions: `python = "pythonX.Y"` should use dynamic version detection: `python = "python#{python_major_minor(libexec/"bin/python)")}"` + end + end + RUBY + + expect_correction(<<~'RUBY') + class Foo < Formula + depends_on "python@3.14" + + def install + python3 = "python#{python_major_minor(libexec/"bin/python)")}" + end + end + RUBY + end + + it "reports no offenses for hardcoded python version assigned to non-python local" do + expect_no_offenses(<<~RUBY) + class Foo < Formula + depends_on "python@3.14" + + def install + interpreter = "python3.14" + end + end + RUBY + end end end