Skip to content

Commit 86ce8af

Browse files
committed
Support linting macros
1 parent 6a26407 commit 86ce8af

File tree

17 files changed

+404
-4
lines changed

17 files changed

+404
-4
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
- name: Deploy docs
3131
run: |
3232
uv run dbt-score list -f markdown -n dbt_score.rules.generic --title Generic > docs/rules/generic.md
33+
uv run dbt-score list -f markdown -n dbt_score.rules.macros --title Macros > docs/rules/macros.md
3334
uv run mkdocs gh-deploy --force
3435
- uses: ncipollo/release-action@v1
3536
with:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to
1111
## [0.14.1] - 2025-10-09
1212

1313
- Migrate to `uv` project manager.
14+
- Support linting dbt macros as a new evaluable entity type.
1415

1516
## [0.14.0] - 2025-08-08
1617

docs/create_rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function is its description. Therefore, it is important to use a
3232
self-explanatory name for the function and document it well.
3333

3434
The type annotation for the rule's argument dictates whether the rule should be
35-
applied to dbt models, sources, snapshots, seeds, or exposures.
35+
applied to dbt models, sources, snapshots, seeds, exposures, or macros.
3636

3737
Here is the same example rule, applied to sources:
3838

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ encourage) good practices. The dbt entities that `dbt-score` is able to lint
1616
- Snapshots
1717
- Exposures
1818
- Seeds
19+
- Macros
1920

2021
## Example
2122

docs/rules/macros.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(content generated in CI)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ nav:
2828
- Programmatic invocations: programmatic_invocations.md
2929
- Rules:
3030
- rules/generic.md
31+
- rules/macros.md
3132
- rules/filters.md
3233
- Reference:
3334
- reference/cli.md

src/dbt_score/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Init dbt_score package."""
22

3-
from dbt_score.models import Exposure, Model, Seed, Snapshot, Source
3+
from dbt_score.models import Exposure, Macro, Model, Seed, Snapshot, Source
44
from dbt_score.rule import Rule, RuleViolation, Severity, rule
55
from dbt_score.rule_filter import RuleFilter, rule_filter
66

77
__all__ = [
88
"Exposure",
9+
"Macro",
910
"Model",
1011
"Source",
1112
"Snapshot",

src/dbt_score/evaluation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def evaluate(self) -> None:
6666
self._manifest_loader.snapshots.values(),
6767
self._manifest_loader.exposures.values(),
6868
self._manifest_loader.seeds.values(),
69+
self._manifest_loader.macros.values(),
6970
):
7071
# type inference on elements from `chain` is wonky
7172
# and resolves to superclass HasColumnsMixin
@@ -101,5 +102,6 @@ def evaluate(self) -> None:
101102
or self._manifest_loader.snapshots
102103
or self._manifest_loader.exposures
103104
or self._manifest_loader.seeds
105+
or self._manifest_loader.macros
104106
):
105107
self._formatter.project_evaluated(self.project_score)

src/dbt_score/formatters/human_readable_formatter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from dbt_score.evaluation import EvaluableResultsType
66
from dbt_score.formatters import Formatter
7-
from dbt_score.models import Evaluable, Exposure, Model, Seed, Snapshot, Source
7+
from dbt_score.models import Evaluable, Exposure, Macro, Model, Seed, Snapshot, Source
88
from dbt_score.rule import RuleViolation
99
from dbt_score.scoring import Score
1010

@@ -41,6 +41,8 @@ def pretty_name(evaluable: Evaluable) -> str:
4141
return evaluable.name
4242
case Seed():
4343
return evaluable.name
44+
case Macro():
45+
return evaluable.name
4446
case _:
4547
raise NotImplementedError
4648

src/dbt_score/models.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,60 @@ def __hash__(self) -> int:
645645
return hash(self.unique_id)
646646

647647

648-
Evaluable: TypeAlias = Model | Source | Snapshot | Seed | Exposure
648+
@dataclass
649+
class Macro:
650+
"""Represents a dbt macro.
651+
652+
Attributes:
653+
unique_id: The unique id of the macro (e.g. `macro.package.macro_name`).
654+
name: The name of the macro.
655+
description: The description of the macro.
656+
original_file_path: The path to the macro file
657+
(e.g. `macros/my_macro.sql`).
658+
package_name: The name of the package this macro belongs to.
659+
macro_sql: The SQL code of the macro.
660+
meta: The metadata attached to the macro.
661+
tags: The list of tags attached to the macro.
662+
depends_on: The depends_on of the macro (macros it depends on).
663+
arguments: The list of arguments the macro accepts.
664+
_raw_values: The raw values of the macro in the manifest.
665+
"""
666+
667+
unique_id: str
668+
name: str
669+
description: str
670+
original_file_path: str
671+
package_name: str
672+
macro_sql: str
673+
meta: dict[str, Any]
674+
tags: list[str]
675+
depends_on: dict[str, list[str]] = field(default_factory=dict)
676+
arguments: list[dict[str, Any]] = field(default_factory=list)
677+
_raw_values: dict[str, Any] = field(default_factory=dict)
678+
679+
@classmethod
680+
def from_node(cls, node_values: dict[str, Any]) -> "Macro":
681+
"""Create a macro object from a node in the manifest."""
682+
return cls(
683+
unique_id=node_values["unique_id"],
684+
name=node_values["name"],
685+
description=node_values.get("description", ""),
686+
original_file_path=node_values["original_file_path"],
687+
package_name=node_values["package_name"],
688+
macro_sql=node_values["macro_sql"],
689+
meta=node_values.get("meta", {}),
690+
tags=node_values.get("tags", []),
691+
depends_on=node_values.get("depends_on", {}),
692+
arguments=node_values.get("arguments", []),
693+
_raw_values=node_values,
694+
)
695+
696+
def __hash__(self) -> int:
697+
"""Compute a unique hash for a macro."""
698+
return hash(self.unique_id)
699+
700+
701+
Evaluable: TypeAlias = Model | Source | Snapshot | Seed | Exposure | Macro
649702

650703

651704
class ManifestLoader:
@@ -677,20 +730,27 @@ def __init__(self, file_path: Path, select: Iterable[str] | None = None):
677730
).items()
678731
if exposure_values["package_name"] == self.project_name
679732
}
733+
self.raw_macros = {
734+
macro_id: macro_values
735+
for macro_id, macro_values in self.raw_manifest.get("macros", {}).items()
736+
if macro_values["package_name"] == self.project_name
737+
}
680738

681739
self.models: dict[str, Model] = {}
682740
self.tests: dict[str, list[dict[str, Any]]] = defaultdict(list)
683741
self.sources: dict[str, Source] = {}
684742
self.snapshots: dict[str, Snapshot] = {}
685743
self.exposures: dict[str, Exposure] = {}
686744
self.seeds: dict[str, Seed] = {}
745+
self.macros: dict[str, Macro] = {}
687746

688747
self._reindex_tests()
689748
self._load_models()
690749
self._load_sources()
691750
self._load_snapshots()
692751
self._load_exposures()
693752
self._load_seeds()
753+
self._load_macros()
694754
self._populate_relatives()
695755

696756
if select:
@@ -702,6 +762,7 @@ def __init__(self, file_path: Path, select: Iterable[str] | None = None):
702762
+ len(self.snapshots)
703763
+ len(self.seeds)
704764
+ len(self.exposures)
765+
+ len(self.macros)
705766
) == 0:
706767
logger.warning("Nothing to evaluate!")
707768

@@ -740,6 +801,13 @@ def _load_seeds(self) -> None:
740801
seed = Seed.from_node(node_values, self.tests.get(node_id, []))
741802
self.seeds[node_id] = seed
742803

804+
def _load_macros(self) -> None:
805+
"""Load the macros from the manifest."""
806+
for macro_id, macro_values in self.raw_macros.items():
807+
if macro_values.get("resource_type") == "macro":
808+
macro = Macro.from_node(macro_values)
809+
self.macros[macro_id] = macro
810+
743811
def _reindex_tests(self) -> None:
744812
"""Index tests based on their associated evaluable."""
745813
for node_values in self.raw_nodes.values():
@@ -796,3 +864,4 @@ def _filter_evaluables(self, select: Iterable[str]) -> None:
796864
self.snapshots = {k: s for k, s in self.snapshots.items() if s.name in selected}
797865
self.exposures = {k: e for k, e in self.exposures.items() if e.name in selected}
798866
self.seeds = {k: s for k, s in self.seeds.items() if s.name in selected}
867+
self.macros = {k: m for k, m in self.macros.items() if m.name in selected}

0 commit comments

Comments
 (0)