diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 118abcd769..5622c21107 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -2685,7 +2685,25 @@ def show_linter_violations( self, violations: t.List[RuleViolation], model: Model, is_error: bool = False ) -> None: severity = "errors" if is_error else "warnings" - violations_msg = "\n".join(f" - {violation}" for violation in violations) + + # Sort violations by line, then alphabetically the name of the violation + # Violations with no range go first + sorted_violations = sorted( + violations, + key=lambda v: ( + v.violation_range.start.line if v.violation_range else -1, + v.rule.name.lower(), + ), + ) + violations_text = [ + ( + f" - Line {v.violation_range.start.line + 1}: {v.rule.name} - {v.violation_msg}" + if v.violation_range + else f" - {v.rule.name}: {v.violation_msg}" + ) + for v in sorted_violations + ] + violations_msg = "\n".join(violations_text) msg = f"Linter {severity} for {model._path}:\n{violations_msg}" if is_error: diff --git a/sqlmesh/core/linter/definition.py b/sqlmesh/core/linter/definition.py index 9cfa4076cd..c7cee6aaa9 100644 --- a/sqlmesh/core/linter/definition.py +++ b/sqlmesh/core/linter/definition.py @@ -108,9 +108,10 @@ def check_model(self, model: Model, context: GenericContext) -> t.List[RuleViola for rule in self._underlying.values(): violation = rule(context).check_model(model) - + if isinstance(violation, RuleViolation): + violation = [violation] if violation: - violations.append(violation) + violations.extend(violation) return violations diff --git a/sqlmesh/core/linter/rule.py b/sqlmesh/core/linter/rule.py index da33df2124..6e63dd2ee6 100644 --- a/sqlmesh/core/linter/rule.py +++ b/sqlmesh/core/linter/rule.py @@ -70,7 +70,9 @@ def __init__(self, context: GenericContext): self.context = context @abc.abstractmethod - def check_model(self, model: Model) -> t.Optional[RuleViolation]: + def check_model( + self, model: Model + ) -> t.Optional[t.Union[RuleViolation, t.List[RuleViolation]]]: """The evaluation function that'll check for a violation of this rule.""" @property diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 805e4d51a0..26fc542632 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -1952,7 +1952,7 @@ def assert_cached_violations_exist(cache: OptimizedQueryCache, model: Model): ctx.plan(environment="dev", auto_apply=True, no_prompts=True) assert ( - """noselectstar: Query should not contain SELECT * on its outer most projections""" + """noselectstar - Query should not contain SELECT * on its outer most projections""" in mock_logger.call_args[0][0] )