Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for disabling checks via comments #1315

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ Additionally, for compatibility reasons, the legacy configurations `~/.proselint
}
```

You can also disable checks inside a file using special comments. Use
`proselint: disable` to disable *all* checks or `proselint: disable=<check>` to
disable only specific checks.

```tex
Here, all checks are used.
% proselint: disable=nonwords.misc,weasel_words.very
Here, the \texttt{nonwords.misc} and \texttt{weasel\_words.very}
checks are disabled.
% proselint: enable=nonwords.misc
At this point, the \texttt{nonwords.misc} check has been reenabled.
```

| ID | Description |
| ----- | --------------- |
| `airlinese.misc` | Avoiding jargon of the airline industry |
Expand Down
60 changes: 54 additions & 6 deletions proselint/tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""General-purpose tools shared across linting checks."""

import copy
import collections
import dbm
import functools
import hashlib
Expand Down Expand Up @@ -140,14 +141,14 @@ def wrapped(*args, **kwargs):
def get_checks(options):
"""Extract the checks."""
sys.path.append(proselint_path)
checks = []
checks = {}
check_names = [key for (key, val) in options["checks"].items() if val]

for check_name in check_names:
module = importlib.import_module("checks." + check_name)
for d in dir(module):
if re.match("check", d):
checks.append(getattr(module, d))
checks[check_name] = getattr(module, d)

return checks

Expand Down Expand Up @@ -230,6 +231,48 @@ def line_and_column(text, position):
return (line_no, position - position_counter)


def find_ignored_checks(available_checks, text):
ignored_checks = collections.defaultdict(dict)
for match in re.finditer(r"^[!-/:-@[-`{-~\s]*proselint: (?P<action>disable|enable)(?P<checks>(?:=(?:[\w\.,]*)))?", text, flags=re.MULTILINE):
Copy link
Member

@Nytelife26 Nytelife26 Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the computational cost of this. RegExes are expensive and moderately slow, so running one on every line just to determine whether or not we can disable some checks feels inefficient.

I am not quite sure what the solution to this would be yet, but in the meantime any ideas are welcome. They may not be quite as simple or elegant, but proselint's performance is lacking as it stands, and implementing this would be a major blow in the wrong direction.

Update: Upon further thought, the changes to how proselint works at its core are not going to be here for some time. I would be willing to accept this as a temporary solution.

if match.group("checks") == "=":
# The equal sign indicates that this only applies to some, but
# checks were specified. We just ignore this.
# TODO: Should this be considered an error?
continue
checks = [c for c in (match.group("checks") or "").lstrip("=").split(",") if c]
if not checks:
# This applies to all checks.
checks = available_checks

for check in checks:
start, end = match.span()
if match.group("action") == "enable":
try:
closest_start = max(ignored_checks[check].keys())
except ValueError:
# The check wasn't disabled previously, so this is a no-op
# FIXME: Print an error here?
pass
else:
ignored_checks[check][closest_start] = end
else:
ignored_checks[check][start] = None
return ignored_checks


def is_check_ignored(ignored_checks, check, start):
try:
closest_start = max(s for s in ignored_checks[check].keys() if s <= start)
except ValueError:
# The check wasn't disabled at all.
return False

closest_end = ignored_checks[check][closest_start]
if closest_end is None or closest_end >= start:
return True
return False


def lint(input_file, debug=False, config=config.default):
"""Run the linter on the input file."""
if isinstance(input_file, str):
Expand All @@ -240,18 +283,23 @@ def lint(input_file, debug=False, config=config.default):
# Get the checks.
checks = get_checks(config)

ignored_checks = find_ignored_checks(checks.keys(), text)

# Apply all the checks.
errors = []
for check in checks:
for check in checks.values():

result = check(text)

for error in result:
(start, end, check, message, replacements) = error
if is_check_ignored(ignored_checks, check, start):
continue
if is_quoted(start, text):
continue
(line, column) = line_and_column(text, start)
if not is_quoted(start, text):
errors += [(check, message, line, column, start, end,
end - start, "warning", replacements)]
errors += [(check, message, line, column, start, end,
end - start, "warning", replacements)]

if len(errors) > config["max_errors"]:
break
Expand Down
51 changes: 51 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,41 @@ def setUp(self):
self.text = """But this is a very bad sentence.
This is also a no-good sentence.

"""
self.text_with_checks_disabled_forever_tex = """
But this is a bad sentence.
% proselint: disable
There is doubtlessly an error in this one.
This sentence is also very bad.
Proselint should also check this sentence unrelentlessly.

"""
self.text_with_checks_disabled_and_reenabled_tex = """
But this is a bad sentence.
% proselint: disable
There is doubtlessly an error in this one.
This sentence is also very bad.
% proselint: enable
Proselint should also check this sentence unrelentlessly.

"""
self.text_with_checks_disabled_and_reenabled_html = """
But this is a bad sentence.
<!-- proselint: disable -->
There is doubtlessly an error in this one.
This sentence is also very bad.
<!-- proselint: enable -->
Proselint should also check this sentence unrelentlessly.

"""
self.text_with_specific_check_disabled_tex = """
But this is a bad sentence.
% proselint: disable=nonwords.misc
There is doubtlessly an error in this one.
This sentence is also very bad.
% proselint: enable=nonwords.misc
Proselint should also check this sentence unrelentlessly.

"""
self.text_with_no_newline = """A very bad sentence."""

Expand All @@ -37,3 +72,19 @@ def test_errors_sorted(self):
def test_on_no_newlines(self):
"""Test that lint works on text without a terminal newline."""
assert len(lint(self.text_with_no_newline)) == 1

def test_checks_disabled_forever_tex(self):
"""Test that disabling all checks works on a (La)TeX document."""
assert len(lint(self.text_with_checks_disabled_forever_tex)) == 1

def test_checks_disabled_and_reenabled_tex(self):
"""Test that disabling and reenabling all checks works on a (La)TeX document."""
assert len(lint(self.text_with_checks_disabled_and_reenabled_tex)) == 2

def test_checks_disabled_and_reenabled_html(self):
"""Test that disabling and reenabling all checks works on an HTML document."""
assert len(lint(self.text_with_checks_disabled_and_reenabled_html)) == 2

def test_specific_check_disabled_tex(self):
"""Test that disabling a specific check works on a (La)TeX document."""
assert len(lint(self.text_with_specific_check_disabled_tex)) == 3