Skip to content

Reject non-literal binary operations at any AST depth - #715

Open
Popy21 wants to merge 1 commit into
google:masterfrom
Popy21:fix/issue-97
Open

Popy21 wants to merge 1 commit into
google:masterfrom
Popy21:fix/issue-97

Conversation

@Popy21

@Popy21 Popy21 commented Oct 16, 2026

Copy link
Copy Markdown

Fixes #97.

Reject non-literal binary operations at any AST depth


Prepared with AI assistance (Claude Code) as part of a personal daily
open-source contribution effort. Validated locally before submission: this
repository's test suite (python3 -m pytest -q) passes on this branch. Happy to adjust
based on review — and happy to stop opening PRs on this repo if the
maintainers would rather not receive automated contributions.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the AST parser in fire/parser.py to reject binary operations anywhere in the parsed expression, with an exception for complex number literals, and adds corresponding tests. The review feedback identifies a performance bottleneck in _IsComplexLiteral when evaluating large containers, as well as an artificial restriction that prevents parsing complex numbers with the imaginary part written first (e.g., 2j+1). Suggestions are provided to optimize the type checking before evaluation and to support both orderings of complex literals.

Comment thread fire/parser.py
Comment on lines +125 to +146
def _IsComplexLiteral(node):
"""Returns whether a binary operation expresses a complex number literal.

Python has no single-token syntax for a complex number with both a real and
an imaginary part, so such numbers are written as the sum or difference of a
real number and an imaginary one, e.g. 1+2j. These are the only binary
operations ast.literal_eval accepts, so they are the only ones Fire accepts.

Args:
node: A node of type BinOp.
Returns:
True if the node has the form <real number> + <imaginary number>, with
either + or -, False otherwise.
"""
if not isinstance(node.op, (ast.Add, ast.Sub)):
return False
try:
real = ast.literal_eval(node.left)
imaginary = ast.literal_eval(node.right)
except (TypeError, ValueError):
return False
return isinstance(real, (int, float)) and isinstance(imaginary, complex)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of _IsComplexLiteral has two issues:

  1. Performance Bottleneck: It calls ast.literal_eval on node.left and node.right without checking their types first. If either operand is a large container (e.g., a list or dictionary with thousands of elements), ast.literal_eval will fully evaluate it, leading to unnecessary overhead and potential performance degradation.
  2. Incorrect Ordering Restriction: It assumes the real part must always come first (e.g., 1+2j). However, Python's ast.literal_eval natively supports complex numbers with the imaginary part first (e.g., 2j+1).

We can resolve both issues by:

  • Adding a fast helper _IsNumericNode to verify that both operands are simple numeric literals (or unary operations on them) before calling ast.literal_eval.
  • Checking both orderings (real + imaginary and imaginary + real) to fully support standard Python complex literals.
def _IsNumericNode(node):
  if isinstance(node, ast.Constant):
    return isinstance(node.value, (int, float, complex))
  if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
    return _IsNumericNode(node.operand)
  if hasattr(ast, 'Num') and isinstance(node, ast.Num):
    return isinstance(node.n, (int, float, complex))
  return False


def _IsComplexLiteral(node):
  """Returns whether a binary operation expresses a complex number literal.

  Python has no single-token syntax for a complex number with both a real and
  an imaginary part, so such numbers are written as the sum or difference of a
  real number and an imaginary one, e.g. 1+2j. These are the only binary
  operations ast.literal_eval accepts, so they are the only ones Fire accepts.

  Args:
    node: A node of type BinOp.
  Returns:
    True if the node has the form <real number> + <imaginary number>, with
    either + or -, False otherwise.
  """
  if not isinstance(node.op, (ast.Add, ast.Sub)):
    return False
  if not (_IsNumericNode(node.left) and _IsNumericNode(node.right)):
    return False
  try:
    left_val = ast.literal_eval(node.left)
    right_val = ast.literal_eval(node.right)
  except (TypeError, ValueError):
    return False
  return (
      (isinstance(left_val, (int, float)) and isinstance(right_val, complex)) or
      (isinstance(left_val, complex) and isinstance(right_val, (int, float)))
  )

Comment thread fire/parser_test.py
Comment on lines +157 to +158
# The real part must come first, as it must for ast.literal_eval.
self.assertEqual(parser.DefaultParseValue('2j+1'), '2j+1')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since we updated _IsComplexLiteral to support both orderings of complex literals (matching Python's standard ast.literal_eval behavior), we should update this test case to assert that 2j+1 is successfully parsed as a complex number 1 + 2j instead of being treated as a string.

Suggested change
# The real part must come first, as it must for ast.literal_eval.
self.assertEqual(parser.DefaultParseValue('2j+1'), '2j+1')
# Both real part first and imaginary part first are supported.
self.assertEqual(parser.DefaultParseValue('2j+1'), 1 + 2j)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Only plain literals should be parsed

1 participant