Repository: roskakori/pygount (python)

Issue:
 Add generated files by name
## Story

As user I want files with certain names to be counted as "generated" instead of "unknown" so that I can treat them accordingly. 

Examples: poetry.lock, uv.lock, \*.g.dart.

## Goals

- [ ] In the command line, the user can specify the names of generated files using the `--generated-names` option, which works similar to `--names-to-skip`.
- [ ] By default, the following files are recognized as "generated":
  - [ ] `*.g.dart`. This seems to be a popular convention, but not a official recommendation, though. Regardless, see for example "[Code Generation with Dart & Flutter: The Ultimate Guide](https://codewithandrea.com/articles/dart-flutter-code-generation/)".
  - [ ] `*.lock`, for example Python's `uv.lock` or Rust's [`Cargo.lock`](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html).
  - [ ] go's [`go.sum`](https://go.dev/ref/mod#go-sum-files).
  - [ ] npm's [`package-lock.json`](https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json) and `npm-shrinkwrap.json`.
  - [ ] pnpm's `pnpm-lock.yaml`.

There is probably many more, but this should be a good starting point.

Estimate: 2.5h

Pull request description:
#190 Add generated files by name
- Close #190.

Patch:
diff --git a/docs/background.md b/docs/background.md
index 92a039d..ab83ba8 100644
--- a/docs/background.md
+++ b/docs/background.md
@@ -59,10 +59,23 @@ When a file is considered to be binary when all the following
 conditions match:
 
 1. The file does not start with a BOM for UTF-8, UTF-16 or UTF-32 (which indicates text files).
-2. The initial 8192 bytes contain at least one 0 byte.
+2. The initial 8192 bytes contain at least one 0-byte.
 
 In this case, pygount assigns it the pseudo language `__binary__` and performs no further analysis.
 
+## Generated files
+
+Generated files are recognized either by their content (`--generated`) or name (`--generated-names`). Use `--help` to see the current default patterns.
+
+In case you think the standard patterns should be extended, modify `pygount.analysis.DEFAULT_GENERATED_LINE|NAME_PATTERNS_TEXT` and [contribute a pull request](contributing.md).
+
+For source code repositories, committing generated files should generally be avoided. Instead, make the generation part of the build process. However, there are valid reasons to include generated files:
+
+1. Package managers generate "lock" files from the package specification to ensure builds use the exact same versions and hashes. For example, "pyproject.toml" and "uv.lock".
+2. Generation takes too long, for example, in Flutter projects with many nested sub-packages.
+3. Generated files cannot be bootstrapped from scratch because of interdependencies.
+4. Cloud tools require certain generated files to be present in the repository. An example would be [ReadTheDocs.org](https://readthedocs.org), which as of May 2025 in combination with [MkDocs](https://www.mkdocs.org/) needs additional dependencies to be specified in a `requirements.txt`. Many Python projects specify their dependencies in `pyproject.toml`, which can be used to generate the `requirements.txt`. However, the ReadTheDocs build does not allow easily including such a step, so the path of least resistance is to just include the generated `requirements.txt` file in the repository.
+
 ## Comparison with other tools
 
 Pygount can analyze more languages than other common tools such as sloccount or cloc because it builds on `pygments`, which provides lexers for hundreds of languages. This also makes enables supporting another language: [Write your own lexer](http://pygments.org/docs/lexerdevelopment/).
diff --git a/docs/changes.md b/docs/changes.md
index a671c73..6e5b708 100644
--- a/docs/changes.md
+++ b/docs/changes.md
@@ -3,13 +3,15 @@
 This chapter describes the changes coming with each new version of
 pygount.
 
-## Version 3.1.0, 2025-xx-xx
+## Version 3.1.0, 2025-05-27
 
+- Add command line option [`--generated-names`](usage.md#-generated-names) to specify which file names should be considered to be generated. The current patterns recognized are somewhat limited, so contributions are welcome. See the section on "[Generated files](background.md#generated-files)" for hints on how to do that (issue [#190](https://github.com/roskakori/pygount/issues/190)).
 - Change documentation from Sphinx to MkDocs in the hope to avoid it breaking regularly (issue [#191](https://github.com/roskakori/pygount/issues/191)).
 
 Development:
 
 - Replace `format()` with f-strings (contributed by Ben Allen, issue [#166](https://github.com/roskakori/pygount/issues/166)).
+- Change sdist archive to include more than just the Python source code.
 
 ## Version 3.0.0, 2025-05-23
 
diff --git a/docs/contributing.md b/docs/contributing.md
index 6c312db..b44472f 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -69,7 +69,7 @@ version = "3.x.x"
 Next, build the project and run the tests to ensure everything works:
 
 ```sh
-$ rm -rf build  # Remove any files from previous builds.
+$ rm -rf dist  # Remove any files from previous builds.
 $ uv build
 $ uv run pytest
 ```
diff --git a/docs/usage.md b/docs/usage.md
index b8a84aa..0537e68 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -120,6 +120,12 @@ If the first actual pattern is `[...]`, default patterns are included. Without i
 
 So for example, to specify that generated code can also contain the German word "generiert" in a case-insensitive way use `--generated="[regex][...](?i).*generiert"`.
 
+### `--generated-names`
+
+In addition to the source code, the file name can indicate that a source code is generated. For example, `--generated-names="*.lock,*.g.dart"`.
+
+The default already recognizes several standard generated names.
+
 ## Counting duplicates
 
 ### `--duplicates`
diff --git a/pygount/analysis.py b/pygount/analysis.py
index cfd2738..bc796f6 100644
--- a/pygount/analysis.py
+++ b/pygount/analysis.py
@@ -16,6 +16,7 @@ from collections.abc import Iterator, Sequence
 from dataclasses import dataclass
 from enum import Enum
 from io import SEEK_CUR, BufferedIOBase, IOBase, RawIOBase, TextIOBase
+from pathlib import Path
 from re import Pattern
 from typing import Optional, Union
 
@@ -27,7 +28,7 @@ import pygments.util
 import pygount.common
 import pygount.lexers
 import pygount.xmldialect
-from pygount.common import mapped_repr
+from pygount.common import mapped_repr, matching_regex
 from pygount.git_storage import GitStorage, git_remote_url_and_revision_if_any
 
 HTTP_URL_REGEX = re.compile(r"^(https?://)")
@@ -49,7 +50,11 @@ DEFAULT_FALLBACK_ENCODING = "cp1252"
 
 #: Default glob patterns for folders not to analyze.
 DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT = ", ".join(
-    [".?*", "_svn", "__pycache__"]  # Subversion hack for Windows  # Python byte code
+    [
+        ".?*",
+        "_svn",  # Subversion hack for Windows
+        "__pycache__",  # Python byte code
+    ]
 )
 
 #: Pygments token type; we need to define our own type because pygments' ``_TokenType`` is internal.
@@ -85,8 +90,9 @@ class SourceState(Enum):
 
 #: Default patterns for regular expressions to detect generated code.
 #: The '(?i)' indicates that the patterns are case-insensitive.
-DEFAULT_GENERATED_PATTERNS_TEXT = pygount.common.REGEX_PATTERN_PREFIX + ", ".join(
+DEFAULT_GENERATED_LINE_PATTERNS_TEXT = pygount.common.REGEX_PATTERN_PREFIX + ", ".join(
     [
+        r"(?i).*autogenerated",
         r"(?i).*automatically generated",
         r"(?i).*do not edit",
         r"(?i).*generated with the .+ utility",
@@ -95,6 +101,18 @@ DEFAULT_GENERATED_PATTERNS_TEXT = pygount.common.REGEX_PATTERN_PREFIX + ", ".joi
     ]
 )
 
+#: Default patterns for file names that are considered to be generated.
+DEFAULT_GENERATED_NAME_PATTERNS_TEXT = ", ".join(
+    [
+        "*.g.dart",  # See, for example, <https://codewithandrea.com/articles/dart-flutter-code-generation/>
+        "*.lock",  # For example, Cargo.lock, poetry.lock, uv.lock.
+        "npm-shrinkwrap.json",  # See <https://docs.npmjs.com/cli/v11/configuring-npm/npm-shrinkwrap-json>.
+        "go.sum",  # See <https://go.dev/ref/mod#go-sum-files>.
+        "package-lock.json",  # See <https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json>.
+        "pnpm-lock.yaml",  # See <https://pnpm.io/cli/install>.
+    ]
+)
+
 #: Default glob patterns for file names not to analyze.
 DEFAULT_NAME_PATTERNS_TO_SKIP_TEXT = ", ".join([".*", "*~"])
 
@@ -319,6 +337,8 @@ class SourceAnalysis:
         file_handle: Optional[IOBase] = None,
         merge_embedded_language: bool = False,
         tmp_dir: Optional[str] = None,
+        *,
+        generated_name_regexes: Optional[list[Pattern]] = None,
     ) -> "SourceAnalysis":
         """
         Factory method to create a :py:class:`SourceAnalysis` by analyzing
@@ -332,13 +352,15 @@ class SourceAnalysis:
           :func:`encoding_for`
         :param generated_regexes: list of regular expression that if found within the first few lines
           if a source code identify is as generated source code for which SLOC should not be counted
+        :param generated_name_regexes: list of regular expression that if the base file name matches,
+          the file is considered to be generated and the SLOC should not be counted
         :param duplicate_pool: a :class:`DuplicatePool` where information about possible duplicates is
           collected, or ``None`` if possible duplicates should be counted multiple times.
         :param file_handle: a file-like object, or ``None`` to read and open the file from
           ``source_path``. If the file is open in text mode, it must be opened with the correct
           encoding.
         :param merge_embedded_language: If pygments detects a base and embedded language, the source
-          code counts towards the base language. For example: "JavaScript+Lasso" counts as
+          code counts towards the base language. For example, "JavaScript+Lasso" counts as
           "JavaScript".
         :param tmp_dir: If a temporary directory was created, strip it from the path name. This happens
           right now only for git repositories.
@@ -348,7 +370,13 @@ class SourceAnalysis:
         result = None
         lexer = None
         source_code = None
-        if file_handle is None:
+        if generated_name_regexes is not None:
+            generated_name_regex = matching_regex(Path(source_path).name, generated_name_regexes)
+            if generated_name_regex is not None:
+                result = SourceAnalysis.from_state(
+                    source_path, group, SourceState.generated, state_info=generated_name_regex.pattern
+                )
+        if result is None and file_handle is None:
             source_size = os.path.getsize(source_path)
             if source_size == 0:
                 _log.info("%s: is empty", source_path)
@@ -386,7 +414,7 @@ class SourceAnalysis:
         actual_generated_regexes = (
             generated_regexes
             if generated_regexes is not None
-            else pygount.common.regexes_from(DEFAULT_GENERATED_PATTERNS_TEXT)
+            else pygount.common.regexes_from(DEFAULT_GENERATED_LINE_PATTERNS_TEXT)
         )
         if (result is None) and (len(actual_generated_regexes) != 0):
             number_line_and_regex = matching_number_line_and_regex(
diff --git a/pygount/command.py b/pygount/command.py
index 8d72862..974d087 100644
--- a/pygount/command.py
+++ b/pygount/command.py
@@ -49,6 +49,10 @@ _HELP_FORMAT = (
 _HELP_GENERATED = """comma separated list of regular expressions to detect
  generated code; default: %(default)s"""
 
+_HELP_GENERATED_NAMES = """comma separated list of glob patterns for file names
+ not to treat as generated. Use "..." as first entry to append patterns to the default
+ patterns; default: %(default)s"""
+
 _HELP_MERGE_EMBEDDED_LANGUAGES = """merge counts for embedded languages into
  their base language; for example, HTML+Jinja2 counts as HTML"""
 
@@ -104,7 +108,10 @@ class Command:
     def __init__(self):
         self.set_encodings(_DEFAULT_ENCODING)
         self._folders_to_skip = pygount.common.regexes_from(pygount.analysis.DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT)
-        self._generated_regexs = pygount.common.regexes_from(pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT)
+        self._generated_line_regexs = pygount.common.regexes_from(pygount.analysis.DEFAULT_GENERATED_LINE_PATTERNS_TEXT)
+        self._generated_name_regexps = pygount.common.regexes_from(
+            pygount.analysis.DEFAULT_GENERATED_NAME_PATTERNS_TEXT
+        )
         self._has_duplicates = False
         self._has_summary = False
         self._has_to_merge_embedded_languages = False
@@ -159,11 +166,20 @@ class Command:
 
     @property
     def generated_regexps(self):
-        return self._generated_regexs
+        return self._generated_line_regexs
 
     def set_generated_regexps(self, regexes_or_patterns_text, source=None):
-        self._generated_regexs = pygount.common.regexes_from(
-            regexes_or_patterns_text, pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT, source
+        self._generated_line_regexs = pygount.common.regexes_from(
+            regexes_or_patterns_text, pygount.analysis.DEFAULT_GENERATED_LINE_PATTERNS_TEXT, source
+        )
+
+    @property
+    def generated_name_regexps(self):
+        return self._generated_name_regexps
+
+    def set_generated_name_regexps(self, regexes_or_pattern_text, source=None):
+        self._generated_name_regexps = pygount.common.regexes_from(
+            regexes_or_pattern_text, pygount.analysis.DEFAULT_NAME_PATTERNS_TO_SKIP_TEXT, source
         )
 
     @property
@@ -256,9 +272,16 @@ class Command:
             "--generated",
             "-g",
             metavar="PATTERNS",
-            default=pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT,
+            default=pygount.analysis.DEFAULT_GENERATED_LINE_PATTERNS_TEXT,
             help=_HELP_GENERATED,
         )
+        parser.add_argument(
+            "--generated-names",
+            "-G",
+            metavar="PATTERNS",
+            default=pygount.analysis.DEFAULT_GENERATED_NAME_PATTERNS_TEXT,
+            help=_HELP_GENERATED_NAMES,
+        )
         parser.add_argument(
             "--merge-embedded-languages",
             "-m",
@@ -330,10 +353,11 @@ class Command:
         self.set_fallback_encoding(fallback_encoding, "option --encoding")
         self.set_folders_to_skip(args.folders_to_skip, "option --folders-to-skip")
         self.set_generated_regexps(args.generated, "option --generated")
+        self.set_generated_name_regexps(args.generated_names, "option --generated-names")
         self.set_has_duplicates(args.duplicates, "option --duplicates")
         self.set_has_to_merge_embedded_languages(args.merge_embedded_languages, "option --merge-embedded-languages")
         self.set_is_verbose(args.verbose, "option --verbose")
-        self.set_names_to_skip(args.names_to_skip, "option --folders-to-skip")
+        self.set_names_to_skip(args.names_to_skip, "option --names-to-skip")
         self.set_output(args.out, "option --out")
         self.set_output_format(args.format, "option --format")
         self.set_source_patterns(args.source_patterns, "option PATTERNS")
@@ -366,7 +390,8 @@ class Command:
                                 path_data.group,
                                 self.default_encoding,
                                 self.fallback_encoding,
-                                generated_regexes=self._generated_regexs,
+                                generated_regexes=self._generated_line_regexs,
+                                generated_name_regexes=self._generated_name_regexps,
                                 duplicate_pool=duplicate_pool,
                                 merge_embedded_language=self.has_to_merge_embedded_languages,
                                 tmp_dir=path_data.tmp_dir,
diff --git a/pygount/common.py b/pygount/common.py
index 272e400..d1578be 100644
--- a/pygount/common.py
+++ b/pygount/common.py
@@ -8,6 +8,7 @@ import fnmatch
 import functools
 import inspect
 import re
+import typing
 import warnings
 from collections.abc import Iterator, Sequence
 from re import Pattern
@@ -100,6 +101,10 @@ def regexes_from(
     return result
 
 
+def matching_regex(text: str, regexes: list[typing.Pattern]) -> Optional[typing.Pattern]:
+    return next((regex for regex in regexes if regex.match(text)), None)
+
+
 def lines(text: str) -> Iterator[str]:
     """
     Generator function to yield lines (delimited with ``'\n'``) stored in
diff --git a/pyproject.toml b/pyproject.toml
index cf9e290..09f0391 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,6 +56,7 @@ testpaths = [
 dev = [
     "coveralls>=4,<5",
     "coverage>=7,<8",
+    "hatchling>=1.27.0",
     "mkdocs>=1,<2",
     "mkdocs-material>=9,<10",
     "pytest>=8,<9",
@@ -71,7 +72,7 @@ default-groups = [
 ]
 
 [tool.hatch.build.targets.sdist]
-include = ["pygount"]
+exclude = [".idea", ".github", ".readthedocs.yaml"]
 
 [tool.hatch.build.targets.wheel]
 packages = ["pygount"]
diff --git a/uv.lock b/uv.lock
index 9a96c09..181c84a 100644
--- a/uv.lock
+++ b/uv.lock
@@ -333,6 +333,22 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" },
 ]
 
+[[package]]
+name = "hatchling"
+version = "1.27.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "packaging" },
+    { name = "pathspec" },
+    { name = "pluggy" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+    { name = "trove-classifiers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6", size = 54983, upload-time = "2024-12-15T17:08:11.894Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794, upload-time = "2024-12-15T17:08:10.364Z" },
+]
+
 [[package]]
 name = "identify"
 version = "2.6.12"
@@ -356,7 +372,7 @@ name = "importlib-metadata"
 version = "8.7.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
-    { name = "zipp" },
+    { name = "zipp", marker = "python_full_version < '3.10'" },
 ]
 sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
 wheels = [
@@ -670,6 +686,7 @@ dependencies = [
 dev = [
     { name = "coverage" },
     { name = "coveralls" },
+    { name = "hatchling" },
     { name = "mkdocs" },
     { name = "mkdocs-material" },
     { name = "pre-commit" },
@@ -691,6 +708,7 @@ requires-dist = [
 dev = [
     { name = "coverage", specifier = ">=7,<8" },
     { name = "coveralls", specifier = ">=4,<5" },
+    { name = "hatchling", specifier = ">=1.27.0" },
     { name = "mkdocs", specifier = ">=1,<2" },
     { name = "mkdocs-material", specifier = ">=9,<10" },
     { name = "pre-commit", specifier = ">=4,<5" },
@@ -931,6 +949,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
 ]
 
+[[package]]
+name = "trove-classifiers"
+version = "2025.5.9.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/04/1cd43f72c241fedcf0d9a18d0783953ee301eac9e5d9db1df0f0f089d9af/trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5", size = 16940, upload-time = "2025-05-09T12:04:48.829Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/92/ef/c6deb083748be3bcad6f471b6ae983950c161890bf5ae1b2af80cc56c530/trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce", size = 14119, upload-time = "2025-05-09T12:04:46.38Z" },
+]
+
 [[package]]
 name = "typing-extensions"
 version = "4.13.2"


Test patch:
diff --git a/tests/test_analysis.py b/tests/test_analysis.py
index 823f682..a68d122 100644
--- a/tests/test_analysis.py
+++ b/tests/test_analysis.py
@@ -286,6 +286,15 @@ class FileAnalysisTest(TempFolderTest):
         assert source_analysis.language.lower() == "html+django/jinja"
         assert source_analysis.code_count == 3
 
+    def test_can_analyze_generated_name(self):
+        test_uv_lock_path = self.create_temp_file("uv.lock", [])
+        source_analysis = analysis.SourceAnalysis.from_file(
+            test_uv_lock_path,
+            "test",
+            generated_name_regexes=pygount.common.regexes_from(pygount.analysis.DEFAULT_GENERATED_NAME_PATTERNS_TEXT),
+        )
+        assert source_analysis.state == analysis.SourceState.generated
+
     def test_can_merge_embedded_language(self):
         test_html_django_path = self.create_temp_file(
             "some.html",
@@ -407,7 +416,7 @@ class GeneratedCodeTest(TempFolderTest):
     )
 
     def test_can_detect_non_generated_code(self):
-        default_generated_regexes = common.regexes_from(analysis.DEFAULT_GENERATED_PATTERNS_TEXT)
+        default_generated_regexes = common.regexes_from(analysis.DEFAULT_GENERATED_LINE_PATTERNS_TEXT)
         with open(__file__, encoding="utf-8") as source_file:
             matching_line_number_and_regex = analysis.matching_number_line_and_regex(
                 source_file, default_generated_regexes
diff --git a/tests/test_common.py b/tests/test_common.py
index ae04338..9af53a7 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -6,7 +6,10 @@ Tests for :py:mod:`pygount.common` module.
 # All rights reserved. Distributed under the BSD License.
 import re
 
+import pytest
+
 import pygount.common
+from pygount.common import matching_regex
 
 
 def test_can_build_str():
@@ -82,6 +85,24 @@ def test_can_represent_iterable_as_list():
     assert pygount.common.as_list(range(3)) == [0, 1, 2]
 
 
+@pytest.mark.parametrize(
+    "text,patterns,expected_regex_index",
+    [
+        ("some", [], -1),
+        ("some", ["some"], 0),
+        ("some", ["other"], -1),
+        ("some", ["other", "some"], 1),
+        ("some", ["s.+"], 0),
+        ("some", [".*T.*"], -1),
+    ],
+)
+def test_can_compute_matching_regex(text: str, patterns: list[str], expected_regex_index: int):
+    regexes = [re.compile(pattern) for pattern in patterns]
+    regex = matching_regex(text, regexes)
+    regex_index = regexes.index(regex) if regex is not None else -1
+    assert regex_index == expected_regex_index
+
+
 def test_can_convert_empty_text_to_lines():
     assert list(pygount.common.lines("")) == []