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 19from typing import List, Tuple 20 21from google.protobuf import text_format 22 23from pw_protobuf_codegen_protos.codegen_options_pb2 import CodegenOptions 24from pw_protobuf_protos.field_options_pb2 import FieldOptions 25 26_MULTI_LINE_COMMENT_RE = re.compile(r'/\*.*?\*/', flags=re.MULTILINE) 27_SINGLE_LINE_COMMENT_RE = re.compile(r'//.*?$', flags=re.MULTILINE) 28_SHELL_STYLE_COMMENT_RE = re.compile(r'#.*?$', flags=re.MULTILINE) 29 30# A list of (proto field path, CodegenOptions) tuples. 31ParsedOptions = List[Tuple[str, CodegenOptions]] 32 33 34def load_options_from(options: ParsedOptions, options_file_name: Path): 35 """Loads a single .options file for the given .proto""" 36 with open(options_file_name) as options_file: 37 # Read the options file and strip all styles of comments before parsing. 38 options_data = options_file.read() 39 options_data = _MULTI_LINE_COMMENT_RE.sub('', options_data) 40 options_data = _SINGLE_LINE_COMMENT_RE.sub('', options_data) 41 options_data = _SHELL_STYLE_COMMENT_RE.sub('', options_data) 42 43 for line in options_data.split('\n'): 44 parts = line.strip().split(None, 1) 45 if len(parts) < 2: 46 continue 47 48 # Parse as a name glob followed by a protobuf text format. 49 try: 50 opts = CodegenOptions() 51 text_format.Merge(parts[1], opts) 52 options.append((parts[0], opts)) 53 except: # pylint: disable=bare-except 54 continue 55 56 57def load_options( 58 include_paths: List[Path], proto_file_name: Path 59) -> ParsedOptions: 60 """Loads the .options for the given .proto.""" 61 options: ParsedOptions = [] 62 63 for include_path in include_paths: 64 options_file_name = include_path / proto_file_name.with_suffix( 65 '.options' 66 ) 67 if options_file_name.exists(): 68 load_options_from(options, options_file_name) 69 70 return options 71 72 73def match_options(name: str, options: ParsedOptions) -> CodegenOptions: 74 """Return the matching options for a name.""" 75 matched = CodegenOptions() 76 for name_glob, mask_options in options: 77 if fnmatchcase(name, name_glob): 78 matched.MergeFrom(mask_options) 79 80 return matched 81 82 83def create_from_field_options( 84 field_options: FieldOptions, 85) -> CodegenOptions: 86 """Create a CodegenOptions from a FieldOptions.""" 87 codegen_options = CodegenOptions() 88 89 if field_options.HasField('max_count'): 90 codegen_options.max_count = field_options.max_count 91 92 if field_options.HasField('max_size'): 93 codegen_options.max_size = field_options.max_size 94 95 return codegen_options 96 97 98def merge_field_and_codegen_options( 99 field_options: CodegenOptions, codegen_options: CodegenOptions 100) -> CodegenOptions: 101 """Merge inline field_options and options file codegen_options.""" 102 # The field options specify protocol-level requirements. Therefore, any 103 # codegen options should not violate those protocol-level requirements. 104 if field_options.max_count > 0 and codegen_options.max_count > 0: 105 assert field_options.max_count == codegen_options.max_count 106 107 if field_options.max_size > 0 and codegen_options.max_size > 0: 108 assert field_options.max_size == codegen_options.max_size 109 110 merged_options = CodegenOptions() 111 merged_options.CopyFrom(field_options) 112 merged_options.MergeFrom(codegen_options) 113 114 return merged_options 115