Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion Library/Homebrew/rubocops/lines.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
70 changes: 70 additions & 0 deletions Library/Homebrew/test/rubocops/text/python_versions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
iMichka marked this conversation as resolved.

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
Loading