Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

from __future__ import annotations

import inspect
from collections.abc import Mapping
from dataclasses import replace
from types import CodeType

from sqlbuild.compiler.compile._helpers.attachment.scope_relationships import (
build_scope_relationship_grants,
Expand Down Expand Up @@ -64,3 +67,41 @@ def build_declaration_scope(
loaded_macros=loaded_macros,
),
)


def rebind_declaration_scope(
*,
scope: DeclarationScopeBuild,
discovered_inputs: DiscoveredProjectInputs,
loaded_macros: dict[str, LoadedMacro],
) -> DeclarationScopeBuild | None:
"""Pair a built index with private macro instances, or return None if their metadata differs."""

if _indexed_macro_metadata(loaded_macros) != _indexed_macro_metadata(scope.loaded_macros):
return None
return DeclarationScopeBuild(
loaded_macros=loaded_macros,
index=scope.index,
resolver=build_declaration_scope_resolver(
discovered_inputs=discovered_inputs,
scope_index=scope.index,
loaded_macros=loaded_macros,
),
)


def _indexed_macro_metadata(loaded_macros: Mapping[str, LoadedMacro]) -> tuple[object, ...]:
return tuple(
(
key,
macro.name,
macro.relative_path,
macro.raw_source,
macro.dependencies,
code.co_firstlineno
if isinstance(code := getattr(macro.function, "__code__", None), CodeType)
else None,
tuple(inspect.signature(macro.function).parameters),
)
for key, macro in loaded_macros.items()
)
21 changes: 15 additions & 6 deletions src/sqlbuild/compiler/compile/main/sql_expansion_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sqlbuild.compiler.compile._helpers.attachment.core import build_effective_vars
from sqlbuild.compiler.compile._helpers.attachment.declaration_scope import (
build_declaration_scope,
rebind_declaration_scope,
)
from sqlbuild.compiler.compile._helpers.render.declarations import (
build_model_declaration_indexes,
Expand Down Expand Up @@ -40,8 +41,9 @@ def build_sql_expansion_context(
cli_vars: dict[str, object] | None = None,
discovered_inputs: DiscoveredProjectInputs | None = None,
declaration_scope: DeclarationScopeBuild | None = None,
static_declaration_scope: DeclarationScopeBuild | None = None,
) -> SqlExpansionContext:
"""Assemble project vars, macros and declarations for SQL expansion."""
"""Assemble SQL expansion inputs; a static scope lends only its index to private macros."""

effective_discovered_inputs: DiscoveredProjectInputs = (
discovered_inputs
Expand All @@ -55,7 +57,7 @@ def build_sql_expansion_context(
cli_vars={} if cli_vars is None else cli_vars,
)
scope: DeclarationScopeBuild = declaration_scope or _build_declaration_scope(
discovered_inputs=effective_discovered_inputs
discovered_inputs=effective_discovered_inputs, static_scope=static_declaration_scope
)
loaded_macros: dict[str, LoadedMacro] = scope.loaded_macros
enums: dict[str, EnumDeclaration]
Expand Down Expand Up @@ -105,9 +107,16 @@ def build_sql_expansion_context(


def _build_declaration_scope(
*, discovered_inputs: DiscoveredProjectInputs
*, discovered_inputs: DiscoveredProjectInputs, static_scope: DeclarationScopeBuild | None
) -> DeclarationScopeBuild:
return build_declaration_scope(
discovered_inputs=discovered_inputs,
loaded_macros=load_project_macros(discovered_inputs.macro_files),
loaded_macros: dict[str, LoadedMacro] = load_project_macros(discovered_inputs.macro_files)
rebound: DeclarationScopeBuild | None = (
None
if static_scope is None
else rebind_declaration_scope(
scope=static_scope, discovered_inputs=discovered_inputs, loaded_macros=loaded_macros
)
)
return rebound or build_declaration_scope(
discovered_inputs=discovered_inputs, loaded_macros=loaded_macros
)
34 changes: 12 additions & 22 deletions src/sqlbuild/lint/_helpers/expansion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
import sqlbuild._native as _native
from sqlbuild.adapter.contract.exceptions import AdapterUserError
from sqlbuild.adapter.discovery.main.resolve_adapter import resolve_adapter
from sqlbuild.compiler.compile.constants import (
HOOK_DIRECTORY_NAME,
MACRO_TOKEN,
)
from sqlbuild.compiler.compile.constants import MACRO_TOKEN
from sqlbuild.compiler.compile.exceptions import CompileInputError
from sqlbuild.compiler.compile.main.expand_sql_with_spans import expand_sql_with_spans
from sqlbuild.compiler.compile.main.sql_expansion_context import build_sql_expansion_context
Expand All @@ -24,9 +21,6 @@
from sqlbuild.compiler.compile.types import TypedSqlValueRenderer
from sqlbuild.compiler.discovery.exceptions import DiscoveryError
from sqlbuild.compiler.discovery.main.discover import discover_project_inputs
from sqlbuild.compiler.discovery.main.named_declaration_role_kind import (
named_declaration_role_kind,
)
from sqlbuild.compiler.discovery.models import DiscoveredProjectInputs
from sqlbuild.compiler.scopes.types import DeclarationKind
from sqlbuild.lint._helpers.sqlbuild_tokens import (
Expand All @@ -37,7 +31,7 @@
)
from sqlbuild.lint.constants import TEMPLATE_INTERPOLATION_START
from sqlbuild.lint.exceptions import ProjectCompileError
from sqlbuild.lint.models import InterpolationSite, LintBody
from sqlbuild.lint.models import InterpolationSite, LintBody, LintFileRole
from sqlbuild.spec.contracts.main.resolve_effective_adapter_name import (
resolve_effective_adapter_name,
)
Expand Down Expand Up @@ -89,6 +83,7 @@ def build_lint_expansion_context(
value_renderer: TypedSqlValueRenderer | None = None,
discovered_inputs: DiscoveredProjectInputs | None = None,
declaration_scope: DeclarationScopeBuild | None = None,
static_declaration_scope: DeclarationScopeBuild | None = None,
) -> SqlExpansionContext:
"""Build the expansion context, reporting compile failures as lint failures."""

Expand All @@ -111,6 +106,7 @@ def build_lint_expansion_context(
discovered_inputs=effective_discovered_inputs,
value_renderer=effective_renderer,
declaration_scope=declaration_scope,
static_declaration_scope=static_declaration_scope,
)
except (AdapterUserError, CompileInputError, DiscoveryError) as error:
raise ProjectCompileError(
Expand All @@ -134,7 +130,7 @@ def _resolve_value_renderer(

def prepare_lint_body(
*,
project_dir: Path,
role: LintFileRole,
file_path: Path,
contents: str,
body_range: tuple[int, int],
Expand All @@ -153,11 +149,11 @@ def prepare_lint_body(
authored_body: str = contents[body_start:body_end]
pre_expansion_sites: tuple[InterpolationSite, ...] = ()
expansion_input: str = authored_body
if _is_generic_audit_path(file_path=file_path, project_dir=project_dir):
if _is_generic_audit_path(role=role):
expansion_input, pre_expansion_sites = neutralize_generic_audit_parameters(
body=authored_body
)
elif _is_sql_hook_path(file_path=file_path, project_dir=project_dir):
elif _is_sql_hook_path(role=role):
expansion_input, pre_expansion_sites = neutralize_context_interpolation(body=authored_body)
expanded: str
expansion_passes: tuple[tuple[ExpansionSpan, ...], ...]
Expand Down Expand Up @@ -232,19 +228,13 @@ def prepare_lint_body(
)


def _is_generic_audit_path(*, file_path: Path, project_dir: Path) -> bool:
def _is_generic_audit_path(*, role: LintFileRole) -> bool:
"""Return whether a lint input is an authored generic-audit definition."""

return (
file_path.is_relative_to(project_dir)
and named_declaration_role_kind(relative_path=file_path.relative_to(project_dir))
is DeclarationKind.AUDIT
)
return role.in_project and role.declaration_kind is DeclarationKind.AUDIT


def _is_sql_hook_path(*, file_path: Path, project_dir: Path) -> bool:
return file_path.is_relative_to(project_dir / HOOK_DIRECTORY_NAME) or (
file_path.is_relative_to(project_dir)
and named_declaration_role_kind(relative_path=file_path.relative_to(project_dir))
is DeclarationKind.SQL_HOOK
def _is_sql_hook_path(*, role: LintFileRole) -> bool:
return role.in_hook_directory or (
role.in_project and role.declaration_kind is DeclarationKind.SQL_HOOK
)
60 changes: 43 additions & 17 deletions src/sqlbuild/lint/_helpers/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@

import re
from functools import cache
from pathlib import Path
from pathlib import Path, PurePath

from sqlbuild.compiler.compile.constants import HOOK_DIRECTORY_NAME
from sqlbuild.compiler.discovery.main.named_declaration_role_kind import (
named_declaration_role_kind,
)
from sqlbuild.compiler.scopes.types import DeclarationKind
from sqlbuild.lint.constants import DSL_HEADER_KINDS
from sqlbuild.lint.models import HeaderSpan
from sqlbuild.lint.models import HeaderSpan, LintFileRole

_QUOTE_CHARACTERS: frozenset[str] = frozenset({"'", '"'})
_ESCAPE_CHARACTER: str = "\\"
_OPEN_PAREN: str = "("
_CLOSE_PAREN: str = ")"
_STATEMENT_TERMINATOR: str = ";"
_UNQUOTED_HEADER_TOKEN: re.Pattern[str] = re.compile(r"""[()'"]""")
_QUOTED_HEADER_TOKENS: dict[str, re.Pattern[str]] = {
quote: re.compile(rf"[\\{quote}]") for quote in _QUOTE_CHARACTERS
}


def scan_headers(*, contents: str, first_only: bool = False) -> tuple[HeaderSpan, ...]:
Expand All @@ -26,6 +31,22 @@ def scan_headers(*, contents: str, first_only: bool = False) -> tuple[HeaderSpan
return _scan_delimited_regions(contents=contents, kinds=DSL_HEADER_KINDS, first_only=first_only)


def lint_file_role(
*, file_path: Path, project_dir: Path, relative_path: PurePath | None = None
) -> LintFileRole:
"""Return the path facts lint needs, reusing a caller's project-relative path."""

if relative_path is None:
if not file_path.is_relative_to(project_dir):
return LintFileRole()
relative_path = file_path.relative_to(project_dir)
return LintFileRole(
declaration_kind=named_declaration_role_kind(relative_path=relative_path),
in_project=True,
in_hook_directory=relative_path.is_relative_to(HOOK_DIRECTORY_NAME),
)


def measurement_body_ranges(*, contents: str) -> tuple[tuple[int, int], ...]:
"""Return authored query ranges inside MEASURE and EVIDENCE blocks."""

Expand All @@ -50,12 +71,17 @@ def lint_body_ranges(
headers: tuple[HeaderSpan, ...],
file_path: Path,
project_dir: Path,
role: LintFileRole | None = None,
) -> tuple[tuple[int, int], ...]:
"""Return lintable authored bodies using resource-specific DSL boundaries."""

if file_path.is_relative_to(project_dir) and named_declaration_role_kind(
relative_path=file_path.relative_to(project_dir)
) in {DeclarationKind.AUDIT, DeclarationKind.SINGULAR_AUDIT}:
file_role: LintFileRole = (
role if role is not None else lint_file_role(file_path=file_path, project_dir=project_dir)
)
if file_role.in_project and file_role.declaration_kind in {
DeclarationKind.AUDIT,
DeclarationKind.SINGULAR_AUDIT,
}:
measurement_ranges: tuple[tuple[int, int], ...] = measurement_body_ranges(contents=contents)
if measurement_ranges:
return measurement_ranges
Expand Down Expand Up @@ -132,24 +158,25 @@ def _match_header_span(
return None
depth: int = 0
in_quote: str | None = None
length: int = len(contents)
while index < length:
match: re.Match[str] | None
while True:
match = (
_UNQUOTED_HEADER_TOKEN if in_quote is None else _QUOTED_HEADER_TOKENS[in_quote]
).search(contents, index)
if match is None:
return None
index = match.start()
character: str = contents[index]
if in_quote is not None:
if character == _ESCAPE_CHARACTER:
index += 2
continue
if character == in_quote:
in_quote = None
index += 1
continue
if character in _QUOTE_CHARACTERS:
in_quote = None
elif character in _QUOTE_CHARACTERS:
in_quote = character
index += 1
continue
if character == _OPEN_PAREN:
elif character == _OPEN_PAREN:
depth += 1
elif character == _CLOSE_PAREN:
else:
depth -= 1
if depth == 0:
terminator_index: int = _skip_whitespace(contents=contents, index=index + 1)
Expand All @@ -161,7 +188,6 @@ def _match_header_span(
span_end = terminator_index + 1
return HeaderSpan(kind=kind, start=keyword_start, end=span_end)
index += 1
return None


def _skip_whitespace(*, contents: str, index: int) -> int:
Expand Down
2 changes: 2 additions & 0 deletions src/sqlbuild/lint/main/build_expansion_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ def build_expansion_context(
project_dir: Path,
discovered_inputs: DiscoveredProjectInputs,
declaration_scope: DeclarationScopeBuild | None = None,
static_declaration_scope: DeclarationScopeBuild | None = None,
) -> SqlExpansionContext:
return build_lint_expansion_context(
project_dir=project_dir,
discovered_inputs=discovered_inputs,
declaration_scope=declaration_scope,
static_declaration_scope=static_declaration_scope,
)
Loading
Loading