Skip to content

Commit 4abf5d5

Browse files
committed
Update checker.py
1 parent 1ac08d1 commit 4abf5d5

1 file changed

Lines changed: 180 additions & 84 deletions

File tree

src/overpass_ql_checker/checker.py

Lines changed: 180 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -803,39 +803,48 @@ def _parse_bbox_setting(self) -> None:
803803
if i > 0:
804804
self.expect(TokenType.COMMA)
805805

806-
if not self.match(TokenType.NUMBER):
806+
if not self.match(TokenType.NUMBER, TokenType.TEMPLATE_PLACEHOLDER):
807807
self.error(f"Expected coordinate {i + 1} in bbox")
808808
else:
809809
coord = self.advance()
810-
try:
811-
coord_val = float(coord.value)
812-
if i in [0, 2]: # latitude values
813-
if not -90 <= coord_val <= 90:
814-
self.error(
815-
f"Latitude must be between -90 and 90: " f"{coord_val}"
816-
)
817-
else: # longitude values
818-
if not -180 <= coord_val <= 180:
819-
self.error(
820-
f"Longitude must be between -180 and 180: "
821-
f"{coord_val}"
822-
)
823-
except ValueError:
824-
self.error(f"Invalid coordinate: {coord.value}")
810+
# Only validate if it's a number (skip template placeholders)
811+
if coord.type == TokenType.NUMBER:
812+
self._validate_bbox_coordinate(coord, i)
813+
814+
def _validate_bbox_coordinate(self, coord: Token, index: int) -> None:
815+
"""Validate a single bbox coordinate."""
816+
try:
817+
coord_val = float(coord.value)
818+
if index in [0, 2]: # latitude values
819+
if not -90 <= coord_val <= 90:
820+
self.error(f"Latitude must be between -90 and 90: {coord_val}")
821+
else: # longitude values
822+
if not -180 <= coord_val <= 180:
823+
self.error(f"Longitude must be between -180 and 180: {coord_val}")
824+
except ValueError:
825+
self.error(f"Invalid coordinate: {coord.value}")
825826

826827
def _parse_date_setting(self, setting_token: Token) -> None:
827828
"""Parse date, diff, or adiff settings."""
828829
self.expect(TokenType.COLON)
829830

830-
if not self.match(TokenType.STRING):
831-
self.error(f"Expected date string after {setting_token.value}:")
831+
if not self.match(TokenType.STRING, TokenType.TEMPLATE_PLACEHOLDER):
832+
error_msg = (
833+
f"Expected date string or template placeholder after "
834+
f"{setting_token.value}:"
835+
)
836+
self.error(error_msg)
832837
else:
833838
date_str = self.advance()
834-
# Basic ISO 8601 date format validation - also accept template placeholders
835-
# Accept both colons and hyphens in time part (common variation)
836-
iso_pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}[-:]\d{2}[-:]\d{2}Z$"
837-
if not (re.match(iso_pattern, date_str.value) or "{{" in date_str.value):
838-
self.error("Invalid date format. Expected YYYY-MM-DDTHH:MM:SSZ")
839+
# Only validate if it's a string (skip template placeholders)
840+
if date_str.type == TokenType.STRING:
841+
# Basic ISO 8601 date format validation
842+
# Accept both colons and hyphens in time part (common variation)
843+
iso_pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}[-:]\d{2}[-:]\d{2}Z$"
844+
if not (
845+
re.match(iso_pattern, date_str.value) or "{{" in date_str.value
846+
):
847+
self.error("Invalid date format. Expected YYYY-MM-DDTHH:MM:SSZ")
839848

840849
def _parse_unknown_setting(self, setting_token: Token) -> None:
841850
"""Parse unknown settings with warning."""
@@ -874,22 +883,57 @@ def _parse_csv_parameters(self) -> None:
874883
self.advance() # Skip (
875884

876885
# Parse CSV parameters - field lists, options, separators, etc.
877-
paren_depth = 1
878-
while paren_depth > 0 and not self.match(TokenType.EOF):
879-
token = self.current_token()
880-
if token.type == TokenType.LPAREN:
881-
paren_depth += 1
882-
elif token.type == TokenType.RPAREN:
883-
paren_depth -= 1
884-
885-
if paren_depth > 0: # Don't advance past the final closing paren
886-
self.advance()
886+
# CSV format can be: csv(field1,field2,...; options; separator)
887+
self._parse_csv_field_list()
888+
889+
# Handle optional sections separated by semicolons
890+
while self.match(TokenType.SEMICOLON):
891+
self.advance() # Skip ;
892+
# Parse options or separators
893+
while not self.match(TokenType.SEMICOLON, TokenType.RPAREN, TokenType.EOF):
894+
if self.match(TokenType.STRING, TokenType.IDENTIFIER, TokenType.NUMBER):
895+
self.advance()
896+
else:
897+
break
887898

888899
if self.match(TokenType.RPAREN):
889900
self.advance() # Skip final )
890901
else:
891902
self.error("Expected ')' after CSV parameters")
892903

904+
def _parse_csv_field_list(self) -> None:
905+
"""Parse CSV field list (first part of CSV parameters)."""
906+
# Handle first field
907+
if self.match(TokenType.STRING, TokenType.IDENTIFIER, TokenType.COLON):
908+
self._parse_csv_field()
909+
910+
# Handle additional fields separated by commas
911+
while self.match(TokenType.COMMA):
912+
self.advance() # Skip comma
913+
if not self.match(
914+
TokenType.SEMICOLON, TokenType.RPAREN
915+
): # Not end of field list
916+
self._parse_csv_field()
917+
918+
def _parse_csv_field(self) -> None:
919+
"""Parse a single CSV field specification."""
920+
# Handle special field types like ::id, ::type, etc.
921+
if self.match(TokenType.COLON):
922+
self.advance() # Skip first :
923+
if self.match(TokenType.COLON):
924+
self.advance() # Skip second :
925+
# Parse field name after ::
926+
if self.match(TokenType.IDENTIFIER):
927+
self.advance()
928+
# Handle quoted field names or regular identifiers
929+
elif self.match(TokenType.STRING, TokenType.IDENTIFIER):
930+
self.advance()
931+
else:
932+
# Allow any tokens in CSV field specifications for now
933+
# since CSV field syntax can be complex
934+
if not self.match(TokenType.COMMA, TokenType.SEMICOLON, TokenType.RPAREN):
935+
self.advance()
936+
893937
def parse_settings(self) -> bool:
894938
"""Parse settings statement."""
895939
if not self.match(TokenType.LBRACKET):
@@ -1373,16 +1417,18 @@ def _parse_id_list_filter(self) -> None:
13731417

13741418
def _parse_user_list_filter(self) -> None:
13751419
"""Parse user list filter."""
1376-
if not self.match(TokenType.STRING):
1377-
self.error("Expected username string after 'user:'")
1420+
if not self.match(TokenType.STRING, TokenType.TEMPLATE_PLACEHOLDER):
1421+
self.error("Expected username string or template placeholder after 'user:'")
13781422
else:
13791423
self.advance() # First username
13801424

13811425
# Parse additional usernames
13821426
while self.match(TokenType.COMMA):
13831427
self.advance()
1384-
if not self.match(TokenType.STRING):
1385-
self.error("Expected username string in user list")
1428+
if not self.match(TokenType.STRING, TokenType.TEMPLATE_PLACEHOLDER):
1429+
self.error(
1430+
"Expected username string or template placeholder in user list"
1431+
)
13861432
else:
13871433
self.advance()
13881434

@@ -1900,40 +1946,65 @@ def _parse_block_parameters(self, block_type: Token) -> None:
19001946
self.expect(TokenType.RPAREN)
19011947

19021948
def _parse_for_evaluator(self) -> None:
1903-
"""Parse FOR loop evaluator like t["key"] or user()."""
1949+
"""Parse FOR loop evaluator like t["key"], user(), keys(), etc."""
19041950
# Handle user() pattern
19051951
if (
19061952
self.match(TokenType.IDENTIFIER)
19071953
and self.current_token().value.lower() == "user"
19081954
and self.peek_token()
19091955
and self.peek_token().type == TokenType.LPAREN
19101956
):
1911-
19121957
self.advance() # Skip 'user'
19131958
self.advance() # Skip '('
19141959
self.expect(TokenType.RPAREN)
19151960
return
19161961

1917-
# Handle t["key"] pattern
1918-
if not self.match(TokenType.IDENTIFIER):
1919-
self.error("Expected evaluator identifier in for loop")
1962+
# Handle keys() pattern
1963+
if (
1964+
self.match(TokenType.IDENTIFIER)
1965+
and self.current_token().value.lower() == "keys"
1966+
and self.peek_token()
1967+
and self.peek_token().type == TokenType.LPAREN
1968+
):
1969+
self.advance() # Skip 'keys'
1970+
self.advance() # Skip '('
1971+
self.expect(TokenType.RPAREN)
19201972
return
1921-
self.advance()
19221973

1923-
# Parse tag filter part like ["key"]
1924-
if self.match(TokenType.LBRACKET):
1974+
# Handle t["key"] pattern and other complex expressions
1975+
if self.match(TokenType.IDENTIFIER):
19251976
self.advance()
1926-
if self.match(TokenType.STRING, TokenType.IDENTIFIER):
1977+
1978+
# Parse tag filter part like ["key"] or function calls
1979+
if self.match(TokenType.LBRACKET):
19271980
self.advance()
1928-
else:
1929-
self.error("Expected key name in for evaluator")
1930-
self.expect(TokenType.RBRACKET)
1981+
if self.match(TokenType.STRING, TokenType.IDENTIFIER):
1982+
self.advance()
1983+
else:
1984+
self.error("Expected key name in for evaluator")
1985+
self.expect(TokenType.RBRACKET)
1986+
elif self.match(TokenType.LPAREN):
1987+
# Handle function calls in for evaluator
1988+
self.advance() # Skip (
1989+
# Parse function arguments
1990+
while not self.match(TokenType.RPAREN, TokenType.EOF):
1991+
if self.match(
1992+
TokenType.STRING, TokenType.IDENTIFIER, TokenType.NUMBER
1993+
):
1994+
self.advance()
1995+
elif self.match(TokenType.COMMA):
1996+
self.advance()
1997+
else:
1998+
break
1999+
self.expect(TokenType.RPAREN)
19312000
else:
1932-
self.error("Expected tag filter in for evaluator")
2001+
self.error("Expected evaluator identifier in for loop")
19332002

19342003
def _parse_block_body(self) -> bool:
19352004
"""Parse block body and return whether braces were used."""
19362005
has_braces = False
2006+
2007+
# Handle both braces { } and parentheses ( ) for block bodies
19372008
if self.match(TokenType.LBRACE):
19382009
has_braces = True
19392010
self.advance()
@@ -1943,6 +2014,15 @@ def _parse_block_body(self) -> bool:
19432014
break
19442015

19452016
self.expect(TokenType.RBRACE)
2017+
elif self.match(TokenType.LPAREN):
2018+
has_braces = True # Treat parentheses like braces for semicolon handling
2019+
self.advance()
2020+
2021+
while not self.match(TokenType.RPAREN, TokenType.EOF):
2022+
if not self.parse_statement():
2023+
break
2024+
2025+
self.expect(TokenType.RPAREN)
19462026
return has_braces
19472027

19482028
def _parse_else_clause(self) -> bool:
@@ -1986,8 +2066,13 @@ def parse_block_statement(self):
19862066
if block_type.type == TokenType.IF:
19872067
has_braces = self._parse_else_clause() or has_braces
19882068

1989-
# Only expect semicolon if we didn't use braces
1990-
if not has_braces:
2069+
# Semicolon handling: optional for braced statements, required for non-braced
2070+
if has_braces:
2071+
# For braced statements, semicolon is completely optional
2072+
if self.match(TokenType.SEMICOLON):
2073+
self.advance()
2074+
else:
2075+
# For non-braced statements, semicolon is required
19912076
self.expect(TokenType.SEMICOLON)
19922077

19932078
return True
@@ -2263,8 +2348,37 @@ def _parse_make_statement(self) -> None:
22632348

22642349
def _parse_make_value_expression(self) -> None:
22652350
"""Parse value expression in make statement."""
2351+
self._parse_make_ternary_expression()
2352+
2353+
def _parse_make_ternary_expression(self) -> None:
2354+
"""Parse ternary expressions (condition ? true_value : false_value)."""
2355+
self._parse_make_comparison_expression()
2356+
2357+
# Handle ternary operator
2358+
if self.match(TokenType.QUESTION):
2359+
self.advance() # Skip ?
2360+
self._parse_make_comparison_expression() # true value
2361+
if self.match(TokenType.COLON):
2362+
self.advance() # Skip :
2363+
self._parse_make_comparison_expression() # false value
2364+
else:
2365+
self.error("Expected ':' in ternary expression")
2366+
2367+
def _parse_make_comparison_expression(self) -> None:
2368+
"""Parse comparison expressions (==, !=, <, >, <=, >=)."""
22662369
self._parse_make_additive_expression()
22672370

2371+
while self.match(
2372+
TokenType.EQUAL_EQUAL,
2373+
TokenType.NOT_EQUALS,
2374+
TokenType.LESS_THAN,
2375+
TokenType.GREATER_THAN,
2376+
TokenType.LESS_EQUAL,
2377+
TokenType.GREATER_EQUAL,
2378+
):
2379+
self.advance() # Skip operator
2380+
self._parse_make_additive_expression()
2381+
22682382
def _parse_make_additive_expression(self) -> None:
22692383
"""Parse additive expressions (+ and -)."""
22702384
self._parse_make_multiplicative_expression()
@@ -2377,10 +2491,20 @@ def _parse_convert_assignments(self) -> None:
23772491

23782492
def _parse_convert_single_assignment(self) -> None:
23792493
"""Parse a single assignment in a convert statement."""
2380-
# Handle ::id syntax
2494+
# Handle ::id syntax or ::=id() syntax
23812495
if self.match(TokenType.COLON):
23822496
self.advance() # Skip first :
2383-
self.expect(TokenType.COLON) # Skip second :
2497+
if self.match(TokenType.COLON):
2498+
self.advance() # Skip second :
2499+
# Handle ::= syntax (double colon equals)
2500+
if self.match(TokenType.EQUALS):
2501+
self.advance() # Skip =
2502+
# Parse value expression after ::=
2503+
self._parse_convert_assignment_value()
2504+
return
2505+
else:
2506+
# Single colon, rewind
2507+
self.pos -= 1
23842508

23852509
# Parse the identifier
23862510
if not self.match(TokenType.IDENTIFIER):
@@ -2399,36 +2523,8 @@ def _parse_convert_single_assignment(self) -> None:
23992523

24002524
def _parse_convert_assignment_value(self) -> None:
24012525
"""Parse the value part of a convert assignment."""
2402-
# Handle function calls like id(), type()
2403-
if (
2404-
self.match(TokenType.IDENTIFIER)
2405-
and self.peek_ahead(1)
2406-
and self.peek_ahead(1).type == TokenType.LPAREN
2407-
):
2408-
self.advance() # Function name
2409-
self.expect(TokenType.LPAREN)
2410-
2411-
# Parse function arguments (can be nested)
2412-
if not self.match(TokenType.RPAREN):
2413-
self._parse_make_function_args() # Reuse from make statement
2414-
2415-
self.expect(TokenType.RPAREN)
2416-
# Handle tag access like t["key"]
2417-
elif self.match(TokenType.IDENTIFIER):
2418-
self.advance()
2419-
# Handle tag access like t["key"]
2420-
if self.match(TokenType.LBRACKET):
2421-
self.advance() # Skip [
2422-
if self.match(TokenType.STRING):
2423-
self.advance() # Skip string key
2424-
else:
2425-
self.error("Expected string key in tag access")
2426-
self.expect(TokenType.RBRACKET)
2427-
# Handle simple values
2428-
elif self.match(TokenType.NUMBER, TokenType.STRING):
2429-
self.advance()
2430-
else:
2431-
self.error("Expected value expression in convert assignment")
2526+
# Reuse the sophisticated expression parsing from make statements
2527+
self._parse_make_value_expression()
24322528

24332529
def _parse_parentheses_content(self) -> None:
24342530
"""Parse content inside parentheses, handling nested parentheses."""

0 commit comments

Comments
 (0)