Skip to content

Commit

Permalink
String concatination (#5)
Browse files Browse the repository at this point in the history
* add support for various string concentration
* refactor handling of certain nodes
jdkandersson authored Dec 20, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 5d89a04 commit fa95756
Showing 4 changed files with 334 additions and 16 deletions.
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

## [Unreleased]

## [v1.1.0] - 2022-12-20

### Added

- Support for string concentration using ` `
- Support for string concentration using `+`
- Support for string concentration using `%`
- Support for string concentration using `str.format`
- Support for string concentration using `str.join`

## [v1.0.1] - 2022-12-19

### Added

- Detailed descriptions for how to resolve linting errors

## [v1.0.0] - 2022-12-19

### Added

- Lint checks for builtin exceptions
- Lint checks for custom exceptions
- Lint checks for exceptions raised with non-constant arguments
- Lint checks for re-raised exceptions

[//]: # "Release links"
[v1.1.0]: https://github.com/jdkandersson/flake8-error-link/releases/v1.1.0
[v1.0.1]: https://github.com/jdkandersson/flake8-error-link/releases/v1.0.1
[v1.0.0]: https://github.com/jdkandersson/flake8-error-link/releases/v1.0.0
140 changes: 125 additions & 15 deletions flake8_error_link.py
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
import builtins
import re
import tomllib
from itertools import chain
from pathlib import Path
from typing import Generator, Iterable, NamedTuple

@@ -75,31 +76,140 @@ def __init__(self, more_info_regex: str = DEFAULT_REGEX) -> None:
self.problems = []
self._more_info_regex = re.compile(rf".*{more_info_regex}.*")

@staticmethod
def _iter_arg_bin_op(node: ast.BinOp) -> Iterable[ast.expr]:
"""Flatenning binary operation.
Args:
node: The node to yield over.
Yields:
All the args including any relevant nested args.
"""
# pylint seems to think self._iter_arg et al doesn't return an iterable
# pylint: disable=not-an-iterable

yield node

# Handle add
if isinstance(node.op, ast.Add):
if isinstance(node.left, ast.Constant) and isinstance(node.left.value, str):
yield from Visitor._iter_arg(node.left)
if isinstance(node.left, ast.BinOp):
yield from Visitor._iter_arg_bin_op(node.left)
if isinstance(node.right, ast.Constant) and isinstance(node.right.value, str):
yield from Visitor._iter_arg(node.right)

# Handle modulus
if (
isinstance(node.op, ast.Mod)
and isinstance(node.left, ast.Constant)
and isinstance(node.left.value, str)
):
yield from Visitor._iter_arg(node.left)
if isinstance(node.right, ast.Tuple):
yield from Visitor._iter_args(node.right.elts)
if isinstance(node.right, ast.Constant):
yield from Visitor._iter_arg(node.right)

@staticmethod
def _iter_arg_call(node: ast.Call) -> Iterable[ast.expr]:
"""Flatenning call operation.
Args:
node: The node to yield over.
Yields:
All the args including any relevant nested args.
"""
# pylint seems to think self._iter_arg et al doesn't return an iterable
# pylint: disable=not-an-iterable

yield node

# Handle str.format, need it all to be in one expression so that mypy works
if (
hasattr(node, "func") # pylint: disable=too-many-boolean-expressions
and hasattr(node.func, "attr")
and node.func.attr == "format"
and hasattr(node.func, "value")
and (
(
isinstance(node.func.value, ast.Constant)
and isinstance(node.func.value.value, str)
)
or isinstance(node.func.value, ast.Name)
)
and hasattr(node, "args")
):
yield from Visitor._iter_arg(node.func.value)
yield from Visitor._iter_args(node.args)

# Handle str.join, need it all to be in one expression so that mypy works
if (
hasattr(node, "func") # pylint: disable=too-many-boolean-expressions
and hasattr(node.func, "attr")
and node.func.attr == "join"
and hasattr(node.func, "value")
and (
(
isinstance(node.func.value, ast.Constant)
and isinstance(node.func.value.value, str)
)
or isinstance(node.func.value, ast.Name)
)
and hasattr(node, "args")
and isinstance(node.args, list)
and len(node.args) == 1
and isinstance(node.args[0], (ast.List, ast.Set, ast.Tuple))
):
yield from Visitor._iter_arg(node.func.value)
yield from Visitor._iter_args(node.args[0].elts)

@staticmethod
def _iter_arg(node: ast.expr) -> Iterable[ast.expr]:
"""Flatenning certain argument types.
Yields certain nested expressions for some kinds of expressions.
Args:
node: The node to yield over.
Yields:
All the args including any relevant nested args.
"""
# pylint seems to think self._iter_arg et al doesn't return an iterable
# pylint: disable=not-an-iterable

match type(node):
case ast.JoinedStr:
assert isinstance(node, ast.JoinedStr)
yield node
yield from Visitor._iter_args(node.values)
case ast.NamedExpr:
assert isinstance(node, ast.NamedExpr)
yield node
yield from Visitor._iter_arg(node.value)
case ast.BinOp:
assert isinstance(node, ast.BinOp)
yield from Visitor._iter_arg_bin_op(node)
case ast.Call:
assert isinstance(node, ast.Call)
yield from Visitor._iter_arg_call(node)
case _:
yield node

@staticmethod
def _iter_args(nodes: list[ast.expr]) -> Iterable[ast.expr]:
"""Iterate over the args whilst flatenning certain argument types.
Yields node and node.values from JoinedStr and node and node.value from NamedExpr and node
in all other cases.
Args:
nodes: The nodes to iterate over.
Yields:
All the args including any relevant nested args.
"""
for node in nodes:
match type(node):
case ast.JoinedStr:
assert isinstance(node, ast.JoinedStr)
yield node
yield from node.values
case ast.NamedExpr:
assert isinstance(node, ast.NamedExpr)
yield node
yield node.value
case _:
yield node
return chain.from_iterable(map(Visitor._iter_arg, nodes))

@staticmethod
def _includes_variable(node: ast.Call) -> bool:
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "flake8-error-link"
version = "1.0.1"
version = "1.1.0"
description = "A linter that ensures all raised Exceptions include an error with a link to more information"
authors = ["David Andersson <[email protected]>"]
license = "Apache 2.0"
175 changes: 175 additions & 0 deletions tests/test_flake8_error_link.py
Original file line number Diff line number Diff line change
@@ -102,6 +102,46 @@ def _result(code: str) -> tuple[str, ...]:
(f"1:0 {BUILTIN_MSG}",),
id="more information not provided from",
),
pytest.param(
"raise Exception(1 + 1)",
(f"1:0 {VARIABLE_INCLUDED_MSG}",),
id="more information not provided +",
),
pytest.param(
"raise Exception(1 + 1 + 1)",
(f"1:0 {VARIABLE_INCLUDED_MSG}",),
id="more information not provided + multiple",
),
pytest.param(
"raise Exception(1 % 1)",
(f"1:0 {VARIABLE_INCLUDED_MSG}",),
id="more information not provided %",
),
pytest.param(
f'raise Exception(1 % "{_VALID_RAISE_MSG}")',
(f"1:0 {VARIABLE_INCLUDED_MSG}",),
id="more information not provided % right string",
),
pytest.param(
'raise Exception("%s" % 1)',
(f"1:0 {VARIABLE_INCLUDED_MSG}",),
id="more information not provided % left string",
),
pytest.param(
'raise Exception("%s" % [1])',
(f"1:0 {VARIABLE_INCLUDED_MSG}",),
id="more information not provided % right list",
),
pytest.param(
f'raise Exception([].join(["{_VALID_RAISE_MSG}"]))',
(f"1:0 {VARIABLE_INCLUDED_MSG}",),
id="more information not provided join not on string",
),
pytest.param(
f'raise Exception([].format("{_VALID_RAISE_MSG}"))',
(f"1:0 {VARIABLE_INCLUDED_MSG}",),
id="more information not provided format not on string",
),
pytest.param(
f'raise Exception("{_VALID_RAISE_MSG}")',
(),
@@ -132,6 +172,141 @@ def _result(code: str) -> tuple[str, ...]:
(),
id="more information provided first args",
),
pytest.param(
f'raise Exception("{_VALID_RAISE_MSG}" + "trailing ")',
(),
id="more information provided string + first",
),
pytest.param(
f'raise Exception("leading " + "{_VALID_RAISE_MSG}")',
(),
id="more information provided string + second",
),
pytest.param(
f'raise Exception("leading " + "{_VALID_RAISE_MSG}" + "trailing")',
(),
id="more information provided string multiple +",
),
pytest.param(
f'raise Exception("{_VALID_RAISE_MSG}" + trailing)',
(),
id="more information provided string + first variable",
),
pytest.param(
f'raise Exception(leading + "{_VALID_RAISE_MSG}")',
(),
id="more information provided string + second variable",
),
pytest.param(
f'raise Exception("{_VALID_RAISE_MSG}" "trailing ")',
(),
id="more information provided string space first",
),
pytest.param(
f'raise Exception("leading " "{_VALID_RAISE_MSG}")',
(),
id="more information provided string space second",
),
pytest.param(
f'raise Exception("leading " "{_VALID_RAISE_MSG}" "trailing")',
(),
id="more information provided string multiple space",
),
pytest.param(
f'raise Exception("leading " "{_VALID_RAISE_MSG}" + "trailing")',
(),
id="more information provided string multiple space and +",
),
pytest.param(
f'raise Exception("%s".format("{_VALID_RAISE_MSG}"))',
(),
id="more information provided string format more info in argument",
),
pytest.param(
f'raise Exception("%s".format("{_VALID_RAISE_MSG}", variable))',
(),
id="more information provided string format more info in argument with variable",
),
pytest.param(
f'raise Exception(variable.format("{_VALID_RAISE_MSG}"))',
(),
id="more information provided string format variable",
),
pytest.param(
f'raise Exception("{_VALID_RAISE_MSG} %s".format(""))',
(),
id="more information provided string format more info in string",
),
pytest.param(
f'raise Exception("".join(["{_VALID_RAISE_MSG}"]))',
(),
id="more information provided string join more info in argument list",
),
pytest.param(
f'raise Exception(variable.join(["{_VALID_RAISE_MSG}"]))',
(),
id="more information provided string join variable",
),
pytest.param(
f'raise Exception("".join(["{_VALID_RAISE_MSG}", variable]))',
(),
id="more information provided string join more info in argument list with variable",
),
pytest.param(
f'raise Exception("".join(("{_VALID_RAISE_MSG}",)))',
(),
id="more information provided string join more info in argument tuple",
),
pytest.param(
f'raise Exception("".join({{"{_VALID_RAISE_MSG}"}}))',
(),
id="more information provided string join more info in argument set",
),
pytest.param(
f'raise Exception("".join(["{_VALID_RAISE_MSG}", "second"]))',
(),
id="more information provided string join more info in argument first",
),
pytest.param(
f'raise Exception("".join(["first", "{_VALID_RAISE_MSG}"]))',
(),
id="more information provided string join more info in argument second",
),
pytest.param(
f'raise Exception("{_VALID_RAISE_MSG}".join([""]))',
(),
id="more information provided string join more info in string",
),
pytest.param(
f'raise Exception("%s" % "{_VALID_RAISE_MSG}")',
(),
id="more information provided string %",
),
pytest.param(
f'raise Exception("%s" % ("{_VALID_RAISE_MSG}",))',
(),
id="more information provided string % tuple",
),
pytest.param(
f'raise Exception("%s" % ("{_VALID_RAISE_MSG}", "trailing "))',
(),
id="more information provided string % multiple first",
),
pytest.param(
f'raise Exception("%s" % ("leading ", "{_VALID_RAISE_MSG}"))',
(),
id="more information provided string % multiple second",
),
pytest.param(
f'raise Exception("%s" % ("leading ", "{_VALID_RAISE_MSG}", "trailing"))',
(),
id="more information provided string % multiple many",
),
pytest.param(
f'raise Exception("{_VALID_RAISE_MSG} %s" % "right")',
(),
id="more information provided string % in left",
),
pytest.param(
f'raise Exception("other text {_VALID_RAISE_MSG}")',
(),

0 comments on commit fa95756

Please sign in to comment.