• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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