Skip to content

Custom Rule Condition Syntax and Methods

The condition field on a Rule is used to restrict pattern matches based on boolean logic. It supports a subset of Python syntax.

See Also

Syntax

Most built-in Python syntax is supported, for example:

  • and, or, and not expressions
  • <, >, ==, and other boolean operators
  • strings, integers, None, and other literals
  • all, any, list, set, len, and other built-in functions

Conditions themselves use a predefined set of methods (see Conditions below). These apply to variable names defined in the pattern of the rule (see Reference: Pattern Syntax Capture Expressions for further reference).

Note

In the following examples, the tests field is used to show examples of what the pattern matches. See Reference: Rule Configuration tests Field for more information.

Single-variable conditions

Captured variables can be conditioned using methods.

Examples

Match any integer assignments to x (see has_type below):

pattern: x = ${i}
condition: i.has_type("int")
tests:
    - match: x = 3
    - no-match: x = "y"

Match classes whose name begins with "Sourcery" (see matches_regex below):

pattern: |
    class ${cls}(...):
        ...
condition: cls.matches_regex("^Sourcery\w*$")
tests:
    - match: |
        class SourceryException(Exception):
            pass
    - no-match: |
        class AdHocSourceryVisitor(Visitor):
            function: typing.Callable[[typing.Any], typing.Any]

Complex conditions

Using boolean expressions, you can combine conditions captured variables.

Examples

Multiple conditions on a single variable (see is_lower_case and starts_with below):

pattern: ${assignee} = ${assignment}
condition: assignee.is_lower_case() and assignee.starts_with("_")
tests:
    - match: _private_variable = 42  # lowercase and starts with "_"
    - no-match: private_variable = 42  # lowercase, but does not start with "_"
    - no-match: _PRIVATE_VARIABLE = 42  # starts with "_", but is not lowercase

Conditions involving more than one variable (see starts_with and statement_count below):

pattern: |
  def ${fn}(...):
      ${statements+}
condition: fn.starts_with("_") and statements.statement_count() > 40
description: Private functions should not exceed a certain length.

Pattern-level conditions

The special name pattern is used to refer to the entire match instead of only to specific captures. In the example below, pattern.in_module_scope() (see in_module_scope) means that the condition is applied to the whole pattern (in this case the assignment) rather than any of the variables within it. Note that you can combine pattern conditions with any other conditions, as shown.

pattern: ${var} = ${value}
condition: |
  pattern.in_module_scope()
    and not var.is_upper_case()
    and not var.starts_with("_")
description: Don't declare `${var}` as a global variable

Conditions

This is a complete list of the methods you can use on captures (and patterns). If none of these conditions fit your use-case, feel free to suggest a new one on GitHub.

Condition Description
capture.is_exception_type() Check if capture represents an exception type, like ValueError.
capture.is_conditional_expression() Returns true for a condition expression (ternary operator).
capture.is_expression_statement() Returns true for a statement containing just an expression.
capture.is_literal() Returns whether the capture is a literal.
capture.is_str_literal() Returns True for string literal constants.
capture.is_numeric_literal() Returns True for numeric literal constants.
capture.is_none_literal() Returns true for a None literal.
capture.matches_regex(pattern: str) Returns true if the regex pattern is found anywhere in capture.
capture.is_lower_case() Return True if all cased characters in the name are lower case.
capture.is_upper_case() Return True if all cased characters in the name are upper case.
capture.is_identifier() Return True if the name is a valid identifier.
capture.is_snake_case() Returns true if the name is lower case, also allowing underscores.
capture.is_dunder_name() Return True if the name starts and ends with double underscores.
capture.is_upper_camel_case() Returns true if the name is only upper or lower case, with no underscores.
left.equals(right: Capture | str) Returns true if left and right names are the same.
capture.character_count() Counts the number of characters.
capture.starts_with(prefix: str) Returns true if the name starts with the specified prefix.
capture.ends_with(suffix: str) Returns true if the name ends with the specified suffix.
capture.in_module_scope() Returns true if the capture is found in module scope.
capture.in_class_scope(name: str = None) Returns true if the capture is found in class scope.
capture.in_function_scope(name: str = None) Returns true if the capture is found in function scope.
capture.in_except_clause() Returns true if the capture is found in an exception clause.
capture.in_finally_clause() Returns true if the capture is found in a finally clause.
capture.statement_count() Returns the number of statements in the capture.
capture.has_quote(quote: str) Returns true if the string uses quote.
capture.with_quote(quote: str) Quotes the string using the quote.
capture.has_type(*types: str) Return true if the capture's inferred type is any of types.
capture.has_exception_type() Returns true if the inferred type of the capture is an Exception.

Capture Conditions

Contains conditions about what has been captured.

capture.is_exception_type() -> bool

Check if capture represents an exception type, like ValueError.

rules:
- id: raise-created-exception
  pattern: |
    ${var} = ${value}
    raise ${var}
  condition: value.is_exception_type()
  replacement: |
    raise ${value}
  description: Directly `raise` exceptions instead of assigning them first

capture.is_conditional_expression() -> bool

Returns true for a condition expression (ternary operator).

Here is an example of a conditional expression:

"sky" if looking_up() else "ground"

And this is a rule that uses this condition:

rules:
- id: conditional-expression-arg
  description: Do not use conditional expressions as arguments
  pattern: ${call}(..., ${arg}, ...)
  condition: arg.is_conditional_expression()
  tests:
  - match: print("hi" if hello else "bye", "world")
  - no-match: print("Hello world!")

See Python docs for more details about conditional expressions.

capture.is_expression_statement() -> bool

Returns true for a statement containing just an expression.

Here's some examples: - print("Hello world") - true - my_list.append(item) - true - name = "Hello world" - false - def func(): ... - false - break - false - pass - false

And this is a rule that uses the condition:

rules:
- id: raise-created-exception
  pattern: ${expr}
  condition: |
    expr.is_expression_statement()
      and (expr.is_exception_type() or expr.has_exception_type())
  replacement: raise ${expr}
  description: Exceptions should not be created without being raised
  tests:
  - match: Exception
  - match: ValueError()
  - no-match: raise Exception
  - no-match: raise ValueError()

Literal Conditions

Conditions related to literals.

capture.is_literal() -> bool

Returns whether the capture is a literal.

rules:
- id: cannot-raise-literal
  pattern: raise ${a}
  condition: a.is_literal()
  description: Cannot raise a literal - did you mean to raise an Exception?
  tests:
    - match: raise 'string'
    - no-match: raise
    - no-match: raise Exception
    - no-match: raise CustomError

capture.is_str_literal() -> bool

Returns True for string literal constants.

rules:
- id: no-hard-coded-file-name
  description: Hard-coded file name
  pattern: ${var} = ${value}
  condition: |
    value.is_str_literal()
      and (var.ends_with("file")
            or var.ends_with("file_name")
            or var.ends_with("dir")
            or var.ends_with("dir_name"))
  explanation: |
    File and directory names should be read from a parameter or the config.
  tests:
    - match: input_file = "test.txt"
    - match: file_name = "placeholder.py"
    - match: working_dir = "/home/user/test"
    - no-match: file_name = f"test_{module}.py"
    - no-match: other_file = input_file
    - no-match: working_dir = Path()

capture.is_numeric_literal() -> bool

Returns True for numeric literal constants.

rules:
- id: no-magic_numbers
  description: Don't assign a magic number to a variable.
  pattern: ${var} = ${value}
  condition: value.is_numeric_literal()
  tests:
    - match: nr = 42
    - match: pi_nr = 3.14
    - no-match: nr = other_nr
    - no-match: nr = 5 + 7
    - no-match: nr = a + 42

capture.is_none_literal() -> bool

Returns true for a None literal.

rules:
- id: do-not-return-value-from-init
  description: __init__ should not return a value
  pattern: return ${value}
  condition: |
      not value.is_none_literal() and pattern.in_function_scope("__init__")
  tests:
    - match: |
        class Example:
            def __init__(stuff):
                self.stuff = stuff
                return self
    - no-match: |
        class Example:
            def __init__(stuff):
                self.stuff = stuff
                return None

Name Conditions

Conditions related to capture names.

Note that all name conditions are themselves named in snake_case instead of the standard Python naming for str functions which uses no underscores.

Python str function Sourcery condition
str.islower capture.is_lower_case
str.isupper capture.is_upper_case
str.startswith capture.starts_with
str.endswith capture.ends_with

This is so that all Sourcery conditions are consistently named, and also allows easy addition of conditions like is_snake_case which is much better named than issnake 🐍!

capture.matches_regex(pattern: str) -> bool

Returns true if the regex pattern is found anywhere in capture.

This search is unanchored meaning it matches anywhere in the string:

rules:
- id: dont-use-long-numbers-in-vars
  description: Do not use long numbers in variable names
  pattern: ${name} = ${value}
  condition: name.matches_regex(r"\d\d\d")  # Use raw strings to handle backslashes
  tests:
  - match: start123end = 123
  - match: begin123 = "BAD"
  - no-match: a12b34c56 = "OK"

To anchor the search:

  • Use ^ to anchor at the start of the string
  • Use $ to anchor at the end of the string
rules:
- id: dont-use-dunder-vars
  description: Do not use dunder name `${name}` as a variable name
  pattern: ${name} = ${value}
  condition: name.matches_regex("^__.*__$")
  tests:
  - match: __strange__ = 123
  - match: __eq__ = "WRONG"
  - no-match: a__b__c = "OK"

capture.is_lower_case() -> bool

Return True if all cased characters in the name are lower case.

rules:
- id: lower-names
  description: Ensure variable name is lower case
  pattern: ${var} = ${value}
  condition: var.is_lower_case()
  tests:
  - match: banana = 1
  - match: banana2 = 1
  - no-match: BANANA = 1
  - no-match: baNana = 1

capture.is_upper_case() -> bool

Return True if all cased characters in the name are upper case.

rules:
- id: upper-names
  description: Do not reassign the value of constants
  pattern: |
    ${var} = ${value}
    ${var} = ${other_value}
  condition: var.is_upper_case()
  tests:
  - match: |
      BANANA = 1
      BANANA = 2
  - match: |
      BANANA = 1
      BANANA = 1
  - match: |
      BANANA2 = 1
      BANANA2 = 2
  - no-match: BANANA2 = 1
  - no-match: |
      banana = 1
      banana = 2
  - no-match: |
      BAnANA = 1
      BAnANA = 2

capture.is_identifier() -> bool

Return True if the name is a valid identifier.

rules:
- id: valid-identifiers
  description: Ensure variable name is a valid identifier
  pattern: ${var} = ${value}
  condition: not var.is_identifier()
  tests:
  - match: banana[1] = 1
  - match: a.b = 1
  - no-match: BANANA = 1
  - no-match: banana = 1

capture.is_snake_case() -> bool

Returns true if the name is lower case, also allowing underscores.

We suggest using this condition mainly in the negative form. E.g. to detect variable or function names not adhering to snake case.

rules:
- id: snake-case-functions
  pattern: |
    def ${function_name}(...):
      ...
  condition: not function_name.is_snake_case()
  description: Use snake case for function names
  tests:
    - match: |
        def miXed():
          pass
    - match: |
        def UpperCamelCase():
          pass
    - match: |
        class Something:
          def UpperCamelCase(self):
            pass
    - match: |
        def too__many__underscores():
          pass
    - match: |
        def double_underscore_suffix__():
          pass
    - match: |
        def mixed_and_underScore():
          pass
    - no-match: |
        def nice_function_name():
          pass
    - no-match: |
        def _private():
          pass
    - no-match: |
        def __very_private():
          pass
    - no-match: |
        class Something:
          def single(self):
            pass
    - no-match: |
        class Something:
          def __init__(self):
            pass

capture.is_dunder_name() -> bool

Return True if the name starts and ends with double underscores.

rules:
- id: dunder-names
  description: Do not call attributes of dunder variables
  pattern: ${var}.${anything}
  condition: var.is_dunder_name()
  tests:
    - match: __version__.whatever()
    - match: __version__.whatever
    - no-match: __version__ = "1.1"

capture.is_upper_camel_case() -> bool

Returns true if the name is only upper or lower case, with no underscores.

We suggest to use this condition mainly in the negative form. E.g. to detect class names not adhering to upper camel case.

rules:
- id: upper-camel-case-names
  description: Use upper camel case for class names
  pattern: |
    class ${class_name}(...):
      ...
  condition: not class_name.is_upper_camel_case()
  tests:
    - match: |
        class lower:
          pass
    - match: |
        class UPPER:
          pass
    - match: |
        class snake_case:
          pass
    - match: |
        class UPPER_UNDERSCORE:
          pass
    - match: |
        class UpperCamelCase_WithUnderscore:
          pass
    - no-match: |
        class UpperCamelCase:
          pass
    - no-match: |
        class UpperCamelCase42:
          pass
    - no-match: |
        class B:
          pass
    - no-match: |
        class _PrivateUpperCamelCase123:
        pass

left.equals(right: Capture | str) -> bool

Returns true if left and right names are the same.

rules:
- id: use-standard-name-for-aliases-pandas
  description: Import `pandas` as `pd`
  pattern: import ${left*}, pandas as ${alias}, ${right*}
  condition: not alias.equals("pd")
  tests:
  - match: import pandas as pds
  - match: import pandas as np
  - match: import numpy, pandas as pds, tensorflow
  - no-match: import pandas
  - no-match: import pandas as pd
  - no-match: import numpy, pandas as pd, tensorflow
  - no-match: import modin.pandas as pds

capture.character_count() -> int

Counts the number of characters.

Can be used to limit the length of a name:

rules:
- id: limit-var-name-length
  description: Variables longer than 20 characters are not allowed
  pattern: ${var} = ${value}
  condition: var.character_count() >= 20
  tests:
  - match: the_meaning_of_life_the_universe_and_everything = 42
  - no-match: concise_name = 43

capture.starts_with(prefix: str) -> bool

Returns true if the name starts with the specified prefix.

rules:
- id: avoid-global-variables
  description: Do not define variables at the module level
  pattern: "${var} = ${value}"
  condition: |
    var.in_module_scope()
      and not var.is_upper_case()
      and not var.starts_with("_")
  tests:
  - match: max_holy_handgrenade_count = 3
  # The next example is wrapped in a string because `:` has a special meaning in yaml
  - match: "max_holy_handgrenade_count: int = 3"
  - no-match: _max_holy_handgrenade_count = 3
  - no-match: MAX_HOLY_HANDGRENADE_COUNT = 3
  - no-match: |
      def f():
          max_holy_handgrenade_count = 3

capture.ends_with(suffix: str) -> bool

Returns true if the name ends with the specified suffix.

rules:
- id: name-type-suffix
  description: Do not use the type of a variable as a suffix.
  explanation: |
    Names shouldn't needlessly include the type of the variable.
  pattern: ${name} = ${value}
  condition: |
    name.ends_with("_dict")
    or name.ends_with("_list")
    or name.ends_with("_set")
    or name.ends_with("_int")
    or name.ends_with("_float")
    or name.ends_with("_str")
  tests:
  - match: magic_int = 42
  - match: magic_int = 42.00
  - match: magic_float = 42
  - match: magic_float = 42.00
  - match: custom_notes_dict = {}

Scope Conditions

Contains conditions related to the scope a capture is in.

All condition only check if the innermost local scope (function, class or module) matches the condition.

See Python Scopes and Namespace for more details about scopes.

capture.in_module_scope() -> bool

Returns true if the capture is found in module scope.

This means it is not within a class or a function definition.

rules:
- id: avoid-global-variables
  description: Do not define variables at the module level - (found variable ${var})
  pattern: ${var} = ${value}
  condition: pattern.in_module_scope() and var.is_lower_case() and not var.starts_with("_") and var.is_identifier()
  tests:
  - match: |
      # This var is not in a class or function so is in module scope
      x = 0
  - no-match: |
      class Example:
          # This var is in a class so is not in module scope
          x = 0
  - no-match: |
      def f():
          # This var is in a function so is not in module scope
          x = 0

capture.in_class_scope(name: str = None) -> bool

Returns true if the capture is found in class scope.

This means it is within a class definition but not within a method definition. The name argument can be used to specify the name of the class.

rules:
- id: no-private-class-attributes
  description: Do not use private class attributes
  pattern: ${var} = ${value}
  condition: var.in_class_scope() and var.starts_with("_")
  tests:
  - match: |
      class Example:
          # This var is within a class, but not in a function, so is in class scope
          _x = 0
  - no-match: |
      # This var is not in a class, so is not in class scope
      _x = 0
  - no-match: |
      class Example:
          def f(self):
              # This var is within a function so is not in class scope
              # despite being within a class
              _x = 0

capture.in_function_scope(name: str = None) -> bool

Returns true if the capture is found in function scope.

This means it is within a function definition but not within an inner class definition within that function. The name argument can be used to specify the name of the function.

Here's an example with no name:

rules:
- id: no-constants-assigned-in-functions
  description: Use upper case variables only as constants outside of functions
  pattern: ${var} = ${value}
  condition: var.in_function_scope() and var.is_upper_case()
  tests:
  - match: |
      def example():
          # This var is in a function, so is in function scope
          VALUE = "value"
  - match: |
      class Example:
          def example(self):
              # This var is in a function inside a class, so is in function
              # scope
              VALUE = "value"
  - no-match: |
      # This var is not in a function, so is not in function scope
      VALUE = "value"
  - no-match: |
      def factory():
          class Example:
              # This var is within a class within a function, so is not in
              # function scope
              VALUE = "value"

Here's an example using name to check it's in the __init__ method:

rules:
- id: do-not-return-value-from-init
  description: __init__ should not return a value
  pattern: return ...
  condition: pattern.in_function_scope("__init__")
  tests:
    - match: |
        def __init__(stuff):
            self.stuff = stuff
            return

capture.in_except_clause() -> bool

Returns true if the capture is found in an exception clause.

This only searches for an enclosing exception clause in the local scope.

rules:
- id: only-bare-raise-in-except-handler
  pattern: raise
  condition: not pattern.in_except_clause()
  description: Bare `raise` statements should only be used in `except` blocks
  tests:
    - match: raise
    - no-match: raise ValueError
    - match: |
        try:
            access_database()
            raise
        except DBNotConnectedError:
            logger.info("DB is not connected")
    - no-match: |
        try:
            access_database()
        except DBNotConnectedError:
            logger.info("DB is not connected")
            raise

capture.in_finally_clause() -> bool

Returns true if the capture is found in a finally clause.

This only searches for an enclosing finally clause in the local scope.

rules:
- id: no-break-in-finally
  pattern: break
  condition: pattern.in_finally_clause()
  description: Do not use `break` statements in `finally` blocks
  tests:
    - match: |
        try:
            x()
        finally:
            break
    - no-match: |
        def f():
            while True:
                try:
                    x()
                except:
                    break
    - no-match: |
        def f():
            for i in range(5):
                x()
                break

Statement Conditions

Conditions related to statements.

capture.statement_count() -> int

Returns the number of statements in the capture.

Counts the number of statement and nested statements.

This for example would give a statement_count of 3:

if arriving():          # +1
    print("Hello")      # +1
else:                   #  0 - This is part of the `if` statement
    print("Good bye")   # +1

rules:
- id: no-long-functions
  description: No functions with more than 40 statements
  pattern: |
      def ${name}(...):
          ${statements+}
  condition: statements.statement_count() > 40

String Conditions

Conditions related to string literals.

capture.has_quote(quote: str) -> bool

Returns true if the string uses quote.

rules:
- id: do-not-use-single-quotes
  pattern: |
    "${string}"
  condition: string.has_quote("'") and not string.matches_regex("\"")
  replacement: |
    ${string.with_quote("\"")}
  description: Do not use single quotes
  tests:
    - match: a = 'reee'
      expect: a = "reee"
    - match: func(something, other, thing, next_arg='ree')
      expect: func(something, other, thing, next_arg="ree")
    - no-match: a = "bbb"
    - no-match: a = '"'

capture.with_quote(quote: str) -> Capture

Quotes the string using the quote.

rules:
- id: do-not-use-single-quotes
  pattern: |
    "${string}"
  condition: string.has_quote("'") and not string.matches_regex("\"")
  replacement: |
    ${string.with_quote("\"")}
  description: Do not use single quotes
  tests:
    - match: a = 'reee'
      expect: a = "reee"
    - match: func(something, other, thing, next_arg='ree')
      expect: func(something, other, thing, next_arg="ree")
    - no-match: a = "bbb"
    - no-match: a = '"'

Type Conditions

Contains conditions related to Capture types.

capture.has_type(*types: str) -> bool

Return true if the capture's inferred type is any of types.

Multiple type arguments can be passed to check whether any of them match.

rules:
- id: dont-use-str-function-on-str-type
  description: Unneccesary call to `str` on value with `str` type
  pattern: str(${value})
  condition: value.has_type("str")
  tests:
  - match: |
      name = "Ada"
      full_name = str(name) + "Lovelace"
  - no-match: |
      count = 6
      message = "Found " + str(count) + "nectarines"

capture.has_exception_type() -> bool

Returns true if the inferred type of the capture is an Exception.

Note that this only recognizes built in exceptions.

rules:
- id: raise-created-exception
  pattern: ${expr}
  condition: |
    expr.is_expression_statement()
        and (expr.is_exception_type() or expr.has_exception_type())
  replacement: raise ${expr}
  description: Exceptions should not be created without being raised
  tests:
  - match: Exception
  - match: ValueError()
  - no-match: raise Exception
  - no-match: raise ValueError()