diff --git a/CHANGELOG.md b/CHANGELOG.md index ef8163c..ff3b3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ We follow Semantic Versions. +## Version 0.6.0 + +### Features + +- Forbid to use `\r\n` (CRLF) end-of-line + + ## Version 0.5.0 ### Features diff --git a/dotenv_linter/checker.py b/dotenv_linter/checker.py index 7d18812..41406d3 100644 --- a/dotenv_linter/checker.py +++ b/dotenv_linter/checker.py @@ -48,7 +48,10 @@ def run(self) -> None: def _prepare_file_contents(self) -> Iterator[Tuple[str, str]]: """Returns iterator with each file contents.""" for filename in self._filenames: # TODO: move this logic from here - with open(filename, encoding='utf8') as file_object: + # From `open` docs on `newline` - If it is '', universal + # newline mode is enabled but line endings are returned + # to the caller untranslated + with open(filename, encoding='utf8', newline='') as file_object: yield filename, file_object.read() def _prepare_fst( diff --git a/dotenv_linter/violations/values.py b/dotenv_linter/violations/values.py index 3e56754..cd8bf88 100644 --- a/dotenv_linter/violations/values.py +++ b/dotenv_linter/violations/values.py @@ -8,6 +8,7 @@ .. autoclass:: SpacedValueViolation .. autoclass:: QuotedValueViolation +.. autoclass:: InvalidEOLViolation """ @@ -62,3 +63,24 @@ class QuotedValueViolation(BaseFSTViolation): code = 301 error_template = 'Found quoted value' + + +@final +class InvalidEOLViolation(BaseFSTViolation): + r""" + Restricts to use `\r\n` (CRLF) end-of-line. + + Reasoning: + Mixing different end-of-line chars can lead to different + hard-to-debug problems. + + Solution: + Use `\n` (LF) end-of-line. + Another option is to add line `text eol=lf` to `.gitattributes`. + + .. versionadded:: 0.6.0 + + """ + + code = 302 + error_template = 'Found CRLF end-of-line' diff --git a/dotenv_linter/visitors/fst/values.py b/dotenv_linter/visitors/fst/values.py index d05eae0..b2ffd81 100644 --- a/dotenv_linter/visitors/fst/values.py +++ b/dotenv_linter/visitors/fst/values.py @@ -1,12 +1,15 @@ -from typing import final +from typing import Final, final from dotenv_linter.grammar.fst import Value from dotenv_linter.violations.values import ( + InvalidEOLViolation, QuotedValueViolation, SpacedValueViolation, ) from dotenv_linter.visitors.base import BaseFSTVisitor +CRLF_EOL: Final = '\r' + @final class ValueVisitor(BaseFSTVisitor): @@ -19,10 +22,13 @@ def visit_value(self, node: Value) -> None: Raises: QuotedValueViolation SpacedValueViolation + InvalidEOLViolation """ self._check_value_quotes(node) self._check_value_spaces(node) + self._is_crlf_eol_used(node) + self.generic_visit(node) def _check_value_spaces(self, node: Value) -> None: @@ -35,3 +41,7 @@ def _check_value_quotes(self, node: Value) -> None: self._add_violation(QuotedValueViolation(node, text=node.raw_text)) elif text.startswith("'") and text.endswith("'"): self._add_violation(QuotedValueViolation(node, text=node.raw_text)) + + def _is_crlf_eol_used(self, node: Value) -> None: + if node.raw_text.endswith(CRLF_EOL): + self._add_violation(InvalidEOLViolation(node, text=node.text)) diff --git a/tests/fixtures/.env.correct b/tests/fixtures/.env.correct index 937d763..606cad9 100644 --- a/tests/fixtures/.env.correct +++ b/tests/fixtures/.env.correct @@ -4,3 +4,4 @@ # See: https://github.com/wemake-services/dotenv-linter/issues/20 TEST=1 EMPTY= +VARIABLE_WITH_TRAILING_SLASH_R=value\r diff --git a/tests/test_cli/test_lint_command.py b/tests/test_cli/test_lint_command.py index e533a91..3240963 100644 --- a/tests/test_cli/test_lint_command.py +++ b/tests/test_cli/test_lint_command.py @@ -1,4 +1,7 @@ import subprocess +from pathlib import Path + +from dotenv_linter.violations.values import InvalidEOLViolation def test_lint_correct_fixture(fixture_path): @@ -74,5 +77,26 @@ def test_lint_wrong_fixture(fixture_path, all_violations): assert process.returncode == 1 - for violation_class in all_violations: + violations_without_eol = set(all_violations) - {InvalidEOLViolation} + for violation_class in violations_without_eol: assert str(violation_class.code) in stderr + + +def test_lint_wrong_eol(tmp_path: Path) -> None: + """Checks that `lint` command works for with with CRLF end-of-line.""" + temp_file = tmp_path / '.env.temp' + temp_file.write_text('VARIABLE_WITH_CRLF_EOL=123\r\n') + + process = subprocess.Popen( + ['dotenv-linter', temp_file.absolute()], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + # It is important to set this to `False`, otherwise eol are normalized + universal_newlines=False, + encoding='utf8', + ) + _, stderr = process.communicate() + + assert process.returncode == 1 + + assert str(InvalidEOLViolation.code) in stderr