55import re
66from bisect import bisect_right
77from pathlib import Path
8+ from threading import Event
89from typing import Any , cast
910
1011import orjson
2829from sqlbuild .compiler .compile .main .map_expanded_offset import map_expanded_offset
2930from sqlbuild .compiler .compile .models import MappedOffset
3031from sqlbuild .lint ._helpers .sqlbuild_tokens import interpolation_text_at
32+ from sqlbuild .lint .classes .stop_context import LintStopContext
3133from sqlbuild .lint .constants import (
3234 GENERATED_SQL_MESSAGE_SUFFIX ,
3335 GENERATED_SQL_MESSAGE_TEMPLATE ,
3436 LINT_ENGINE_NATIVE ,
3537 VIOLATION_SEVERITY_FAULT ,
3638 VIOLATION_SEVERITY_WARNING ,
3739)
38- from sqlbuild .lint .exceptions import NativeLintError
40+ from sqlbuild .lint .exceptions import LintStoppedError , NativeLintError
3941from sqlbuild .lint .models import LintBody , LintConfig , LintEdit , LintViolation
4042
4143_NATIVE_LINT_API_VERSION : int = 1
@@ -87,6 +89,7 @@ def run_native_sql_lint(
8789 """Lint expanded bodies in Rust and map diagnostics to authored source."""
8890
8991 violations_by_file : dict [Path , list [LintViolation ]] = {}
92+ line_starts_by_path : dict [Path , tuple [int , ...]] = {}
9093 requests : dict [_NativeCacheKey , dict [str , object ]] = {}
9194 for body in bodies :
9295 cache_key : _NativeCacheKey = _native_cache_key (body = body , config = config )
@@ -118,15 +121,20 @@ def run_native_sql_lint(
118121 if cache_key [- 1 ]:
119122 payload ["relation_keys" ] = dict (cache_key [- 1 ])
120123 requests [cache_key ] = payload
124+ check_lint_not_stopped ()
121125 response_cache : dict [_NativeCacheKey , _NativeResult ] = _native_responses (requests = requests )
122126
123127 for body in bodies :
128+ check_lint_not_stopped ()
124129 response : _NativeResult = response_cache [_native_cache_key (body = body , config = config )]
130+ contents : str = contents_by_path [body .file_path ]
131+ line_starts : tuple [int , ...] | None = line_starts_by_path .get (body .file_path )
132+ if line_starts is None :
133+ line_starts = _line_starts (contents )
134+ line_starts_by_path [body .file_path ] = line_starts
125135 if isinstance (response , NativeLintError ):
126136 parse_violation : LintViolation | None = _parse_failure_violation (
127- error = response ,
128- body = body ,
129- contents = contents_by_path [body .file_path ],
137+ error = response , body = body , line_starts = line_starts
130138 )
131139 if parse_violation is None :
132140 raise response
@@ -140,18 +148,27 @@ def run_native_sql_lint(
140148 violation : LintViolation | None = _authored_violation (
141149 raw_diagnostic = raw_diagnostic ,
142150 body = body ,
143- contents = contents_by_path [body .file_path ],
151+ contents = contents ,
152+ line_starts = line_starts ,
144153 dialect = config .dialect ,
145154 )
146155 except NativeLintError as error :
147156 violation = _parse_failure_violation (
148- error = error , body = body , contents = contents_by_path [ body . file_path ]
157+ error = error , body = body , line_starts = line_starts
149158 )
150159 if violation is not None :
151160 violations_by_file .setdefault (body .file_path , []).append (violation )
152161 return {path : tuple (entries ) for path , entries in violations_by_file .items ()}
153162
154163
164+ def check_lint_not_stopped () -> None :
165+ """Abandon a speculative lint run on this thread once its owner no longer needs it."""
166+
167+ stop : Event | None = LintStopContext .active .get ()
168+ if stop is not None and stop .is_set ():
169+ raise LintStoppedError
170+
171+
155172def _native_cache_key (* , body : LintBody , config : LintConfig ) -> _NativeCacheKey :
156173 return (
157174 body .lint_text ,
@@ -223,7 +240,7 @@ def _native_result(*, response: object) -> _NativeResult:
223240
224241
225242def _parse_failure_violation (
226- * , error : NativeLintError , body : LintBody , contents : str
243+ * , error : NativeLintError , body : LintBody , line_starts : tuple [ int , ...]
227244) -> LintViolation :
228245 """Convert a per-body native parse rejection into an authored project fault."""
229246
@@ -237,7 +254,7 @@ def _parse_failure_violation(
237254 expanded_offset += min (expanded_column - 1 , len (expanded_lines [expanded_line - 1 ]))
238255 mapped : MappedOffset = map_expanded_offset (offset = expanded_offset , passes = body .passes )
239256 absolute_offset : int = body .body_start + mapped .offset
240- line , column = _offset_position (offset = absolute_offset , line_starts = _line_starts ( contents ) )
257+ line , column = _offset_position (offset = absolute_offset , line_starts = line_starts )
241258 return LintViolation (
242259 file_path = body .file_path ,
243260 line = line ,
@@ -251,7 +268,12 @@ def _parse_failure_violation(
251268
252269
253270def _authored_violation (
254- * , raw_diagnostic : object , body : LintBody , contents : str , dialect : str
271+ * ,
272+ raw_diagnostic : object ,
273+ body : LintBody ,
274+ contents : str ,
275+ line_starts : tuple [int , ...],
276+ dialect : str ,
255277) -> LintViolation | None :
256278 if not isinstance (raw_diagnostic , dict ):
257279 raise NativeLintError ("native lint diagnostic must be an object" )
@@ -286,16 +308,15 @@ def _authored_violation(
286308 _SQLBUILD_HARNESS_CTE_PREFIXES , absolute_offset
287309 ):
288310 return None
289- starts : tuple [int , ...] = _line_starts (contents )
290- line , column = _offset_position (offset = absolute_offset , line_starts = starts )
311+ line , column = _offset_position (offset = absolute_offset , line_starts = line_starts )
291312 end_position : tuple [int , int ] | None = _authored_end_position (
292313 start = start ,
293314 end = end ,
294315 mapped_start = mapped ,
295316 absolute_start = absolute_offset ,
296317 body = body ,
297318 contents = contents ,
298- line_starts = starts ,
319+ line_starts = line_starts ,
299320 dialect = dialect ,
300321 )
301322 fix : LintEdit | None = _authored_fix (
0 commit comments