Skip to content

Commit

Permalink
Cleanup docs and add more tests (#3258)
Browse files Browse the repository at this point in the history
  • Loading branch information
kddejong authored May 22, 2024
1 parent 27f45ff commit 08c0bbc
Show file tree
Hide file tree
Showing 20 changed files with 829 additions and 255 deletions.
6 changes: 2 additions & 4 deletions src/cfnlint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,9 @@ def __call__(self, parser, namespace, values, option_string=None):
class CliArgs:
"""Base Args class"""

cli_args: Dict = {}

def __init__(self, cli_args):
def __init__(self, cli_args: Sequence[str] | None):
self.parser = self.create_parser()
self.cli_args = self.parser.parse_args(cli_args)
self.cli_args = self.parser.parse_args(cli_args or [])

def create_parser(self):
"""Do first round of parsing parameters to set options"""
Expand Down
14 changes: 12 additions & 2 deletions src/cfnlint/decode/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,18 @@ def _decode(
template = json_f(payload)
except cfn_json.JSONDecodeError as json_errs:
for json_err in json_errs.matches:
json_err.filename = filename
matches = json_errs.matches
matches.append(
Match(
message=json_err.message,
filename=filename,
linenumber=json_err.linenumber,
columnnumber=json_err.columnnumber,
linenumberend=json_err.linenumberend,
columnnumberend=json_err.columnnumberend,
rule=json_err.rule,
parent_id=json_err.parent_id,
)
)
except JSONDecodeError as json_err:
if hasattr(json_err, "msg"):
if json_err.msg in [
Expand Down
17 changes: 0 additions & 17 deletions src/cfnlint/decode/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import logging
from copy import deepcopy

from cfnlint.decode.exceptions import TemplateAttributeError
from cfnlint.decode.mark import Mark

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -84,21 +83,6 @@ def get(self, key, default=None):
return node_class


def create_intrinsic_node_class(cls):
"""
Create dynamic sub class
"""

class intrinsic_class(cls):
"""Node class created based on the input class"""

def is_valid(self):
raise TemplateAttributeError("intrisnic class shouldn't be directly used")

intrinsic_class.__name__ = f"{cls.__name__}_intrinsic"
return intrinsic_class


def create_dict_list_class(cls):
"""
Create dynamic list class
Expand Down Expand Up @@ -132,4 +116,3 @@ def __deepcopy__(self, memo):
str_node = create_str_node_class(str)
dict_node = create_dict_node_class(dict)
list_node = create_dict_list_class(list)
intrinsic_node = create_intrinsic_node_class(dict_node)
108 changes: 64 additions & 44 deletions src/cfnlint/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,65 @@

import hashlib
import uuid
from dataclasses import InitVar, dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from cfnlint.rules import CloudFormationLintRule, RuleMatch


@dataclass(frozen=True, eq=True)
class Match:
"""Match Classes"""

def __init__(
self,
linenumber: int,
columnnumber: int,
linenumberend: int,
columnnumberend: int,
filename: str,
rule: CloudFormationLintRule,
message=None,
rulematch_obj=None,
parent_id=None,
):
"""Init"""
self.linenumber = linenumber
"""Starting line number of the region this match spans"""
self.columnnumber = columnnumber
"""Starting line number of the region this match spans"""
self.linenumberend = linenumberend
"""Ending line number of the region this match spans"""
self.columnnumberend = columnnumberend
"""Ending column number of the region this match spans"""
self.filename = filename
"""Name of the filename associated with this match, or
None if there is no such file"""
self.rule = rule
"""The rule of this match"""
self.message = message # or rule.shortdesc
"""
Represents a match found by a CloudFormationLintRule.
Attributes:
linenumber (int): The line number where the match was found.
columnnumber (int): The column number where the match was found.
linenumberend (int): The ending line number of the match.
columnnumberend (int): The ending column number of the match.
filename (str): The name of the file where the match was found.
rule (CloudFormationLintRule): The rule that found the match.
message (str): The message associated with the match.
id (str): A unique identifier for the match.
parent_id (str): The ID of the parent match,
if this match is a child of another.
Methods:
create(message: str, filename: str, rule: CloudFormationLintRule,
linenumber: int = None, columnnumber: int = None,
linenumberend: int = None, columnnumberend: int = None,
rulematch_obj: RuleMatch = None, parent_id: str = None) -> Match:
Factory method to create a new Match instance.
"""

message: str = field(init=True)
rule: CloudFormationLintRule = field(init=True)
filename: str | None = field(init=True, default=None)
linenumber: int = field(init=True, default=1)
columnnumber: int = field(init=True, default=1)
linenumberend: int = field(init=True, default=1)
columnnumberend: int = field(init=True, default=1)
id: str = field(init=False)
parent_id: str | None = field(init=True, default=None)
rulematch_obj: InitVar[RuleMatch | None] = None

def __post_init__(self, rulematch_obj):
hex_string = hashlib.md5(f"{self}".encode("UTF-8")).hexdigest()
self.id: str = str(uuid.UUID(hex=hex_string))
super().__setattr__("id", str(uuid.UUID(hex=hex_string)))

self.parent_id = parent_id
"""The message of this match"""
if rulematch_obj:
for k, v in vars(rulematch_obj).items():
if not hasattr(self, k):
setattr(self, k, v)
super().__setattr__(k, v)

def __repr__(self):
"""Represent"""
# use the Posix path to keep things consistent across platforms
file_str = Path(self.filename).as_posix() + ":" if self.filename else ""
return f"[{self.rule}] ({self.message}) matched {file_str}{self.linenumber}"

def __eq__(self, item):
"""Override equal to compare matches"""
return (self.linenumber, self.columnnumber, self.rule.id, self.message) == (
item.linenumber,
item.columnnumber,
item.rule.id,
item.message,
)

@classmethod
def create(
cls,
Expand All @@ -83,6 +79,30 @@ def create(
rulematch_obj: RuleMatch | None = None,
parent_id: str | None = None,
) -> "Match":
"""
Factory method to create a new Match instance.
Args:
message (str): The message associated with the match.
filename (str): The name of the file where the match was found.
rule (CloudFormationLintRule): The rule that found the match.
linenumber (int, optional): The line number where the match
was found.
columnnumber (int, optional): The column number where the
match was found.
linenumberend (int, optional): The ending line number of
the match.
columnnumberend (int, optional): The ending column number of
the match.
rulematch_obj (RuleMatch, optional): The RuleMatch object that
generated this Match.
parent_id (str, optional): The ID of the parent match, if this
match is a child of another.
Returns:
Match: A new Match instance.
"""

if columnnumber is None:
columnnumber = 1
if columnnumberend is None:
Expand All @@ -92,7 +112,7 @@ def create(
if linenumberend is None:
linenumberend = linenumber

return Match(
return cls(
linenumber=linenumber,
columnnumber=columnnumber,
linenumberend=linenumberend,
Expand Down
74 changes: 62 additions & 12 deletions src/cfnlint/rules/_Rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import logging
from datetime import datetime
from typing import Any, Callable, Dict, Iterator, List, Sequence, Tuple, Union
from typing import Any, Dict, Iterator, List, Sequence, Tuple, Union

import cfnlint.helpers
import cfnlint.rules.custom
Expand Down Expand Up @@ -55,10 +55,37 @@ def _rule_is_enabled(


class RuleMatch:
"""Rules Error"""
"""
Represents a rule match found by a CloudFormationLintRule.
Attributes:
path (Sequence[str | int]): The path to the element that
triggered the rule match.
path_string (str): The string representation of the path.
message (str): The message associated with the rule match.
context (List[RuleMatch]): Additional context information
related to the rule match.
Methods:
__eq__(self, item) -> bool:
Override the equality comparison operator to compare
rule matches based on their path and message.
__hash__(self) -> int:
Override the hash function to allow rule matches to
be used as keys in a dictionary.
"""

def __init__(self, path: Sequence[str | int], message: str, **kwargs):
"""Init"""
"""
Initialize a new RuleMatch instance.
Args:
path (Sequence[str | int]): The path to the element
that triggered the rule match.
message (str): The message associated with the rule match.
**kwargs: Additional keyword arguments to be stored
as attributes on the RuleMatch instance.
"""
self.path: Sequence[str | int] = path
self.path_string: str = "/".join(map(str, path))
self.message: str = message
Expand All @@ -67,11 +94,27 @@ def __init__(self, path: Sequence[str | int], message: str, **kwargs):
setattr(self, k, v)

def __eq__(self, item):
"""Override unique"""
"""
Override the equality comparison operator to compare rule
matches based on their path and message.
Args:
item (RuleMatch): The other RuleMatch instance to compare with.
Returns:
bool: True if the path and message of the two rule matches
are equal, False otherwise.
"""
return (self.path, self.message) == (item.path, item.message)

def __hash__(self):
"""Hash for comparisons"""
"""
Override the hash function to allow rule matches to be
used as keys in a dictionary.
Returns:
int: The hash value of the RuleMatch instance.
"""
return hash((self.path, self.message))


Expand Down Expand Up @@ -128,9 +171,6 @@ def decorator(match_function):

def wrapper(self, filename: str, cfn: Template, *args, **kwargs):
"""Wrapper"""
if not getattr(self, match_type):
return

if match_type == "match_resource_properties":
if args[1] not in self.resource_property_types:
return
Expand Down Expand Up @@ -167,7 +207,6 @@ class CloudFormationLintRule:

def __init__(self) -> None:
self.resource_property_types: List[str] = []
self.resource_sub_property_types: List[str] = []
self.config: Dict[str, Any] = {} # `-X E3012:strict=false`... Show more
self.config_definition: Dict[str, Any] = {}
self._child_rules: Dict[str, "CloudFormationLintRule" | None] = {}
Expand All @@ -178,6 +217,9 @@ def __init__(self) -> None:
def __repr__(self):
return f"{self.id}: {self.shortdesc}"

def __eq__(self, other):
return self.id == other.id

@property
def child_rules(self) -> Dict[str, "CloudFormationLintRule" | None]:
return self._child_rules
Expand Down Expand Up @@ -258,9 +300,17 @@ def configure(self, configs=None, experimental=False):
elif self.config_definition[key]["itemtype"] == "integer":
self.config[key].append(int(l_value))

match: Callable[[Template], List[RuleMatch]] = None # type: ignore
# ruff: noqa: E501
match_resource_properties: Callable[[Dict, str, List[str], Template], List[RuleMatch]] = None # type: ignore
def match(self, cfn: Template) -> List[RuleMatch]:
return []

def match_resource_properties(
self,
properties: dict[str, Any],
resourcetype: str,
path: Sequence[str | int],
cfn: Template,
) -> list[RuleMatch]:
return []

@matching("match")
# pylint: disable=W0613
Expand Down
9 changes: 4 additions & 5 deletions src/cfnlint/rules/_Rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

import cfnlint.helpers
import cfnlint.rules.custom
from cfnlint.decode.exceptions import TemplateAttributeError
from cfnlint.exceptions import DuplicateRuleError
from cfnlint.rules._Rule import CloudFormationLintRule, Match
from cfnlint.rules.RuleError import RuleError
Expand Down Expand Up @@ -314,10 +313,8 @@ def is_rule_enabled(self, rule: CloudFormationLintRule):
def run_check(self, check, filename, rule_id, *args):
"""Run a check"""
try:
return check(*args)
except TemplateAttributeError as err:
LOGGER.debug(str(err))
return []
matches = list(check(*args))
return matches
except Exception as err: # pylint: disable=W0703
if self.is_rule_enabled(RuleError()):
# In debug mode, print the error include complete stack trace
Expand All @@ -334,6 +331,8 @@ def run_check(self, check, filename, rule_id, *args):
)
]

return []

def run(self, filename: Optional[str], cfn: Template, config=None):
"""Run rules"""
matches = []
Expand Down
1 change: 0 additions & 1 deletion src/cfnlint/rules/jsonschema/PropertyNames.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def _max_length(

if hasattr(rule, "maxLength") and callable(getattr(rule, "maxLength")):
validate = getattr(rule, "maxLength")
print("Call it")
yield from validate(validator, mL, instance, schema)
return

Expand Down
Loading

0 comments on commit 08c0bbc

Please sign in to comment.