1# Copyright 2022 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Options file parsing for proto generation.""" 15 16from fnmatch import fnmatchcase 17from pathlib import Path 18import re 19 20from google.protobuf import text_format 21 22from pw_protobuf_codegen_protos.codegen_options_pb2 import CodegenOptions 23from pw_protobuf_protos.field_options_pb2 import FieldOptions 24 25_MULTI_LINE_COMMENT_RE = re.compile(r'/\*.*?\*/', flags=re.MULTILINE) 26_SINGLE_LINE_COMMENT_RE = re.compile(r'//.*?$', flags=re.MULTILINE) 27_SHELL_STYLE_COMMENT_RE = re.compile(r'#.*?$', flags=re.MULTILINE) 28 29# A list of (proto field path, CodegenOptions) tuples. 30ParsedOptions = list[tuple[str, CodegenOptions]] 31 32 33def load_options_from(options: ParsedOptions, options_file_name: Path): 34 """Loads a single .options file for the given .proto""" 35 with open(options_file_name) as options_file: 36 # Read the options file and strip all styles of comments before parsing. 37 options_data = options_file.read() 38 options_data = _MULTI_LINE_COMMENT_RE.sub('', options_data) 39 options_data = _SINGLE_LINE_COMMENT_RE.sub('', options_data) 40 options_data = _SHELL_STYLE_COMMENT_RE.sub('', options_data) 41 42 for line in options_data.split('\n'): 43 parts = line.strip().split(None, 1) 44 if len(parts) < 2: 45 continue 46 47 # Parse as a name glob followed by a protobuf text format. 48 try: 49 opts = CodegenOptions() 50 text_format.Merge(parts[1], opts) 51 options.append((parts[0], opts)) 52 except: # pylint: disable=bare-except 53 continue 54 55 56def load_options( 57 include_paths: list[Path], proto_file_name: Path, import_prefix: str 58) -> ParsedOptions: 59 """Loads the .options for the given .proto.""" 60 options: ParsedOptions = [] 61 62 if import_prefix and Path(import_prefix) in proto_file_name.parents: 63 proto_file_name = proto_file_name.relative_to(import_prefix) 64 65 for include_path in include_paths: 66 options_file_name = include_path / proto_file_name.with_suffix( 67 '.options' 68 ) 69 if options_file_name.exists(): 70 load_options_from(options, options_file_name) 71 72 return options 73 74 75def match_options(name: str, options: ParsedOptions) -> CodegenOptions: 76 """Return the matching options for a name.""" 77 matched = CodegenOptions() 78 for name_glob, mask_options in options: 79 if fnmatchcase(name, name_glob): 80 matched.MergeFrom(mask_options) 81 82 return matched 83 84 85def create_from_field_options( 86 field_options: FieldOptions, 87) -> CodegenOptions: 88 """Create a CodegenOptions from a FieldOptions.""" 89 codegen_options = CodegenOptions() 90 91 if field_options.HasField('max_count'): 92 codegen_options.max_count = field_options.max_count 93 94 if field_options.HasField('max_size'): 95 codegen_options.max_size = field_options.max_size 96 97 return codegen_options 98 99 100def merge_field_and_codegen_options( 101 field_options: CodegenOptions, codegen_options: CodegenOptions 102) -> CodegenOptions: 103 """Merge inline field_options and options file codegen_options.""" 104 # The field options specify protocol-level requirements. Therefore, any 105 # codegen options should not violate those protocol-level requirements. 106 if field_options.max_count > 0 and codegen_options.max_count > 0: 107 assert field_options.max_count == codegen_options.max_count 108 109 if field_options.max_size > 0 and codegen_options.max_size > 0: 110 assert field_options.max_size == codegen_options.max_size 111 112 merged_options = CodegenOptions() 113 merged_options.CopyFrom(field_options) 114 merged_options.MergeFrom(codegen_options) 115 116 return merged_options 117