Repository: PyCQA/flake8

Issue:
physical line plugins aren't passed every physical line when an expression contains a multiline string
### how did you install flake8?

```console
$ pip install git+https://github.com/pycqa/flake8@ca573a7ccf2d4a1c7df0b577bb6d3455c941e828
Collecting git+https://github.com/pycqa/flake8@ca573a7ccf2d4a1c7df0b577bb6d3455c941e828
  Cloning https://github.com/pycqa/flake8 (to revision ca573a7ccf2d4a1c7df0b577bb6d3455c941e828) to /tmp/pip-req-build-lh3_65yl
  Running command git clone --filter=blob:none -q https://github.com/pycqa/flake8 /tmp/pip-req-build-lh3_65yl
  Running command git rev-parse -q --verify 'sha^ca573a7ccf2d4a1c7df0b577bb6d3455c941e828'
  Running command git fetch -q https://github.com/pycqa/flake8 ca573a7ccf2d4a1c7df0b577bb6d3455c941e828
  Resolved https://github.com/pycqa/flake8 to commit ca573a7ccf2d4a1c7df0b577bb6d3455c941e828
  Preparing metadata (setup.py) ... done
Requirement already satisfied: mccabe<0.7.0,>=0.6.0 in ./venv/lib/python3.8/site-packages (from flake8==4.0.1) (0.6.1)
Requirement already satisfied: pycodestyle<2.9.0,>=2.8.0 in ./venv/lib/python3.8/site-packages (from flake8==4.0.1) (2.8.0)
Requirement already satisfied: pyflakes<2.5.0,>=2.4.0 in ./venv/lib/python3.8/site-packages (from flake8==4.0.1) (2.4.0)
Building wheels for collected packages: flake8
  Building wheel for flake8 (setup.py) ... done
  Created wheel for flake8: filename=flake8-4.0.1-py2.py3-none-any.whl size=61251 sha256=96363641001fa7562bbd594af37564c697971950775dcd9335e7d7f94cad3238
  Stored in directory: /home/asottile/.cache/pip/wheels/9b/f3/37/5b6c870430baba34a27d437055cfdb1b431514495819e6fd0b
Successfully built flake8
Installing collected packages: flake8
  Attempting uninstall: flake8
    Found existing installation: flake8 4.0.1
    Uninstalling flake8-4.0.1:
      Successfully uninstalled flake8-4.0.1
Successfully installed flake8-4.0.1
```


### unmodified output of `flake8 --bug-report`

```json
{
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.8.10",
    "system": "Linux"
  },
  "plugins": [
    {
      "plugin": "mccabe",
      "version": "0.6.1"
    },
    {
      "plugin": "pycodestyle",
      "version": "2.8.0"
    },
    {
      "plugin": "pyflakes",
      "version": "2.4.0"
    }
  ],
  "version": "4.0.1"
}
```


### describe the problem

#### what I expected to happen

I expect to get two physical lines outputted such as:

```console
$ flake8 t.py
'no_flake8_e501_error = "My line longer than 80 chars xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + """\n'
'"""\n'
```


#### sample code

(t.py)
```python
no_flake8_e501_error = "My line longer than 80 chars xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + """
"""
```

(plug.py)
```python
def plug(physical_line: str):
    print(repr(physical_line))
    return
    yield
```

(setup.cfg)
```ini
[flake8:local-plugins]
extension =
    A=plug:plug
paths = .
```

#### commands ran

```console
$ flake8 t.py
'"""\n'
'"""\n'
```


Discussion:

Patch:
diff --git a/src/flake8/checker.py b/src/flake8/checker.py
index 1161a5b..651cd34 100644
--- a/src/flake8/checker.py
+++ b/src/flake8/checker.py
@@ -581,7 +581,7 @@ class FileChecker:
             line_no = token[2][0]
             with self.processor.inside_multiline(line_number=line_no):
                 for line in self.processor.split_line(token):
-                    self.run_physical_checks(line + "\n")
+                    self.run_physical_checks(line)
 
 
 def _pool_init() -> None:
diff --git a/src/flake8/processor.py b/src/flake8/processor.py
index 40b3266..bc0ef51 100644
--- a/src/flake8/processor.py
+++ b/src/flake8/processor.py
@@ -233,8 +233,10 @@ class FileProcessor:
 
         This also auto-increments the line number for the caller.
         """
-        for line in token[1].split("\n")[:-1]:
-            yield line
+        # intentionally don't include the last line, that line will be
+        # terminated later by a future end-of-line
+        for line_no in range(token.start[0], token.end[0]):
+            yield self.lines[line_no - 1]
             self.line_number += 1
 
     def keyword_arguments_for(


Test patch:
diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py
index c3be049..03f0caf 100644
--- a/tests/integration/test_plugins.py
+++ b/tests/integration/test_plugins.py
@@ -160,3 +160,37 @@ extension =
     out, err = capsys.readouterr()
     assert out == f"{t_py}:1:1: ABC123 error\n"
     assert err == ""
+
+
+def yields_physical_line(physical_line):
+    yield 0, f"T001 {physical_line!r}"
+
+
+def test_physical_line_plugin_multiline_string(tmpdir, capsys):
+    cfg_s = f"""\
+[flake8:local-plugins]
+extension =
+    T = {yields_physical_line.__module__}:{yields_physical_line.__name__}
+"""
+
+    cfg = tmpdir.join("tox.ini")
+    cfg.write(cfg_s)
+
+    src = '''\
+x = "foo" + """
+bar
+"""
+'''
+    t_py = tmpdir.join("t.py")
+    t_py.write_binary(src.encode())
+
+    with tmpdir.as_cwd():
+        assert main(("t.py", "--config", str(cfg))) == 1
+
+    expected = '''\
+t.py:1:1: T001 'x = "foo" + """\\n'
+t.py:2:1: T001 'bar\\n'
+t.py:3:1: T001 '"""\\n'
+'''
+    out, err = capsys.readouterr()
+    assert out == expected
diff --git a/tests/unit/test_file_processor.py b/tests/unit/test_file_processor.py
index 78740fc..c5c10d5 100644
--- a/tests/unit/test_file_processor.py
+++ b/tests/unit/test_file_processor.py
@@ -256,30 +256,30 @@ def test_keyword_arguments_for_does_not_handle_attribute_errors(
         file_processor.keyword_arguments_for({"fake": True})
 
 
-@pytest.mark.parametrize(
-    "unsplit_line, expected_lines",
-    [
-        ("line", []),
-        ("line 1\n", ["line 1"]),
-        ("line 1\nline 2\n", ["line 1", "line 2"]),
-        ("line 1\n\nline 2\n", ["line 1", "", "line 2"]),
-    ],
-)
-def test_split_line(unsplit_line, expected_lines, default_options):
-    """Verify the token line splitting."""
+def test_processor_split_line(default_options):
     file_processor = processor.FileProcessor(
         "-",
         default_options,
         lines=[
-            "Line 1",
+            'x = """\n',
+            "contents\n",
+            '"""\n',
         ],
     )
-
-    token = tokenize.TokenInfo(1, unsplit_line, (0, 0), (0, 0), "")
-    actual_lines = list(file_processor.split_line(token))
-    assert expected_lines == actual_lines
-
-    assert len(actual_lines) == file_processor.line_number
+    token = tokenize.TokenInfo(
+        3,
+        '"""\ncontents\n"""',
+        (1, 4),
+        (3, 3),
+        'x = """\ncontents\n"""\n',
+    )
+    expected = [('x = """\n', 0), ("contents\n", 1)]
+    actual = [
+        (line, file_processor.line_number)
+        for line in file_processor.split_line(token)
+    ]
+    assert expected == actual
+    assert file_processor.line_number == 2
 
 
 def test_build_ast(default_options):