Skip to content

Commit f682844

Browse files
committed
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.
1 parent a1d8508 commit f682844

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

Library/Homebrew/rubocops/lines.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,10 +581,33 @@ def audit_formula(formula_nodes)
581581
class PythonVersions < FormulaCop
582582
extend AutoCorrector
583583

584+
PYTHON_VERSION_REFERENCE_REGEX = /^python(@)?(\d\.\d+)$/
585+
586+
HARDCODED_PYTHON_ASSIGNMENT_MSG =
587+
"`python = \"pythonX.Y\"` should use dynamic version detection: " \
588+
"`python = \"python\#{Language::Python.major_minor_version libexec/\"bin/python\"}\"`"
589+
584590
sig { override.params(formula_nodes: FormulaNodes).void }
585591
def audit_formula(formula_nodes)
586592
return if (body_node = formula_nodes.body_node).nil?
587593

594+
body_node.each_descendant(:lvasgn) do |assignment_node|
595+
variable_name = assignment_node.children.first
596+
next unless [:python, :python3].include?(variable_name)
597+
598+
value = assignment_node.children.last
599+
next unless value.is_a?(RuboCop::AST::StrNode)
600+
next unless PYTHON_VERSION_REFERENCE_REGEX.match?(string_content(value))
601+
602+
offending_node(value)
603+
problem HARDCODED_PYTHON_ASSIGNMENT_MSG do |corrector|
604+
corrector.replace(
605+
value.source_range,
606+
"\"python\#{Language::Python.major_minor_version libexec/\"bin/python\"}\"",
607+
)
608+
end
609+
end
610+
588611
python_formula_node = find_every_method_call_by_name(body_node, :depends_on).find do |dep|
589612
string_content(parameters(dep).fetch(0)).start_with? "python@"
590613
end
@@ -605,7 +628,7 @@ def audit_formula(formula_nodes)
605628
find_strings(body_node).each do |str|
606629
content = string_content(str)
607630

608-
next unless (match = content.match(/^python(@)?(\d\.\d+)$/))
631+
next unless (match = content.match(PYTHON_VERSION_REFERENCE_REGEX))
609632
next if python_version == match[2]
610633

611634
fix = if match[1]

Library/Homebrew/test/rubocops/text/python_versions_spec.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,75 @@ def install
220220
end
221221
RUBY
222222
end
223+
224+
it "reports and corrects hardcoded `python = \"pythonX.Y\"` assignments" do
225+
expect_offense(<<~'RUBY')
226+
class Foo < Formula
227+
depends_on "python@3.14"
228+
229+
def install
230+
python = "python3.12"
231+
^^^^^^^^^^^^ FormulaAudit/PythonVersions: `python = "pythonX.Y"` should use dynamic version detection: `python = "python#{Language::Python.major_minor_version libexec/"bin/python"}"`
232+
end
233+
end
234+
RUBY
235+
236+
expect_correction(<<~'RUBY')
237+
class Foo < Formula
238+
depends_on "python@3.14"
239+
240+
def install
241+
python = "python#{Language::Python.major_minor_version libexec/"bin/python"}"
242+
end
243+
end
244+
RUBY
245+
end
246+
247+
it "reports no offenses for dynamic python assignments" do
248+
expect_no_offenses(<<~'RUBY')
249+
class Foo < Formula
250+
depends_on "python@3.14"
251+
252+
def install
253+
python = "python#{Language::Python.major_minor_version libexec/"bin/python"}"
254+
end
255+
end
256+
RUBY
257+
end
258+
259+
it "reports and corrects hardcoded `python3 = \"pythonX.Y\"` assignments" do
260+
expect_offense(<<~'RUBY')
261+
class Foo < Formula
262+
depends_on "python@3.14"
263+
264+
def install
265+
python3 = "python3.12"
266+
^^^^^^^^^^^^ FormulaAudit/PythonVersions: `python = "pythonX.Y"` should use dynamic version detection: `python = "python#{Language::Python.major_minor_version libexec/"bin/python"}"`
267+
end
268+
end
269+
RUBY
270+
271+
expect_correction(<<~'RUBY')
272+
class Foo < Formula
273+
depends_on "python@3.14"
274+
275+
def install
276+
python3 = "python#{Language::Python.major_minor_version libexec/"bin/python"}"
277+
end
278+
end
279+
RUBY
280+
end
281+
282+
it "reports no offenses for hardcoded python version assigned to non-python local" do
283+
expect_no_offenses(<<~RUBY)
284+
class Foo < Formula
285+
depends_on "python@3.14"
286+
287+
def install
288+
interpreter = "python3.12"
289+
end
290+
end
291+
RUBY
292+
end
223293
end
224294
end

0 commit comments

Comments
 (0)