1#!/usr/bin/env python3 2# Copyright (C) 2022 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16from __future__ import absolute_import 17from __future__ import division 18from __future__ import print_function 19 20import re 21from typing import List, Tuple 22 23Errors = List[str] 24CommentLines = List[str] 25 26LOWER_NAME = r'[a-z_\d]*' 27UPPER_NAME = r'[A-Z_\d]*' 28ANY_WORDS = r'[A-Za-z_\d, \n]*' 29TYPE = r'[A-Z]*' 30SQL = r'[\s\S]*?' 31 32Pattern = { 33 'create_table_view': ( 34 # Match create table/view and catch type 35 r'CREATE (?:VIRTUAL )?(TABLE|VIEW)?(?:IF NOT EXISTS)?\s*' 36 # Catch the name 37 fr'({LOWER_NAME})\s*(?:AS|USING)?.*'), 38 'create_function': ( 39 r"SELECT\s*CREATE_FUNCTION\(\s*" 40 # Function name: we are matching everything [A-Z]* between ' and ). 41 fr"'\s*({UPPER_NAME})\s*\(" 42 # Args: anything before closing bracket with '. 43 fr"({ANY_WORDS})\)',\s*" 44 # Type: [A-Z]* between two '. 45 fr"'({TYPE})',\s*" 46 # Sql: Anything between ' and ');. We are catching \'. 47 fr"'({SQL})'\s*\);"), 48 'create_view_function': ( 49 r"SELECT\s*CREATE_VIEW_FUNCTION\(\s*" 50 # Function name: we are matching everything [A-Z]* between ' and ). 51 fr"'({UPPER_NAME})\s*\(" 52 # Args: anything before closing bracket with '. 53 fr"({ANY_WORDS})\)',\s*" 54 # Return columns: anything between two '. 55 fr"'\s*({ANY_WORDS})',\s*" 56 # Sql: Anything between ' and ');. We are catching \'. 57 fr"'({SQL})'\s*\);"), 58 'column': fr'^-- @column\s*({LOWER_NAME})\s*({ANY_WORDS})', 59 'arg_str': fr"\s*({LOWER_NAME})\s*({TYPE})\s*", 60 'args': fr'^-- @arg\s*({LOWER_NAME})\s*({TYPE})\s*(.*)', 61 'return_arg': fr"^-- @ret ({TYPE})\s*(.*)", 62 'typed_line': fr'^-- @([a-z]*)' 63} 64 65 66def fetch_comment(lines_reversed: CommentLines) -> CommentLines: 67 comment_reversed = [] 68 for line in lines_reversed: 69 # Break on empty line, as that suggests it is no longer a part of 70 # this comment. 71 if not line or not line.startswith('--'): 72 break 73 74 # The only option left is a description, but it has to be after 75 # schema columns. 76 comment_reversed.append(line) 77 78 comment_reversed.reverse() 79 return comment_reversed 80 81 82def match_pattern(pattern: str, file_str: str) -> dict: 83 objects = {} 84 for match in re.finditer(pattern, file_str): 85 line_id = file_str[:match.start()].count('\n') 86 objects[line_id] = match.groups() 87 return dict(sorted(objects.items())) 88 89 90# Whether the name starts with module_name. 91def validate_name(name: str, module: str, upper: bool = False) -> Errors: 92 module_pattern = f"^{module}_.*" 93 if upper: 94 module_pattern = module_pattern.upper() 95 starts_with_module_name = re.match(module_pattern, name) 96 if module == "common": 97 if starts_with_module_name: 98 return [(f"Invalid name in module {name}. " 99 f"In module 'common' the name shouldn't " 100 f"start with '{module_pattern}'.\n")] 101 else: 102 if not starts_with_module_name: 103 return [(f"Invalid name in module {name}. " 104 f"Name has to begin with '{module_pattern}'.\n")] 105 return [] 106 107 108# Parses string with multiple arguments with type separated by comma into dict. 109def parse_args_str(args_str: str) -> Tuple[dict, Errors]: 110 if not args_str.strip(): 111 return None, [] 112 113 errors = [] 114 args = {} 115 for arg_str in args_str.split(","): 116 m = re.match(Pattern['arg_str'], arg_str) 117 if m is None: 118 errors.append(f"Wrong arguments formatting for '{arg_str}'\n") 119 continue 120 args[m.group(1)] = m.group(2) 121 return args, errors 122 123 124def get_text(line: str, no_break_line: bool = True) -> str: 125 line = line.lstrip('--').strip() 126 if not line: 127 return '' if no_break_line else '\n' 128 return line 129