PK!uhdotenv_linter/__init__.py# -*- coding: utf-8 -*- PK!Mrdotenv_linter/checker.py# -*- coding: utf-8 -*- import sys from enum import Enum from typing import Iterator, NoReturn, Optional, Tuple from typing_extensions import final from dotenv_linter.grammar.fst import Module from dotenv_linter.grammar.parser import DotenvParser, ParsingError from dotenv_linter.logics.report import Report from dotenv_linter.violations.parsing import ParsingViolation from dotenv_linter.visitors.fst import assigns, comments, names, values @final class _ExitCodes(Enum): initial = -1 success = 0 linting_error = 1 system_error = 137 @final class _FSTChecker(object): """Internal checker instance to actually run all the checks.""" _visitors_pipeline = ( assigns.AssignVisitor, comments.CommentVisitor, names.NameVisitor, names.NameInModuleVisitor, values.ValueVisitor, ) def __init__(self, filenames: Tuple[str, ...]) -> None: """Creates new instance.""" self._filenames = filenames self._parser = DotenvParser() self.status = _ExitCodes.initial def run(self) -> None: """Executes all checks for each given filename.""" for filename, file_contents in self._prepare_file_contents(): fst = self._prepare_fst(filename, file_contents) self._lint_file(filename, fst) self._check_global_status() 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: yield filename, file_object.read() def _prepare_fst( self, filename: str, file_contents: str, ) -> Optional[Module]: try: return self._parser.parse(file_contents) except ParsingError: return None def _lint_file(self, filename: str, fst: Optional[Module]) -> None: report = Report(filename) # TODO: this looks not that pretty. A refactor maybe? if fst is None: report.collect_one(ParsingViolation()) else: for visitor_class in self._visitors_pipeline: visitor = visitor_class(fst) visitor.run() report.collect_from(visitor) report.report() self._check_report_status(report) def _check_report_status(self, report: Report) -> None: """Checks report status and sets the global status.""" if report.has_violations: self.status = _ExitCodes.linting_error def _check_global_status(self) -> None: """Checks the final status when all checks has been executed.""" if self.status == _ExitCodes.initial: self.status = _ExitCodes.success @final class DotenvFileChecker(object): """ Main class of the application. It does all the communication. """ # TODO: create options def __init__(self, filenames: Tuple[str, ...], options=None): """Creates new instance.""" self._fst_checker = _FSTChecker(filenames) def run(self) -> None: """Executes the linting process.""" self._fst_checker.run() if self._fst_checker.status == _ExitCodes.initial: # This means, that linting process did not change status: self._fst_checker.status = _ExitCodes.system_error self._exit() def fail(self) -> NoReturn: """Exits the program with fail status code.""" self._fst_checker.status = _ExitCodes.system_error self._exit() def _exit(self) -> NoReturn: sys.exit(int(self._fst_checker.status.value)) PK!8++$dotenv_linter/cli.py # -*- coding: utf-8 -*- # See also: # https://click.palletsprojects.com/en/7.x/setuptools/#setuptools-integration # https://click.palletsprojects.com/en/7.x/arguments/ import sys from typing import Tuple import click from click_default_group import DefaultGroup from dotenv_linter.checker import DotenvFileChecker from dotenv_linter.version import pkg_version @click.group( cls=DefaultGroup, default='lint', default_if_no_args=True, invoke_without_command=True, ) @click.option('--version', is_flag=True, default=False) def cli(version): """ Main entrypoint to the app. Runs ``lint`` command by default if nothing else is not specified. Runs ``--version`` subcommand if this option is provided. """ if version: print(pkg_version) # noqa: T001 @cli.command() @click.argument( 'files', nargs=-1, required=True, type=click.Path(exists=True, dir_okay=False), ) def lint(files: Tuple[str, ...]): """Runs linting process for the given files.""" try: checker = DotenvFileChecker(files) checker.run() except Exception as ex: print(ex, file=sys.stderr) # noqa: T001 checker.fail() PK!@(mdotenv_linter/exceptions.py# -*- coding: utf-8 -*- class ParsingError(Exception): """Used when dotenv file has incorrect grammar and cannot be parsed.""" PK!!dotenv_linter/grammar/__init__.pyPK! dotenv_linter/grammar/fst.py# -*- coding: utf-8 -*- """ Here we define nodes for our full syntax tree. What is full syntax tree? It is a code representation which always obeys this law: ``to_string(fst(code)) == code`` It is different from abstract syntax tree only in one thing: it does not loose any relevant details such as: - comments - whitespaces - line breaks See also: https://en.wikipedia.org/wiki/Abstract_syntax_tree """ from dataclasses import dataclass, field from typing import List, Optional, Type, TypeVar, Union from ply import lex from dotenv_linter.logics.text import normalize_text TNode = TypeVar('TNode', bound='Node') TAssign = TypeVar('TAssign', bound='Assign') @dataclass(frozen=True) class Node(object): """ Base class for all other nodes. Defines base fields that all other nodes have. """ __slots__ = {'lineno', 'raw_text'} lineno: int raw_text: str text: str = field(init=False) def __post_init__(self) -> None: """Used to tweak instance internals after initialization.""" object.__setattr__(self, 'text', normalize_text(self.raw_text)) @classmethod def from_token(cls: Type[TNode], token: lex.LexToken) -> TNode: """Creates instance from parser's token.""" return cls( lineno=token.lineno, raw_text=token.value, ) @dataclass(frozen=True) class Comment(Node): """ Represent a single line comment message. Is not derived from Statement, since it has no effect by design. """ @dataclass(frozen=True) class Name(Node): """Represents an inline name which is used as a key for future values.""" @dataclass(frozen=True) class Value(Node): """Represents an inline value which is used together with key.""" @dataclass(frozen=True) class Statement(Node): """Base class for all affecting statements.""" @dataclass(frozen=True) class Assign(Statement): """Represents key-value pair separated by ``=``.""" __slots__ = {'lineno', 'col_offset', 'raw_text', 'text', 'left', 'right'} left: Name right: Optional[Value] @classmethod def from_token( cls: Type[TAssign], name_token: lex.LexToken, equal_token: lex.LexToken = None, value_token: lex.LexToken = None, ) -> TAssign: """Creates instance from parser's token.""" if equal_token is None: raise ValueError('Empty EQUAL node is not allowed') if value_token is None: value_item = None else: value_item = Value.from_token(value_token) return cls( left=Name.from_token(name_token), right=value_item, lineno=name_token.lineno, raw_text=equal_token.value, ) @dataclass(frozen=True) class Module(Node): """Wrapper node that represents a single file with or without contents.""" __slots__ = {'lineno', 'raw_text', 'text', 'body'} body: List[Union[Comment, Statement]] PK!n  dotenv_linter/grammar/lexer.py# -*- coding: utf-8 -*- """ Lexer definition. See also: https://www.dabeaz.com/ply/ply.html#ply_nn3 """ from typing import ClassVar, Tuple from ply import lex from typing_extensions import final from dotenv_linter.exceptions import ParsingError @final # noqa: Z214 class DotenvLexer(object): """Custom lexer wrapper, grouping methods and attrs together.""" tokens: ClassVar[Tuple[str, ...]] = ( 'WHITESPACE', 'COMMENT', 'NAME', 'EQUAL', 'VALUE', ) states: ClassVar[Tuple[Tuple[str, str], ...]] = ( ('name', 'exclusive'), # we have found Name definition ('value', 'exclusive'), # we have found Equal definition ) re_whitespaces: ClassVar[str] = r'[ \t\v\f\u00A0]' def __init__(self, **kwargs) -> None: """Creates inner lexer.""" self._lexer = lex.lex(module=self, **kwargs) self.reset() def reset(self) -> 'DotenvLexer': """ Resets lexers inner state. Is done between two separate lexing operations. Should not be called directly, since it is a part of ``ply`` API. """ self._lexer.lineno = 1 self._lexer.begin('INITIAL') return self def input(self, text: str) -> 'DotenvLexer': # noqa: A003 """ Passes input to the lexer. It is done once per lexing operation. Should not be called directly, since it is a part of ``ply`` API. """ self.reset() self._lexer.input(text) return self def token(self) -> lex.LexToken: """ Returns the next token to work with. Should not be called directly, since it is a part of ``ply`` API. """ return self._lexer.token() @lex.TOKEN(re_whitespaces + r'*[\w-]+') def t_NAME(self, token: lex.LexToken) -> lex.LexToken: # noqa: N802 """Parsing NAME tokens.""" token.lexer.push_state('name') return token @lex.TOKEN(re_whitespaces + r'*\#.*') def t_COMMENT(self, token: lex.LexToken) -> lex.LexToken: # noqa: N802 """Parsing COMMENT tokens.""" return token @lex.TOKEN(re_whitespaces + r'*=') def t_name_EQUAL(self, token: lex.LexToken) -> lex.LexToken: # noqa: N802 """Parsing EQUAL tokens.""" token.lexer.push_state('value') return token @lex.TOKEN(r'.+') def t_value_VALUE(self, token: lex.LexToken) -> lex.LexToken: # noqa: N802 """Parsing VALUE tokens.""" token.lexer.pop_state() return token @lex.TOKEN(r'[\n\r\u2028\u2029]') def t_ANY_newline(self, token: lex.LexToken) -> None: # noqa: N802 """ Defines a rule so we can track line numbers. These tokens are skipped. """ token.lexer.lineno += len(token.value) token.lexer.begin('INITIAL') def t_ANY_error(self, token: lex.LexToken) -> None: # noqa: N802 """ Error handling rule. Raises an exception that file can not be parsed. """ raise ParsingError(token.value) PK!<.m m dotenv_linter/grammar/parser.py# -*- coding: utf-8 -*- """ Full BNF grammar for this language can be specified as: .. code:: text body : | body line line : assign | name | comment assign : NAME EQUAL | NAME EQUAL VALUE name : NAME comment : COMMENT This module generates ``parser.out`` and ``parsetab.py`` when first invoked. Do not touch these files, unless you know what you are doing. See also: https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form https://www.dabeaz.com/ply/ply.html#ply_nn11 """ from typing import List, NoReturn, Optional, Union from ply import lex, yacc from typing_extensions import final from dotenv_linter.exceptions import ParsingError from dotenv_linter.grammar.fst import Assign, Comment, Module, Name, Statement from dotenv_linter.grammar.lexer import DotenvLexer def _get_token( parsed: yacc.YaccProduction, index: int, ) -> Optional[lex.LexToken]: # TODO: lex.LexToken is in fact just `Any` """YaccProduction has a broken __getitem__ method definition.""" return getattr(parsed, 'slice')[index] @final # noqa: Z214 class DotenvParser(object): """ Custom parser wrapper, grouping methods and attrs together. Methods starting with ``p_`` uses BNF grammar to be correctly collected by ``ply.yacc`` module. Do not change them. """ tokens = DotenvLexer.tokens def __init__(self, **kwarg) -> None: """Creates inner parser instance.""" self._lexer = DotenvLexer() self._body_items: List[Union[Comment, Statement]] = [] self._parser = yacc.yacc(module=self, **kwarg) # should be last def parse(self, to_parse: str, **kwargs) -> Module: """Parses input string to FST.""" self._parser.parse(input=to_parse, lexer=self._lexer, **kwargs) return Module(lineno=0, raw_text=to_parse, body=self._body_items) def p_body(self, parsed: yacc.YaccProduction) -> None: """ body : | body line """ if len(parsed) == 3 and parsed[2] is not None: self._body_items.append(parsed[2]) parsed[0] = parsed[2] def p_line(self, parsed: yacc.YaccProduction) -> None: """ line : assign | name | comment """ parsed[0] = parsed[1] def p_assign(self, parsed: yacc.YaccProduction) -> None: """ assign : NAME EQUAL | NAME EQUAL VALUE """ value_token = _get_token(parsed, 3) if len(parsed) == 4 else None parsed[0] = Assign.from_token( name_token=_get_token(parsed, 1), equal_token=_get_token(parsed, 2), value_token=value_token, ) def p_name(self, parsed: yacc.YaccProduction) -> None: """name : NAME""" parsed[0] = Name.from_token(_get_token(parsed, 1)) def p_comment(self, parsed: yacc.YaccProduction) -> None: """comment : COMMENT""" parsed[0] = Comment.from_token(_get_token(parsed, 1)) def p_error(self, parsed: yacc.YaccProduction) -> NoReturn: """Raising exceptions on syntax errors.""" raise ParsingError(parsed) PK!,ܢj!dotenv_linter/grammar/parsetab.py # parsetab.py # This file is automatically generated. Do not edit. # pylint: disable=W,C,R _tabversion = '3.10' _lr_method = 'LALR' _lr_signature = 'COMMENT EQUAL NAME VALUE WHITESPACE\n body :\n | body line\n \n line : assign\n | name\n | comment\n \n assign : NAME EQUAL\n | NAME EQUAL VALUE\n name : NAMEcomment : COMMENT' _lr_action_items = {'NAME':([0,1,2,3,4,5,6,7,8,9,],[-1,6,-2,-3,-4,-5,-8,-9,-6,-7,]),'COMMENT':([0,1,2,3,4,5,6,7,8,9,],[-1,7,-2,-3,-4,-5,-8,-9,-6,-7,]),'$end':([0,1,2,3,4,5,6,7,8,9,],[-1,0,-2,-3,-4,-5,-8,-9,-6,-7,]),'EQUAL':([6,],[8,]),'VALUE':([8,],[9,]),} _lr_action = {} for _k, _v in _lr_action_items.items(): for _x,_y in zip(_v[0],_v[1]): if not _x in _lr_action: _lr_action[_x] = {} _lr_action[_x][_k] = _y del _lr_action_items _lr_goto_items = {'body':([0,],[1,]),'line':([1,],[2,]),'assign':([1,],[3,]),'name':([1,],[4,]),'comment':([1,],[5,]),} _lr_goto = {} for _k, _v in _lr_goto_items.items(): for _x, _y in zip(_v[0], _v[1]): if not _x in _lr_goto: _lr_goto[_x] = {} _lr_goto[_x][_k] = _y del _lr_goto_items _lr_productions = [ ("S' -> body","S'",1,None,None,None), ('body -> ','body',0,'p_body','parser.py',64), ('body -> body line','body',2,'p_body','parser.py',65), ('line -> assign','line',1,'p_line','parser.py',71), ('line -> name','line',1,'p_line','parser.py',72), ('line -> comment','line',1,'p_line','parser.py',73), ('assign -> NAME EQUAL','assign',2,'p_assign','parser.py',79), ('assign -> NAME EQUAL VALUE','assign',3,'p_assign','parser.py',80), ('name -> NAME','name',1,'p_name','parser.py',85), ('comment -> COMMENT','comment',1,'p_comment','parser.py',89), ] PK!uh dotenv_linter/logics/__init__.py# -*- coding: utf-8 -*- PK!.dotenv_linter/logics/report.py# -*- coding: utf-8 -*- import sys from itertools import chain from typing import Iterable, List from typing_extensions import final from dotenv_linter.violations.base import BaseViolation from dotenv_linter.visitors.base import BaseVisitor class Report(object): """ Reports are used to show multiple violations to the user. Reports format and sort violations the way it they want it. """ def __init__(self, filename: str) -> None: """Creates new report instance.""" self._filename = filename self._collected_from: List[Iterable[BaseViolation]] = [] self.has_violations = False @final def collect_from(self, visitor: BaseVisitor) -> None: """Collects violations from different visitors.""" self._collected_from.append(visitor.violations) @final def collect_one(self, violation: BaseViolation) -> None: """Collects a single violation.""" self._collected_from.append((violation,)) def report(self) -> None: """Reports violations from all visitors.""" sorted_violations = sorted( chain.from_iterable( violations for violations in self._collected_from ), key=lambda violation: violation.location(), ) for violation in sorted_violations: print( # noqa: T001 '{0}:{1}'.format(self._filename, violation.as_line()), file=sys.stderr, ) self.has_violations = bool(sorted_violations) PK!4--dotenv_linter/logics/text.py# -*- coding: utf-8 -*- def normalize_text(text: str) -> str: """ Removes trailing and leading spaces and quotes. >>> normalize_text('abc') 'abc' >>> normalize_text(' abc ') 'abc' >>> normalize_text(' "abc" ') 'abc' """ return text.strip().strip('\'"') PK!{TCCdotenv_linter/types.py# -*- coding: utf-8 -*- """ This module contains custom ``mypy`` types that we commonly use. Policy ------ If any of the following statements is true, move the type to this file: - if type is used in multiple files - if type is complex enough it has to be documented - if type is very important for the public API """ PK! dotenv_linter/version.py# -*- coding: utf-8 -*- import pkg_resources def _get_version(dist_name: str) -> str: # pragma: no cover """Fetches distribution name. Contains a fix for Sphinx.""" try: return pkg_resources.get_distribution(dist_name).version except pkg_resources.DistributionNotFound: return '' # readthedocs can not install `poetry` projects pkg_name = 'dotenv-linter' #: We store the version number inside the `pyproject.toml`: pkg_version: str = _get_version(pkg_name) PK!uh$dotenv_linter/violations/__init__.py# -*- coding: utf-8 -*- PK!F#dotenv_linter/violations/assigns.py# -*- coding: utf-8 -*- """ Rules that define how assigns should be made. .. currentmodule:: dotenv_linter.violations.assigns .. autoclass:: SpacedAssignViolation """ from typing_extensions import final from dotenv_linter.violations.base import BaseFSTViolation @final class SpacedAssignViolation(BaseFSTViolation): """ Restricts to write ``=`` signs with extra spaces. Reasoning: Valid ``shell`` syntax requires to write assigns without any spaces. Solution: Remove any spaces between the ``=`` char. Example:: # Correct: KEY=1 OTHER= # Wrong: KEY = 1 OTHER = .. versionadded:: 0.1.0 """ code = 200 error_template = 'Found spaced assign' PK! k dotenv_linter/violations/base.py# -*- coding: utf-8 -*- from typing_extensions import final from dotenv_linter.grammar.fst import Node class BaseViolation(object): """Base class for all violations.""" code: int error_template: str def __init__(self, node, text: str) -> None: """Creates instance of any violation.""" self._node = node self._text = text @final def as_line(self) -> str: """Coverts violation to a single line information.""" return '{0} {1} {2}'.format( self.location(), self._formated_code(), self.error_template.format(self._text), ) def location(self) -> int: """Returns in-file location of a violation.""" raise NotImplementedError('Should be redefined in a subclass') @final def _formated_code(self) -> str: return str(self.code).zfill(3) class BaseFSTViolation(BaseViolation): """Base class for all ``fst`` violations.""" _node: Node @final def location(self) -> int: """Returns in-file location of a violation.""" return self._node.lineno class BaseFileViolation(BaseViolation): """Base class for all violations that operate on whole files.""" def __init__(self, node=None, text=None) -> None: """Creates instance of file-based violation.""" self._node = node self._text = text @final def location(self) -> int: """Returns in-file location of a violation.""" return 0 PK!HT$dotenv_linter/violations/comments.py# -*- coding: utf-8 -*- """ Rules that define how comments should be written. .. currentmodule:: dotenv_linter.violations.comments .. autoclass:: SpacedCommentViolation """ from typing_extensions import final from dotenv_linter.violations.base import BaseFSTViolation @final class SpacedCommentViolation(BaseFSTViolation): """ Restricts to write comment with leading or trailing spaces. Reasoning: These spaces are meaningless and will be removed. So, why would you want to have them? Solution: Remove leading or trailing spaces from the comment body. .. versionadded:: 0.1.0 """ code = 400 error_template = 'Found spaced comment: {0}' PK!d $L L !dotenv_linter/violations/names.py# -*- coding: utf-8 -*- """ Rules that define how names should be defined. .. currentmodule:: dotenv_linter.violations.names .. autoclass:: SpacedNameViolation .. autoclass:: IncorrectNameViolation .. autoclass:: DuplicateNameViolation .. autoclass:: RawNameViolation """ from typing_extensions import final from dotenv_linter.violations.base import BaseFSTViolation @final class SpacedNameViolation(BaseFSTViolation): """ Restricts to use duplicate names variables. Reasoning: This spaces will be removed by the parsing mechanism, but they might cause some confusion to users. Solution: Remove leading spaces. Example:: # Correct: SOME_KEY=1 # Wrong: SOME_KEY=1 .. versionadded:: 0.1.0 """ code = 100 error_template = 'Found leading spaces: {0}' @final class IncorrectNameViolation(BaseFSTViolation): """ Restricts to use restricted symbols to define names. Reasoning: By convention we can only use letters, numbers, and underscores to define dotenv variables. Moreover, variables can not start with numbers. Solution: Refactor your file to contain only allowed characters. Example:: # Correct: SOME_KEY=1 # Wrong: SOME-KEY=1 .. versionadded:: 0.1.0 """ code = 101 error_template = 'Found incorrect name: {0}' @final class DuplicateNameViolation(BaseFSTViolation): """ Restricts to use duplicate names variables. Reasoning: There is no need to crate duplicate variables inside your dotenv file. Since it will be implicitly overridden by the parsing mechanism. Solution: Remove one of the duplicate variables. Example:: # Correct: SOME_KEY=1 OTHER_KEY=2 # Wrong: SOME_KEY=1 SOME_KEY=2 .. versionadded:: 0.1.0 """ code = 102 error_template = 'Found duplicate name: {0}' @final class RawNameViolation(BaseFSTViolation): """ Restricts to use raw names without equal sign or value. Reasoning: It does not make any sense to just state some names. It might also break some ``.env`` parsers. Solution: Append equal sign to it. So, this would became a declaration of an empty variable. You can also add a value if it makes sense. Example:: # Correct: KEY=1 OTHER= # Wrong: KEY .. versionadded:: 0.1.0 """ code = 103 error_template = 'Found raw name without assign: {0}' PK!Vm#dotenv_linter/violations/parsing.py# -*- coding: utf-8 -*- """ Different error that might happen during file parsing phase. .. currentmodule:: dotenv_linter.violations.parsing .. autoclass:: ParsingViolation """ from typing_extensions import final from dotenv_linter.violations.base import BaseFileViolation @final class ParsingViolation(BaseFileViolation): """ Indicates that given file can not be correctly parsed. This may include: 1. Incorrect OS behavior 2. Incorrect syntax inside this file 3. Errors in our grammar definition 4. Our internal errors .. versionadded:: 0.1.0 """ code = 1 error_template = 'Unable to correctly parse dotenv file' PK!.i"dotenv_linter/violations/values.py# -*- coding: utf-8 -*- """ Rules about writing correct dotenv values. By convention we do not print values to the output. Since they might contain private values. .. currentmodule:: dotenv_linter.violations.values .. autoclass:: SpacedValueViolation .. autoclass:: QuotedValueViolation """ from typing_extensions import final from dotenv_linter.violations.base import BaseFSTViolation @final class SpacedValueViolation(BaseFSTViolation): """ Restricts to write values with trailing spaces. Reasoning: These spaces are not guaranteed to be preserved. So, it is better not to rely on them. Solution: Remove trailing spaces from the value. .. versionadded:: 0.1.0 """ code = 300 error_template = 'Found spaced value' @final class QuotedValueViolation(BaseFSTViolation): """ Restricts to quoted values. Reasoning: Dotenv parser usually strips quotes away, so it is hard to say whether these quotes will stay on a final value, or not. Solution: Remove any quotes from the value. Example:: # Correct: KEY=1 # Wrong: KEY="1" .. versionadded:: 0.1.0 """ code = 301 error_template = 'Found quoted value' PK!uh"dotenv_linter/visitors/__init__.py# -*- coding: utf-8 -*- PK!hK]1 1 dotenv_linter/visitors/base.py# -*- coding: utf-8 -*- from dataclasses import fields from typing import Any, Iterable, Iterator, List, Tuple, Union from typing_extensions import final from dotenv_linter.grammar.fst import Module, Node from dotenv_linter.violations.base import BaseViolation #: Defines field internals of a dataclass, could be `Any`, that's why ignored FieldInfo = Tuple[str, Union[List[Any], Any]] # type: ignore def iter_fields(node: Node) -> Iterator[FieldInfo]: """Iterates over all fields inside a ``fst`` node.""" for field in fields(node): yield field.name, getattr(node, field.name) class BaseVisitor(object): """Base visitor class for all possible cases.""" def __init__(self, fst: Module) -> None: """Creates default visitor instance.""" self._fst = fst self._violations: List[BaseViolation] = [] @property def violations(self) -> Iterable[BaseViolation]: """Utility getter not to expose violations directly.""" return self._violations def _add_violation(self, violation: BaseViolation) -> None: """Adds new violations to the visitor.""" self._violations.append(violation) def _post_visit(self) -> None: """ Method to be executed after all nodes have been visited. By default does nothing. """ class BaseFSTVisitor(BaseVisitor): """ Allows to check ``fst`` trees. Attributes: contents: ``str`` of the given file. """ def visit(self, node: Node) -> None: """ Visit a ``fst`` node. This code is copy-pasted from ``ast`` module, so it should stay as it is now. No need to refactor it. """ method = 'visit_' + node.__class__.__qualname__.lower() visitor = getattr(self, method, self.generic_visit) return visitor(node) def generic_visit(self, node: Node) -> None: """ Called if no explicit visitor function exists for a node. This code is copy-pasted from ``ast`` module, so it should stay as it is now. No need to refactor it. """ for _field, node_value in iter_fields(node): if isinstance(node_value, list): for sub_node in node_value: if isinstance(sub_node, Node): self.visit(sub_node) # noqa: Z220 elif isinstance(node_value, Node): self.visit(node_value) @final def run(self) -> None: """Visits all token types that have a handler method.""" self.visit(self._fst) self._post_visit() PK!uh&dotenv_linter/visitors/fst/__init__.py# -*- coding: utf-8 -*- PK!"-n%dotenv_linter/visitors/fst/assigns.py# -*- coding: utf-8 -*- from typing_extensions import final from dotenv_linter.grammar.fst import Assign from dotenv_linter.violations.assigns import SpacedAssignViolation from dotenv_linter.visitors.base import BaseFSTVisitor @final class AssignVisitor(BaseFSTVisitor): """Finds wrong assigns.""" def visit_assign(self, node: Assign) -> None: """ Visits assign nodes to find errors. Raises: SpacedAssignViolation """ self._check_assign_char(node) self.generic_visit(node) def _check_assign_char(self, node: Assign) -> None: if node.raw_text.startswith(' '): self._add_violation( SpacedAssignViolation(node, text=node.text), ) PK!DQ""&dotenv_linter/visitors/fst/comments.py# -*- coding: utf-8 -*- from typing_extensions import final from dotenv_linter.grammar.fst import Comment from dotenv_linter.violations.comments import SpacedCommentViolation from dotenv_linter.visitors.base import BaseFSTVisitor @final class CommentVisitor(BaseFSTVisitor): """Finds wrong comment.""" def visit_comment(self, node: Comment) -> None: """ Checks how comments are defined. Raises: SpacedCommentViolation """ self._check_comment_spaces(node) self.generic_visit(node) def _check_comment_spaces(self, node: Comment) -> None: if node.raw_text.startswith(' ') or node.raw_text.endswith(' '): self._add_violation( SpacedCommentViolation(node, text=node.raw_text), ) PK!xBY Y #dotenv_linter/visitors/fst/names.py# -*- coding: utf-8 -*- import re from typing import ClassVar, List from typing.re import Pattern from typing_extensions import final from dotenv_linter.grammar.fst import Assign, Module, Name from dotenv_linter.violations.names import ( DuplicateNameViolation, IncorrectNameViolation, RawNameViolation, SpacedNameViolation, ) from dotenv_linter.visitors.base import BaseFSTVisitor @final class NameInModuleVisitor(BaseFSTVisitor): """Finds wrong names in dotenv modules.""" def __init__(self, *args, **kwargs) -> None: """Creates a list of all names in a file.""" super().__init__(*args, **kwargs) self._names: List[Name] = [] def visit_module(self, node: Module) -> None: """ Visits module to find raw names. Raises: RawNameViolation DuplicateNameViolation """ self._check_raw_name(node) self._save_names(node) self.generic_visit(node) def _post_visit(self) -> None: text_names = [name_node.text for name_node in self._names] for name_node in self._names: if text_names.count(name_node.text) > 1: self._add_violation( DuplicateNameViolation(name_node, text=name_node.text), ) def _check_raw_name(self, node: Module) -> None: for sub_node in node.body: if isinstance(sub_node, Name): self._add_violation( RawNameViolation(sub_node, text=sub_node.text), ) def _save_names(self, node: Module) -> None: for sub_node in node.body: if isinstance(sub_node, Name): self._names.append(sub_node) elif isinstance(sub_node, Assign): self._names.append(sub_node.left) @final class NameVisitor(BaseFSTVisitor): """Finds wrong names.""" _correct_name: ClassVar[Pattern] = re.compile(r'[A-Z_]+[A-Z0-9_]*') def visit_name(self, node: Name) -> None: """ Checks how names are defined. Raises: SpacedNameViolation IncorrectNameViolation """ self._check_name_correctness(node) self._check_name_spaces(node) self.generic_visit(node) def _check_name_correctness(self, node: Name) -> None: if not self._correct_name.fullmatch(node.text): self._add_violation(IncorrectNameViolation(node, text=node.text)) def _check_name_spaces(self, node: Name) -> None: if node.raw_text.startswith(' '): self._add_violation(SpacedNameViolation(node, text=node.text)) PK!_$dotenv_linter/visitors/fst/values.py# -*- coding: utf-8 -*- from typing_extensions import final from dotenv_linter.grammar.fst import Value from dotenv_linter.violations.values import ( QuotedValueViolation, SpacedValueViolation, ) from dotenv_linter.visitors.base import BaseFSTVisitor @final class ValueVisitor(BaseFSTVisitor): """Finds wrong values.""" def visit_value(self, node: Value) -> None: """ Visits value nodes to find errors. Raises: QuotedValueViolation SpacedValueViolation """ self._check_value_quotes(node) self._check_value_spaces(node) self.generic_visit(node) def _check_value_spaces(self, node: Value) -> None: if node.raw_text.endswith(' ') or node.raw_text.startswith(' '): self._add_violation(SpacedValueViolation(node, text=node.raw_text)) def _check_value_quotes(self, node: Value) -> None: text = node.raw_text.strip() if text.startswith('"') and text.endswith('"'): self._add_violation(QuotedValueViolation(node, text=node.raw_text)) elif text.startswith("'") and text.endswith("'"): self._add_violation(QuotedValueViolation(node, text=node.raw_text)) PK!HQ/7.dotenv_linter-0.1.2.dist-info/entry_points.txtN+I/N.,()J/I++I-! #֨I2G͢ YҁD \qy Zyv>UՋZ/yA$\NFGxꚟY=6ߢzݜͽ/jw/ /=9="`]p2< NRK:#d8YvnQ`zp6]w-rg/y-^y,(ujoz7;|p^K84_4* Dzm۲F<F*{d"$raeK .&\YhA$+R:!HL]sY*&wȢt$4bDȸa"s-s{c"N1BiTFI/˺BV}H#ȷZfaԶ, ^4+Y ʻGe!l 9q@SMH9&(Vs.σ߸Np2S̸`rm 8YjEFuٍpX裙F4O!nB4Eg)jcA:1)J1@T`>4h[#7;hR c٭ ύʐF3xKOW^ܑwն̧n3{l[*C]rU,-U*${FLoTP;錋Vd$ǰɄ1>TXc $PK!H8 J5 $dotenv_linter-0.1.2.dist-info/RECORDɒVнc\xQQÓČCjo8dܼcQ1"1u21AԚG&vw %DcyO^WB]>xipW7 W`5&l ~O6'q$^TK򙖤vлG{qx5ͺdrè҇NZrR/b{Idfp->AN++ QJ[MNP`_0>p( ?->Sf+u9pkv@ J46vx _2~BwkSاT]VTZeGZ8E TK茶uF=q$~Nb\:l}c;0{f! 0@pa^-`:YyPXjރ=֨:%)]u ݀ +n:0/) BƑ@%}iَ[=w":ڡoUigٟxM b{(%G0g~UOT)צCViW8{CfMн*v7&] !Ol;wDOA6na@,AeK֊qFi! E}M'=Nՠ>$;Jb[w(kWEj``u}Ӷf|d<O`bl8ȩJ,ǰb&?tbj]˃~eR)4a Tv94'@_/6Cn$ʚbV!J8 .'!4'zԜTӑRu{8?Z3&v)EQNЯ6motӭNl%l&CՃ4}ž)eP$-HG<¢Nu2Oz.OOzm^ :l'Y<8m C(tѼ ݀Olh,.oua:È0y^_f NbmB`|9G---K,( F|aDaoQ3MC%I:R:Ny[ZN 3SV#%QA^]IaB&J[n\&b*R8ݹs4_t_$n)Dn{m~ۛ"mHq0I%T+^ Oc(k?$A~PK!uhdotenv_linter/__init__.pyPK!MrOdotenv_linter/checker.pyPK!8++$dotenv_linter/cli.pyPK!@(mdotenv_linter/exceptions.pyPK!!dotenv_linter/grammar/__init__.pyPK! dotenv_linter/grammar/fst.pyPK!n   dotenv_linter/grammar/lexer.pyPK!<.m m  -dotenv_linter/grammar/parser.pyPK!,ܢj!9dotenv_linter/grammar/parsetab.pyPK!uh @dotenv_linter/logics/__init__.pyPK!.;Adotenv_linter/logics/report.pyPK!4--yGdotenv_linter/logics/text.pyPK!{TCCHdotenv_linter/types.pyPK! WJdotenv_linter/version.pyPK!uh$zLdotenv_linter/violations/__init__.pyPK!F#Ldotenv_linter/violations/assigns.pyPK! k Pdotenv_linter/violations/base.pyPK!HT$)Vdotenv_linter/violations/comments.pyPK!d $L L !,Ydotenv_linter/violations/names.pyPK!Vm#cdotenv_linter/violations/parsing.pyPK!.i"fdotenv_linter/violations/values.pyPK!uh"kdotenv_linter/visitors/__init__.pyPK!hK]1 1 #ldotenv_linter/visitors/base.pyPK!uh&vdotenv_linter/visitors/fst/__init__.pyPK!"-n%vdotenv_linter/visitors/fst/assigns.pyPK!DQ""&"zdotenv_linter/visitors/fst/comments.pyPK!xBY Y #}dotenv_linter/visitors/fst/names.pyPK!_$"dotenv_linter/visitors/fst/values.pyPK!HQ/7.-dotenv_linter-0.1.2.dist-info/entry_points.txtPK!f00%dotenv_linter-0.1.2.dist-info/LICENSEPK!HnHTU#dotenv_linter-0.1.2.dist-info/WHEELPK!Hs`q2 &dotenv_linter-0.1.2.dist-info/METADATAPK!H8 J5 $&dotenv_linter-0.1.2.dist-info/RECORDPK!!