Repository: wemake-services/wemake-python-styleguide

Issue:
def func(): ... causes false positives for WPS220, E704 and WPS473
### What's wrong

Source code:
https://github.com/MobileTeleSystems/horizon/blob/adc102caa8a84c281b8401b17d6d7998c073941e/horizon/client/base.py#L29
```python
class BaseResponse(Protocol):
    """Response-like object. Same interface is shared between requests.Response and httpx.Response"""

    @property
    def status_code(self) -> int:
        ...

    @property
    def content(self) -> Any:
        ...

    def json(self) -> Any:
        ...

    def raise_for_status(self) -> None:
        ...

    @property
    def headers(self) -> dict[str, str]:
        ...
```

After upgrading to black 24 it changed formatting to:
https://github.com/MobileTeleSystems/horizon/blob/a842768ff2e9f403fc41a1ea378d1a636fde6119/horizon/client/base.py#L29
```python
class BaseResponse(Protocol):
    """Response-like object. Same interface is shared between requests.Response and httpx.Response"""

    @property
    def status_code(self) -> int: ...

    @property
    def content(self) -> Any: ...

    def json(self) -> Any: ...

    def raise_for_status(self) -> None: ...

    @property
    def headers(self) -> dict[str, str]: ...
```

But wemake-python-styleguide started complaining about this:
https://github.com/MobileTeleSystems/horizon/actions/runs/7789891136/job/21242515619?pr=4#step:8:1
```
horizon/client/base.py:33:5: E704 multiple statements on one line (def)
    def status_code(self) -> int: ...
    ^
horizon/client/base.py:33:9: WPS473 Found too many empty lines in `def`: 4 > 3
    def status_code(self) -> int: ...
        ^
horizon/client/base.py:33:35: WPS220 Found too deep nesting: 34 > 20
    def status_code(self) -> int: ...
                                  ^
horizon/client/base.py:36:5: E704 multiple statements on one line (def)
    def content(self) -> Any: ...
    ^
horizon/client/base.py:36:31: WPS220 Found too deep nesting: 30 > 20
    def content(self) -> Any: ...
                              ^
horizon/client/base.py:38:5: E704 multiple statements on one line (def)
    def json(self) -> Any: ...
    ^
horizon/client/base.py:38:28: WPS220 Found too deep nesting: 27 > 20
    def json(self) -> Any: ...
                           ^
horizon/client/base.py:40:5: E704 multiple statements on one line (def)
    def raise_for_status(self) -> None: ...
    ^
horizon/client/base.py:40:41: WPS220 Found too deep nesting: 40 > 20
    def raise_for_status(self) -> None: ...
                                        ^
horizon/client/base.py:43:5: E704 multiple statements on one line (def)
    def headers(self) -> dict[str, str]: ...
    ^
horizon/client/base.py:43:42: WPS220 Found too deep nesting: 41 > 20
    def headers(self) -> dict[str, str]: ...
                                         ^
5     E704 multiple statements on one line (def)
5     WPS220 Found too deep nesting: 34 > 20
1     WPS473 Found too many empty lines in `def`: 4 > 3
```

### How it should be

Inlined `def func(): ...` should not cause E704 or WPS220

### Flake8 version and plugins

```json
{
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.11.6",
    "system": "Linux"
  },
  "plugins": [
    {
      "plugin": "Flake8-pyproject",
      "version": "1.2.3"
    },
    {
      "plugin": "darglint",
      "version": "1.8.1"
    },
    {
      "plugin": "flake8-bandit",
      "version": "4.1.1"
    },
    {
      "plugin": "flake8-broken-line",
      "version": "1.0.0"
    },
    {
      "plugin": "flake8-bugbear",
      "version": "23.12.2"
    },
    {
      "plugin": "flake8-commas",
      "version": "2.1.0"
    },
    {
      "plugin": "flake8-comprehensions",
      "version": "3.14.0"
    },
    {
      "plugin": "flake8-debugger",
      "version": "4.1.2"
    },
    {
      "plugin": "flake8-docstrings",
      "version": "1.7.0"
    },
    {
      "plugin": "flake8-eradicate",
      "version": "1.5.0"
    },
    {
      "plugin": "flake8-isort",
      "version": "6.1.1"
    },
    {
      "plugin": "flake8-quotes",
      "version": "3.3.2"
    },
    {
      "plugin": "flake8-rst-docstrings",
      "version": "0.3.0"
    },
    {
      "plugin": "flake8-string-format",
      "version": "0.3.0"
    },
    {
      "plugin": "mccabe",
      "version": "0.7.0"
    },
    {
      "plugin": "pep8-naming",
      "version": "0.13.3"
    },
    {
      "plugin": "pycodestyle",
      "version": "2.11.1"
    },
    {
      "plugin": "pyflakes",
      "version": "3.1.0"
    },
    {
      "plugin": "wemake-python-styleguide",
      "version": "0.18.0"
    }
  ],
  "version": "6.1.0"
}
```

### pip information

```
pip 23.3.1 from /home/maxim/Repo/horizon/.venv/lib/python3.11/site-packages/pip (python 3.11)
alabaster==0.7.16
alembic==1.13.1
annotated-types==0.5.0
anyio==4.2.0
apeye==1.4.1
apeye-core==1.1.4
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
asgi-correlation-id==4.3.0
astor==0.8.1
asttokens==2.4.1
async-timeout==4.0.3
asyncpg==0.29.0
attrs==23.2.0
Authlib==1.3.0
autodoc-pydantic==2.0.1
autodocsumm==0.2.11
Babel==2.14.0
bandit==1.7.6
beautifulsoup4==4.12.2
black==23.12.1
blinker==1.7.0
bonsai==1.5.2
build==1.0.3
CacheControl==0.13.1
certifi==2023.11.17
cffi==1.15.1
cfgv==3.4.0
charset-normalizer==3.3.2
cleo==2.1.0
click==8.1.7
coloredlogs==15.0.1
coverage==7.4.0
crashtest==0.4.1
cryptography==41.0.7
cssutils==2.9.0
darglint==1.8.1
deepmerge==1.1.0
devtools==0.12.2
dict2css==0.3.0
distlib==0.3.8
docutils==0.20.1
domdf-python-tools==3.7.0
dulwich==0.21.6
ecdsa==0.18.0
eradicate==2.3.0
erdantic==0.6.0
executing==2.0.1
fastapi==0.109.0
fastjsonschema==2.19.1
filelock==3.13.1
flake8==6.1.0
flake8-bandit==4.1.1
flake8-broken-line==1.0.0
flake8-bugbear==23.12.2
flake8-commas==2.1.0
flake8-comprehensions==3.14.0
flake8-debugger==4.1.2
flake8-docstrings==1.7.0
flake8-eradicate==1.5.0
flake8-isort==6.1.1
Flake8-pyproject==1.2.3
flake8-quotes==3.3.2
flake8-rst-docstrings==0.3.0
flake8-string-format==0.3.0
Flask==3.0.0
freezegun==1.2.2
gevent==23.9.1
gitdb==4.0.11
GitPython==3.1.41
graphviz==0.20.1
greenlet==3.0.3
h11==0.14.0
html5lib==1.1
httpcore==1.0.2
httpx==0.26.0
humanfriendly==10.0
identify==2.5.33
idna==3.6
imagesize==1.4.1
importlib-metadata==6.8.0
incremental==22.10.0
iniconfig==2.0.0
installer==0.7.0
isort==5.13.2
itsdangerous==2.1.2
jaraco.classes==3.3.0
jeepney==0.8.0
Jinja2==3.1.3
keyring==24.2.0
ldap3==2.9.1
Mako==1.3.0
markdown-it-py==3.0.0
MarkupSafe==2.1.4
mccabe==0.7.0
mdurl==0.1.2
mistune==2.0.5
more-itertools==10.1.0
msgpack==1.0.7
mypy==1.8.0
mypy-extensions==1.0.0
natsort==8.4.0
nodeenv==1.8.0
numpydoc==1.6.0
packaging==23.2
passlib==1.7.4
pathspec==0.12.1
pbr==6.0.0
pep8-naming==0.13.3
pexpect==4.8.0
picobox==3.0.0
pkginfo==1.9.6
platformdirs==4.1.0
pluggy==1.2.0
poetry==1.7.1
poetry-bumpversion==0.3.1
poetry-core==1.8.1
poetry-plugin-export==1.6.0
pre-commit==3.6.0
prometheus-client==0.19.0
psycopg2-binary==2.9.9
ptyprocess==0.7.0
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycodestyle==2.11.1
pycparser==2.21
pydantic==2.5.3
pydantic-settings==2.1.0
pydantic_core==2.14.6
pydocstyle==6.3.0
pyflakes==3.1.0
Pygments==2.17.2
pygraphviz==1.11
pyproject_hooks==1.0.0
pytest==7.4.4
pytest-asyncio==0.23.3
pytest-deadfixtures==2.2.1
pytest-lazy-fixture==0.6.3
pytest-mock==3.12.0
pytest-randomly==3.15.0
pytest-rerunfailures==13.0
python-dateutil==2.8.2
python-dotenv==1.0.0
python-jose==3.3.0
python-json-logger==2.0.7
python-ldap==3.4.3
python-multipart==0.0.6
PyYAML==6.0.1
rapidfuzz==3.5.1
requests==2.31.0
requests-toolbelt==1.0.0
responses==0.24.1
restructuredtext-lint==1.4.0
rich==13.7.0
rsa==4.9
ruamel.yaml==0.18.5
ruamel.yaml.clib==0.2.8
SecretStorage==3.3.3
setuptools-git-versioning==1.13.5
shellingham==1.5.4
six==1.16.0
smmap==5.0.1
sniffio==1.3.0
snowballstemmer==2.2.0
soupsieve==2.5
Sphinx==7.2.6
sphinx-autodoc-typehints==1.25.2
sphinx-copybutton==0.5.2
sphinx-favicon==1.0.1
sphinx-issues==3.0.1
sphinx-jinja2-compat==0.2.0
sphinx-plantuml==1.0.0
sphinx-prompt==1.8.0
sphinx-rtd-theme==2.0.0
sphinx-tabs==3.4.4
sphinx-toolbox==3.5.0
sphinx_design==0.5.0
sphinx_mdinclude==0.5.3
sphinxcontrib-applehelp==1.0.8
sphinxcontrib-devhelp==1.0.6
sphinxcontrib-htmlhelp==2.0.5
sphinxcontrib-httpdomain==1.8.1
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-openapi==0.8.3
sphinxcontrib-qthelp==1.0.7
sphinxcontrib-serializinghtml==1.1.10
sphinxcontrib-towncrier==0.4.0a0
SQLAlchemy==2.0.25
SQLAlchemy-Utils==0.41.1
starlette==0.35.1
starlette_exporter==0.19.0
stevedore==5.1.0
swagger-plugin-for-sphinx==3.4.0
tabulate==0.9.0
tomlkit==0.12.1
towncrier==23.11.0
trove-classifiers==2023.10.18
typer==0.9.0
types-ldap3==2.9.13.15
types-passlib==1.7.7.20240106
types-pyasn1==0.5.0.20240106
types-python-jose==3.3.4.20240106
types-PyYAML==6.0.12.12
types-requests==2.31.0.20240106
typing_extensions==4.9.0
urllib3==2.0.7
uuid6==2024.1.12
uvicorn==0.26.0
virtualenv==20.25.0
webencodings==0.5.1
wemake-python-styleguide==0.18.0
Werkzeug==3.0.1
zipp==3.17.0
zope.event==5.0
zope.interface==6.1
```

### OS information

Linux 6.7.4-1-MANJARO


Discussion:
@sobolevn I have fixed false positive violations generated by WPS473. Take a look, please
#2855 fixed only false positives of WPS473. But false positives of E704 and WPS220 are still not solved. Please reopen the issue
Indeed:

```
5:35     WPS220 Found too deep nesting: 34 > 20
  def status_code(self) -> int: ...
                                ^
```

We cannot fix `E704` though.

Resolving patch:
diff --git a/wemake_python_styleguide/visitors/ast/complexity/offset.py b/wemake_python_styleguide/visitors/ast/complexity/offset.py
--- a/wemake_python_styleguide/visitors/ast/complexity/offset.py
+++ b/wemake_python_styleguide/visitors/ast/complexity/offset.py
@@ -3,6 +3,8 @@
 
 from typing_extensions import final
 
+from wemake_python_styleguide.compat.aliases import FunctionNodes
+from wemake_python_styleguide.logic.nodes import get_parent
 from wemake_python_styleguide.violations.complexity import (
     TooDeepNestingViolation,
 )
@@ -55,6 +57,15 @@ def visit_line_expression(self, node: ast.AST) -> None:
         self.generic_visit(node)
 
     def _check_offset(self, node: ast.AST) -> None:
+        is_function_ellipsis = (
+            isinstance(get_parent(node), FunctionNodes) and
+            isinstance(node, ast.Expr) and
+            isinstance(node.value, ast.Constant) and
+            node.value.value is Ellipsis
+        )
+        if is_function_ellipsis:
+            return
+
         offset = getattr(node, 'col_offset', 0)
         baseline = self._max_offset_blocks * 4
         if offset > baseline:


Test patch:
diff --git a/tests/test_visitors/test_ast/test_complexity/test_offset_visitor.py b/tests/test_visitors/test_ast/test_complexity/test_offset_visitor.py
--- a/tests/test_visitors/test_ast/test_complexity/test_offset_visitor.py
+++ b/tests/test_visitors/test_ast/test_complexity/test_offset_visitor.py
@@ -9,7 +9,7 @@
 nested_if = """
 def container():
     if True:
-        x = 1
+        ...  # this needs to be an ellipsis for the test
 """
 
 nested_if2 = """
@@ -85,6 +85,17 @@ async def update_control():
                                                            'point': 1})
 """
 
+# Only ellipsis in the top level function definition is fine:
+
+top_level_function_ellipsis = """
+def function_with_really_long_name(): ...
+"""
+
+top_level_method_ellipsis = """
+class MyClass:
+    def function_with_really_long_name(self): ...
+"""
+
 
 @pytest.mark.parametrize('code', [
     nested_if,
@@ -108,6 +119,8 @@ async def update_control():
             reason='Pattern matching was added in 3.10',
         ),
     ),
+    top_level_function_ellipsis,
+    top_level_method_ellipsis,
 ])
 def test_nested_offset(
     assert_errors,