blob: bf35d55535e55d45cdb3a00af8b554a61b9b9512 [file] [log] [blame]
#!/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.
# This tool checks that every SQL object created without prefix
# 'internal_' is documented with proper schema.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
import re
import sys
from typing import Union, List, Tuple, Dict
from dataclasses import dataclass
from python.generators.stdlib_docs.utils import *
from python.generators.stdlib_docs.validate import *
from python.generators.stdlib_docs.parse import *
CommentLines = List[str]
AnyDocs = Union['TableViewDocs', 'FunctionDocs', 'ViewFunctionDocs']
# Stores documentation for CREATE {TABLE|VIEW} with comment split into
# segments.
@dataclass
class TableViewDocs:
name: str
obj_type: str
desc: CommentLines
columns: CommentLines
path: str
# Contructs new TableViewDocs from the entire comment, by splitting it on
# typed lines. Returns None for improperly structured schemas.
@staticmethod
def create_from_comment(path: str, comment: CommentLines, module: str,
matches: Tuple) -> Tuple['TableViewDocs', Errors]:
obj_type, name = matches[:2]
# Ignore internal tables and views.
if re.match(r"^internal_.*", name):
return None, []
errors = validate_name(name, module)
col_start = None
has_desc = False
# Splits code into segments by finding beginning of column segment.
for i, line in enumerate(comment):
# Ignore only '--' line.
if line == "--":
continue
m = re.match(Pattern['typed_line'], line)
# Ignore untyped lines
if not m:
if not col_start:
has_desc = True
continue
line_type = m.group(1)
if line_type == "column" and not col_start:
col_start = i
continue
if not has_desc:
errors.append(f"No description for {obj_type}: '{name}' in {path}'\n")
return None, errors
if not col_start:
errors.append(f"No columns for {obj_type}: '{name}' in {path}'\n")
return None, errors
return (
TableViewDocs(name, obj_type, comment[:col_start], comment[col_start:],
path),
errors,
)
def check_comment(self) -> Errors:
return validate_columns(self)
def parse_comment(self) -> dict:
return {
'name': self.name,
'type': self.obj_type,
'desc': parse_desc(self),
'cols': parse_columns(self)
}
# Stores documentation for create_function with comment split into segments.
class FunctionDocs:
def __init__(
self,
path: str,
data_from_sql: dict,
module: str,
name: str,
desc: str,
args: CommentLines,
ret: CommentLines,
):
self.path = path
self.data_from_sql = data_from_sql
self.module = module
self.name = name
self.desc = desc
self.args = args
self.ret = ret
# Contructs new FunctionDocs from whole comment, by splitting it on typed
# lines. Returns None for improperly structured schemas.
@staticmethod
def create_from_comment(path: str, comment: CommentLines, module: str,
matches: Tuple) -> Tuple['FunctionDocs', Errors]:
name, args, ret, sql = matches
# Ignore internal functions.
if re.match(r"^INTERNAL_.*", name):
return None, []
errors = validate_name(name, module, upper=True)
has_desc, start_args, start_ret = False, None, None
args_dict, parse_errors = parse_args_str(args)
errors += parse_errors
# Splits code into segments by finding beginning of args and ret segments.
for i, line in enumerate(comment):
# Ignore only '--' line.
if line == "--":
continue
m = re.match(Pattern['typed_line'], line)
# Ignore untyped lines
if not m:
if not start_args:
has_desc = True
continue
line_type = m.group(1)
if line_type == "arg" and not start_args:
start_args = i
continue
if line_type == "ret" and not start_ret:
start_ret = i
continue
if not has_desc:
errors.append(f"No description for '{name}' in {path}'\n")
return None, errors
if not start_ret or (args_dict and not start_args):
errors.append(f"Function requires 'arg' and 'ret' comments.\n"
f"'{name}' in {path}\n")
return None, errors
if not args_dict:
start_args = start_ret
data_from_sql = {'name': name, 'args': args_dict, 'ret': ret, 'sql': sql}
return (
FunctionDocs(
path,
data_from_sql,
module,
name,
comment[:start_args],
comment[start_args:start_ret] if args_dict else None,
comment[start_ret:],
),
errors,
)
def check_comment(self) -> Errors:
errors = validate_args(self)
errors += validate_ret(self)
return errors
def parse_comment(self) -> dict:
ret_type, ret_desc = parse_ret(self)
return {
'name': self.name,
'desc': parse_desc(self),
'args': parse_args(self),
'return_type': ret_type,
'return_desc': ret_desc
}
# Stores documentation for create_view_function with comment split into
# segments.
class ViewFunctionDocs:
def __init__(
self,
path: str,
data_from_sql: str,
module: str,
name: str,
desc: CommentLines,
args: CommentLines,
columns: CommentLines,
):
self.path = path
self.data_from_sql = data_from_sql
self.module = module
self.name = name
self.desc = desc
self.args = args
self.columns = columns
# Contructs new ViewFunctionDocs from whole comment, by splitting it on typed
# lines. Returns None for improperly structured schemas.
@staticmethod
def create_from_comment(path: str, comment: CommentLines, module: str,
matches: Tuple) -> Tuple['ViewFunctionDocs', Errors]:
name, args, columns, sql = matches
# Ignore internal functions.
if re.match(r"^INTERNAL_.*", name):
return None, []
errors = validate_name(name, module, upper=True)
args_dict, parse_errors = parse_args_str(args)
errors += parse_errors
has_desc, start_args, start_cols = False, None, None
# Splits code into segments by finding beginning of args and cols segments.
for i, line in enumerate(comment):
# Ignore only '--' line.
if line == "--":
continue
m = re.match(Pattern['typed_line'], line)
# Ignore untyped lines
if not m:
if not start_args:
has_desc = True
continue
line_type = m.group(1)
if line_type == "arg" and not start_args:
start_args = i
continue
if line_type == "column" and not start_cols:
start_cols = i
continue
if not has_desc:
errors.append(f"No description for '{name}' in {path}'\n")
return None, errors
if not start_cols or (args_dict and not start_args):
errors.append(f"Function requires 'arg' and 'column' comments.\n"
f"'{name}' in {path}\n")
return None, errors
if not args_dict:
start_args = start_cols
cols_dict, parse_errors = parse_args_str(columns)
errors += parse_errors
data_from_sql = dict(name=name, args=args_dict, columns=cols_dict, sql=sql)
return (
ViewFunctionDocs(
path,
data_from_sql,
module,
name,
comment[:start_args],
comment[start_args:start_cols] if args_dict else None,
comment[start_cols:],
),
errors,
)
def check_comment(self) -> Errors:
errors = validate_args(self)
errors += validate_columns(self, use_data_from_sql=True)
return errors
def parse_comment(self) -> dict:
return {
'name': self.name,
'desc': parse_desc(self),
'args': parse_args(self),
'cols': parse_columns(self)
}
# Reads the provided SQL and, if possible, generates a dictionary with data
# from documentation together with errors from validation of the schema.
def parse_file_to_dict(path: str, sql: str) -> Tuple[Dict[str, any], Errors]:
if sys.platform.startswith('win'):
path = path.replace("\\", "/")
# Get module name
module_name = path.split("/stdlib/")[-1].split("/")[0]
imports, import_errors = parse_typed_docs(path, module_name, sql,
Pattern['create_table_view'],
TableViewDocs)
functions, function_errors = parse_typed_docs(path, module_name, sql,
Pattern['create_function'],
FunctionDocs)
view_functions, view_function_errors = parse_typed_docs(
path, module_name, sql, Pattern['create_view_function'], ViewFunctionDocs)
errors = import_errors + function_errors + view_function_errors
if errors:
sys.stderr.write("\n\n".join(errors))
return ({
'imports': [imp.parse_comment() for imp in imports if imp],
'functions': [fun.parse_comment() for fun in functions if fun],
'view_functions': [
view_fun.parse_comment() for view_fun in view_functions if view_fun
]
}, errors)