blob: 24fef9774c46a8f0de14a75348dd437cc1b55872 [file] [edit]
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Java naming: class_name parsing, FeatureMap derivation, clash checks."""
import re
from typing import Dict, List, Optional
import models
_JAVA_CLASS_NAME_RE = re.compile(r'^((?:[a-z_][A-Za-z0-9_]*\.)+)([A-Z]\w*)$')
# Simple type names referenced by the generated code. A nested class with one
# of these names would shadow it.
_RESERVED_TYPE_NAMES = ('String', 'NullMarked')
def parse_java_class_name(full_name: str, arg_name: str) -> models.JavaClass:
m = _JAVA_CLASS_NAME_RE.match(full_name)
if not m:
raise models.GeneratorError([
f'error: {arg_name} "{full_name}" is not a fully-qualified Java '
f'class name such as "org.chromium.foo.FooFeatures".'
])
return models.JavaClass(package=m.group(1)[:-1], name=m.group(2))
def feature_map_for_source(
source: models.ParsedSource, output_class: models.JavaClass,
override: Optional[models.JavaClass]) -> models.JavaClass:
"""Returns the FeatureMap class used to query the features of `source`.
When `override` is set, it is returned without looking at `source`.
"""
if override:
return override
names = source.feature_map_names
if not names:
raise models.GeneratorError([
f'{source.path}: error: Cannot determine the FeatureMap class: no '
f'JNI_<Name>_GetNativeMap function found. Please set '
f'feature_map_class_name in the java_flag_generator() target, e.g. '
f'feature_map_class_name = "{output_class.package}.FooFeatureMap".'
])
if len(names) > 1:
raise models.GeneratorError([
f'{source.path}: error: Cannot determine the FeatureMap class: '
f'found several JNI_<Name>_GetNativeMap functions '
f'({", ".join(names)}). Set feature_map_class_name in the '
f'java_flag_generator() target, e.g. '
f'feature_map_class_name = "{output_class.package}.FooFeatureMap".'
])
return models.JavaClass(output_class.package, names[0])
def nested_class_name(entry: models.FeatureEntry) -> str:
"""kFoo -> Foo.
This Foo is always a valid Java identifier (see cpp_parser._ENTRY_RE).
"""
return entry.identifier[1:]
def helper_method_name(feature_map: models.JavaClass,
all_maps: List[models.JavaClass]) -> str:
if len(all_maps) == 1:
return 'isFeatureEnabled'
return f'isFeatureEnabledIn{feature_map.name}'
def validate(features_class: models.FeaturesClass) -> None:
errors = []
outer = features_class.java_class.name
maps_by_simple_name: Dict[str, models.JavaClass] = {}
for feature_map in features_class.feature_maps:
other = maps_by_simple_name.setdefault(feature_map.name, feature_map)
if other != feature_map:
errors.append(
f'error: FeatureMap classes {other.full_name} and '
f'{feature_map.full_name} have the same simple name.')
if feature_map.name == outer:
errors.append(
f'error: The generated class {outer} has the same name as '
f'its FeatureMap class {feature_map.full_name}.')
reserved = set(_RESERVED_TYPE_NAMES) | set(maps_by_simple_name)
by_class_name: Dict[str, models.ResolvedFeature] = {}
by_feature_name: Dict[str, models.ResolvedFeature] = {}
for feature in features_class.features:
entry = feature.entry
name = feature.java_class_name
where = f'{entry.source_path}:{entry.line}'
if name == outer:
errors.append(
f'{where}: error: Nested class "{name}" would have the '
f'same name as the generated class.')
elif name in reserved:
errors.append(f'{where}: error: Nested class "{name}" would '
f'shadow a type used by the generated class.')
first = by_class_name.setdefault(name, feature)
if first is not feature:
errors.append(
f'{where}: error: "{entry.written}" and '
f'"{first.entry.written}" ({first.entry.source_path}:'
f'{first.entry.line}) both map to the nested class '
f'"{name}".')
continue
first = by_feature_name.setdefault(feature.feature_name, feature)
if first is not feature:
errors.append(
f'{where}: error: "{entry.written}" and '
f'"{first.entry.written}" ({first.entry.source_path}:'
f'{first.entry.line}) both have the feature name '
f'"{feature.feature_name}".')
if errors:
raise models.GeneratorError(errors)