blob: 47de64182dddb40571e5aa269edc34ded239fecc [file]
#!/usr/bin/env python3
# Copyright (C) 2025 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.
"""
Unified stdlib parser library for Perfetto SQL standard library.
This module provides functions to parse stdlib SQL files and generate
structured output for consumption by various tools.
"""
import os
from typing import List, Tuple, Optional
from python.generators.sql_processing.docs_parse import DocParseOptions, ParsedModule, parse_file
from python.generators.sql_processing.utils import is_internal
def get_module_name(rel_path: str) -> str:
"""Convert a relative SQL file path to its module name.
Args:
rel_path: Relative path from stdlib root (e.g., "slices/stack.sql")
Returns:
Module name (e.g., "slices.stack")
"""
# Remove .sql extension
path_without_ext = rel_path.removesuffix('.sql')
# Convert path separators to dots for module name
module_name = path_without_ext.replace(os.sep, '.')
return module_name
def parse_all_modules(
stdlib_path: str,
include_internal: bool = False,
name_filter: Optional[str] = None
) -> List[Tuple[str, str, str, ParsedModule]]:
"""Parse all SQL modules in the stdlib.
Args:
stdlib_path: Path to stdlib directory
include_internal: Whether to include internal (private) artifacts
name_filter: Optional regex to filter module names
Returns:
List of tuples: (abs_path, rel_path, module_name, parsed_module)
"""
import re
modules = []
for root, _, files in os.walk(stdlib_path, topdown=True):
for f in files:
abs_path = os.path.join(root, f)
if not abs_path.endswith(".sql"):
continue
rel_path = os.path.relpath(abs_path, stdlib_path)
module_name = get_module_name(rel_path)
# Apply name filter if provided
if name_filter is not None:
try:
pattern = re.compile(name_filter)
except re.error as e:
raise ValueError(f"Invalid regex pattern '{name_filter}': {e}")
if not pattern.match(rel_path):
continue
# Read and parse the file
with open(abs_path, 'r', encoding='utf-8') as f:
sql = f.read()
parsed = parse_file(
rel_path,
sql,
options=DocParseOptions(
enforce_every_column_set_is_documented=True,
include_internal=include_internal),
)
# Some modules (i.e. `deprecated`) should not generate output
if not parsed:
continue
modules.append((abs_path, rel_path, module_name, parsed))
return modules
def format_entities(modules: List[Tuple[str, str, str, ParsedModule]]) -> dict:
"""Format parsed modules as entity map for dependency checking.
Output format:
{
"modules": {
"slices.stack": {
"entities": [
{"name": "stack_from_stack_profile_callsite", "is_internal": false},
{"name": "_intervals_flatten", "is_internal": true}
],
"includes": ["slices.with_context", "graphs.search"]
},
...
},
"entity_to_module": {
"stack_from_stack_profile_callsite": "slices.stack",
"_intervals_flatten": "slices.stack",
...
}
}
"""
modules_dict = {}
entity_to_module = {}
for _, _, module_name, parsed in modules:
# Extract all entity names with internal flag
entities = []
# Tables and views
for table in parsed.table_views:
entities.append({
"name": table.name,
"is_internal": is_internal(table.name)
})
entity_to_module[table.name] = module_name
# Functions
for func in parsed.functions:
entities.append({
"name": func.name,
"is_internal": is_internal(func.name)
})
entity_to_module[func.name] = module_name
# Table functions
for func in parsed.table_functions:
entities.append({
"name": func.name,
"is_internal": is_internal(func.name)
})
entity_to_module[func.name] = module_name
# Macros
for macro in parsed.macros:
entities.append({
"name": macro.name,
"is_internal": is_internal(macro.name)
})
entity_to_module[macro.name] = module_name
# Extract includes
# Note: inc.module already contains the full module name
# Example: inc.module = "android.suspend", inc.package = "android"
includes = [inc.module for inc in parsed.includes]
modules_dict[module_name] = {
"entities": entities,
"includes": includes,
}
return {
"modules": modules_dict,
"entity_to_module": entity_to_module,
}