Coverage for lintro / parsers / base_issue.py: 100%

33 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Base issue class for all linting tool issues. 

2 

3This module provides a common base class for all issue types to reduce 

4duplication across the 14+ different issue dataclasses. 

5 

6The base class includes: 

7- Common fields (file, line, column, message) 

8- A to_display_row() method for unified formatting with configurable field mapping 

9- A get_severity() method for normalized severity access 

10""" 

11 

12from __future__ import annotations 

13 

14from dataclasses import dataclass, field 

15from typing import ClassVar 

16 

17from lintro.enums.severity_level import SeverityLevel, normalize_severity_level 

18 

19 

20@dataclass 

21class BaseIssue: 

22 """Base class for all linting issues with unified display support. 

23 

24 Provides common fields that are shared across all issue types. 

25 Specific issue types should inherit from this class and add their 

26 own fields as needed. 

27 

28 The to_display_row() method converts the issue to a dictionary format 

29 suitable for the unified formatter. Subclasses can customize the mapping 

30 by setting DISPLAY_FIELD_MAP class variable instead of overriding the method. 

31 

32 Attributes: 

33 DISPLAY_FIELD_MAP: Maps display keys to attribute names for custom fields. 

34 Default mappings: code->code, severity->severity, fixable->fixable. 

35 Example: {"severity": "level"} to map self.level to severity output. 

36 DEFAULT_SEVERITY: Fallback severity when the issue has no native value. 

37 Override in subclasses (e.g. INFO for pure-formatting tools). 

38 file: File path where the issue was found. 

39 line: Line number where the issue was found (1-based, 0 means unknown). 

40 column: Column number where the issue was found (1-based, 0 means unknown). 

41 message: Human-readable description of the issue. 

42 doc_url: URL to the rule's documentation page (empty when unavailable). 

43 """ 

44 

45 # Default field mapping - subclasses can override specific keys 

46 DISPLAY_FIELD_MAP: ClassVar[dict[str, str]] = { 

47 "code": "code", 

48 "severity": "severity", 

49 "fixable": "fixable", 

50 "message": "message", 

51 } 

52 

53 DEFAULT_SEVERITY: ClassVar[SeverityLevel] = SeverityLevel.WARNING 

54 

55 file: str = field(default="") 

56 line: int = field(default=0) 

57 column: int = field(default=0) 

58 message: str = field(default="") 

59 doc_url: str = field(default="", repr=False) 

60 

61 def get_severity(self) -> SeverityLevel: 

62 """Return the normalized severity for this issue. 

63 

64 Reads the native severity value via DISPLAY_FIELD_MAP (handles 

65 tools that store it as ``level``, ``issue_severity``, etc.), 

66 passes it through ``normalize_severity_level()``, and falls back 

67 to ``DEFAULT_SEVERITY`` when the value is empty/None. 

68 

69 Returns: 

70 SeverityLevel: Normalized severity enum value. 

71 """ 

72 attr_name = self.DISPLAY_FIELD_MAP.get("severity", "severity") 

73 raw = getattr(self, attr_name, None) 

74 

75 if not raw: 

76 return self.DEFAULT_SEVERITY 

77 

78 if isinstance(raw, SeverityLevel): 

79 return raw 

80 

81 try: 

82 return normalize_severity_level(str(raw)) 

83 except ValueError: 

84 return self.DEFAULT_SEVERITY 

85 

86 def to_display_row(self) -> dict[str, str]: 

87 """Convert issue to unified display format. 

88 

89 Returns a dictionary with standardized keys that the unified formatter 

90 can use to create consistent output across all tools. 

91 

92 Uses DISPLAY_FIELD_MAP to resolve attribute names, allowing subclasses 

93 to customize field mapping without overriding this method. 

94 

95 Returns: 

96 Dictionary with keys: file, line, column, code, message, severity, 

97 fixable, doc_url. 

98 """ 

99 # Get the field mapping (supports inheritance) 

100 field_map = self.DISPLAY_FIELD_MAP 

101 

102 # Resolve each mapped field 

103 code_attr = field_map.get("code", "code") 

104 fixable_attr = field_map.get("fixable", "fixable") 

105 message_attr = field_map.get("message", "message") 

106 

107 code_val = getattr(self, code_attr, None) or "" 

108 fixable_val = getattr(self, fixable_attr, False) 

109 message_val = getattr(self, message_attr, "") or "" 

110 

111 return { 

112 "file": self.file, 

113 "line": str(self.line) if self.line else "-", 

114 "column": str(self.column) if self.column else "-", 

115 "code": str(code_val) if code_val else "", 

116 "message": message_val, 

117 "severity": str(self.get_severity()), 

118 "fixable": "Yes" if fixable_val else "", 

119 "doc_url": self.doc_url, 

120 }