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_with_suffix( 57 include_paths: list[Path], 58 proto_file_name: Path, 59 options_files: list[Path], 60 suffix: str, 61) -> ParsedOptions: 62 """Loads the options for the given .proto file.""" 63 options: ParsedOptions = [] 64 65 # First load from any files that were directly specified. 66 for options_file in options_files: 67 if ( 68 options_file.name == proto_file_name.with_suffix(suffix).name 69 and options_file.exists() 70 ): 71 load_options_from(options, options_file) 72 73 # Then search specified search paths for options files. 74 for include_path in include_paths: 75 options_file_name = include_path / proto_file_name.with_suffix(suffix) 76 if options_file_name.exists(): 77 load_options_from(options, options_file_name) 78 79 return options 80 81 82def load_options( 83 include_paths: list[Path], 84 proto_file_name: Path, 85 options_files: list[Path], 86 allow_generic_options_extension: bool = True, 87) -> ParsedOptions: 88 # Try to load pwpb_options first. If they exist, ignore regular options. 89 options = _load_options_with_suffix( 90 include_paths, proto_file_name, options_files, ".pwpb_options" 91 ) 92 if allow_generic_options_extension and not options: 93 options = _load_options_with_suffix( 94 include_paths, proto_file_name, options_files, ".options" 95 ) 96 97 return options 98 99 100def match_options(name: str, options: ParsedOptions) -> CodegenOptions: 101 """Return the matching options for a name.""" 102 matched = CodegenOptions() 103 for name_glob, mask_options in options: 104 if fnmatchcase(name, name_glob): 105 matched.MergeFrom(mask_options) 106 107 return matched 108 109 110def create_from_field_options( 111 field_options: FieldOptions, 112) -> CodegenOptions: 113 """Create a CodegenOptions from a FieldOptions.""" 114 codegen_options = CodegenOptions() 115 116 if field_options.HasField('max_count'): 117 codegen_options.max_count = field_options.max_count 118 119 if field_options.HasField('max_size'): 120 codegen_options.max_size = field_options.max_size 121 122 return codegen_options 123 124 125def merge_field_and_codegen_options( 126 field_options: CodegenOptions, codegen_options: CodegenOptions 127) -> CodegenOptions: 128 """Merge inline field_options and options file codegen_options.""" 129 # The field options specify protocol-level requirements. Therefore, any 130 # codegen options should not violate those protocol-level requirements. 131 if field_options.max_count > 0 and codegen_options.max_count > 0: 132 assert field_options.max_count == codegen_options.max_count 133 134 if field_options.max_size > 0 and codegen_options.max_size > 0: 135 assert field_options.max_size == codegen_options.max_size 136 137 merged_options = CodegenOptions() 138 merged_options.CopyFrom(field_options) 139 merged_options.MergeFrom(codegen_options) 140 141 return merged_options 142