blob: df51633de00e879daaa95c061e6278dea5776efe [file]
# 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.
"""Matches kFeaturesExposedToJava entries to their feature definitions."""
import collections
from typing import Dict, List, Tuple
from definition_files import PathContext
import models
_HELP_TEXT = """\
To fix, add a comment of the form
// FEATURE_DEFINITION_FILE: //path/to/file.cc
to {source}, where file.cc contains the feature's BASE_FEATURE() definition.
If file.cc is a generated file, also add the GN target that generates it to
the `deps` of the java_flag_generator() target."""
def _lookup_names(
entry: models.FeatureEntry, array_namespace: Tuple[str, ...]
) -> List[str]:
"""Qualified names to try, innermost scope first, like C++ name lookup."""
if entry.qualifier.startswith('::'):
return [entry.qualifier[2:] + entry.identifier]
written = [c for c in entry.qualifier.split('::') if c]
return [
'::'.join(list(array_namespace[:i]) + written + [entry.identifier])
for i in range(len(array_namespace), -1, -1)
]
def _describe(definitions: List[models.FeatureDefinition]) -> str:
return '\n'.join(
f' {d.path}:{d.line}: {d.qualified_name} -> '
f'{d.name if d.name is not None else "(unknown)"}'
for d in definitions
)
def resolve(
source: models.ParsedSource,
definitions_by_file: Dict[str, List[models.FeatureDefinition]],
candidates: List[str],
directive_files: List[Tuple[models.DefinitionDirective, str]],
ctx: PathContext,
) -> List[Tuple[models.FeatureEntry, models.FeatureDefinition]]:
"""Returns (entry, definition) pairs in entry order.
Raises GeneratorError listing every problem, including all missing features.
"""
searched = candidates + [path for _, path in directive_files]
by_qualified = collections.defaultdict(list)
by_identifier = collections.defaultdict(list)
for path in searched:
for definition in definitions_by_file[path]:
by_qualified[definition.qualified_name].append(definition)
by_identifier[definition.identifier].append(definition)
results = []
missing = []
used_files = set()
errors = []
for entry in source.entries:
where = f'{entry.source_path}:{entry.line}'
matches = []
for name in _lookup_names(entry, source.array_namespace):
if name in by_qualified:
matches = by_qualified[name]
break
else:
# Fall back to the bare identifier, but never guess between
# different features.
matches = by_identifier.get(entry.identifier, [])
if len({d.qualified_name for d in matches}) > 1:
errors.append(
f'{where}: error: Cannot tell which definition '
f'"{entry.written}" refers to. Candidates:\n'
f'{_describe(matches)}'
)
continue
if not matches:
missing.append(
f'{where}: error: Cannot find the definition of '
f'"{entry.written}".'
)
continue
if len({(d.name, d.error) for d in matches}) > 1:
errors.append(
f'{where}: error: "{entry.written}" has conflicting '
f'definitions:\n{_describe(matches)}'
)
continue
definition = matches[0]
if definition.error:
errors.append(f'{where}: error: {definition.error}')
continue
used_files.update(d.path for d in matches)
results.append((entry, definition))
for directive, path in directive_files:
if path not in used_files:
errors.append(
f'{directive.source_path}:{directive.line}: error: '
f'FEATURE_DEFINITION_FILE {directive.written_path} is unused: '
f'no entry in {source.path} is defined there. Please remove '
f'this comment.'
)
if missing:
searched_list = '\n'.join(f' {ctx.display(p)}' for p in searched)
errors.extend(missing)
errors.append(
f'Searched these files:\n{searched_list}\n'
+ _HELP_TEXT.format(source=ctx.display(source.path))
)
if errors:
raise models.GeneratorError(errors)
return results