| #!/usr/bin/env python3 |
| # Copyright (C) 2022 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import re |
| from typing import List, Tuple |
| |
| Errors = List[str] |
| CommentLines = List[str] |
| |
| LOWER_NAME = r'[a-z_\d]*' |
| UPPER_NAME = r'[A-Z_\d]*' |
| ANY_WORDS = r'[A-Za-z_\d, \n]*' |
| TYPE = r'[A-Z]*' |
| SQL = r'[\s\S]*?' |
| |
| Pattern = { |
| 'create_table_view': ( |
| # Match create table/view and catch type |
| r'CREATE (?:VIRTUAL )?(TABLE|VIEW)?(?:IF NOT EXISTS)?\s*' |
| # Catch the name |
| fr'({LOWER_NAME})\s*(?:AS|USING)?.*'), |
| 'create_function': ( |
| r"SELECT\s*CREATE_FUNCTION\(\s*" |
| # Function name: we are matching everything [A-Z]* between ' and ). |
| fr"'\s*({UPPER_NAME})\s*\(" |
| # Args: anything before closing bracket with '. |
| fr"({ANY_WORDS})\)',\s*" |
| # Type: [A-Z]* between two '. |
| fr"'({TYPE})',\s*" |
| # Sql: Anything between ' and ');. We are catching \'. |
| fr"'({SQL})'\s*\);"), |
| 'create_view_function': ( |
| r"SELECT\s*CREATE_VIEW_FUNCTION\(\s*" |
| # Function name: we are matching everything [A-Z]* between ' and ). |
| fr"'({UPPER_NAME})\s*\(" |
| # Args: anything before closing bracket with '. |
| fr"({ANY_WORDS})\)',\s*" |
| # Return columns: anything between two '. |
| fr"'\s*({ANY_WORDS})',\s*" |
| # Sql: Anything between ' and ');. We are catching \'. |
| fr"'({SQL})'\s*\);"), |
| 'column': fr'^-- @column\s*({LOWER_NAME})\s*({ANY_WORDS})', |
| 'arg_str': fr"\s*({LOWER_NAME})\s*({TYPE})\s*", |
| 'args': fr'^-- @arg\s*({LOWER_NAME})\s*({TYPE})\s*(.*)', |
| 'return_arg': fr"^-- @ret ({TYPE})\s*(.*)", |
| 'typed_line': fr'^-- @([a-z]*)' |
| } |
| |
| |
| def fetch_comment(lines_reversed: CommentLines) -> CommentLines: |
| comment_reversed = [] |
| for line in lines_reversed: |
| # Break on empty line, as that suggests it is no longer a part of |
| # this comment. |
| if not line or not line.startswith('--'): |
| break |
| |
| # The only option left is a description, but it has to be after |
| # schema columns. |
| comment_reversed.append(line) |
| |
| comment_reversed.reverse() |
| return comment_reversed |
| |
| |
| def match_pattern(pattern: str, file_str: str) -> dict: |
| objects = {} |
| for match in re.finditer(pattern, file_str): |
| line_id = file_str[:match.start()].count('\n') |
| objects[line_id] = match.groups() |
| return dict(sorted(objects.items())) |
| |
| |
| # Whether the name starts with module_name. |
| def validate_name(name: str, module: str, upper: bool = False) -> Errors: |
| module_pattern = f"^{module}_.*" |
| if upper: |
| module_pattern = module_pattern.upper() |
| starts_with_module_name = re.match(module_pattern, name) |
| if module == "common": |
| if starts_with_module_name: |
| return [(f"Invalid name in module {name}. " |
| f"In module 'common' the name shouldn't " |
| f"start with '{module_pattern}'.\n")] |
| else: |
| if not starts_with_module_name: |
| return [(f"Invalid name in module {name}. " |
| f"Name has to begin with '{module_pattern}'.\n")] |
| return [] |
| |
| |
| # Parses string with multiple arguments with type separated by comma into dict. |
| def parse_args_str(args_str: str) -> Tuple[dict, Errors]: |
| if not args_str.strip(): |
| return None, [] |
| |
| errors = [] |
| args = {} |
| for arg_str in args_str.split(","): |
| m = re.match(Pattern['arg_str'], arg_str) |
| if m is None: |
| errors.append(f"Wrong arguments formatting for '{arg_str}'\n") |
| continue |
| args[m.group(1)] = m.group(2) |
| return args, errors |
| |
| |
| def get_text(line: str, no_break_line: bool = True) -> str: |
| line = line.lstrip('--').strip() |
| if not line: |
| return '' if no_break_line else '\n' |
| return line |