bazelbuild/bazel · error · OptionProcessorException

No-op options must be annotated with @Deprecated, or have me

Error message

No-op options must be annotated with @Deprecated, or have metadata tag HIDDEN or INTERNAL. Alternatively add %s to the allowlist.

What it means

This compile-time error enforces Bazel's policy that no-op options (effectTags contains NO_OP) must not look like live, supported flags. A NO_OP option must additionally be marked @Deprecated, or carry metadata tag HIDDEN or INTERNAL, or its enclosing class must be listed in the processor's temporary NO_OP_OPTION_ALLOWLIST (test classes being fixed). Otherwise the processor rejects it.

Source

Thrown at src/main/java/com/google/devtools/common/options/processor/OptionsClassProcessor.java:518

          "com.google.devtools.common.options.TestOptions");

  private void checkDeprecated(ExecutableElement method) throws OptionProcessorException {
    Option annotation = method.getAnnotation(Option.class);
    ImmutableList<OptionEffectTag> effectTags = ImmutableList.copyOf(annotation.effectTags());
    ImmutableList<OptionMetadataTag> metadataTags = ImmutableList.copyOf(annotation.metadataTags());
    boolean hasDeprecatedAnnotation = method.getAnnotation(Deprecated.class) != null;
    boolean hasDeprecatedMetadataTag = metadataTags.contains(OptionMetadataTag.DEPRECATED);

    if (effectTags.contains(OptionEffectTag.NO_OP)
        && !metadataTags.contains(OptionMetadataTag.HIDDEN)
        && !metadataTags.contains(OptionMetadataTag.INTERNAL)
        && !hasDeprecatedAnnotation) {
      // Allowlist for tests - these are in the process of being fixed.
      String enclosingClassName = method.getEnclosingElement().toString();
      boolean allowlisted =
          NO_OP_OPTION_ALLOWLIST.stream().anyMatch(enclosingClassName::startsWith);
      if (!allowlisted) {
        throw new OptionProcessorException(
            method,
            "No-op options must be annotated with @Deprecated, or have metadata tag HIDDEN or"
                + " INTERNAL. Alternatively add %s to the allowlist.",
            enclosingClassName);
      }
    }

    if (hasDeprecatedMetadataTag && !hasDeprecatedAnnotation) {
      throw new OptionProcessorException(
          method, "Options with metadata tag DEPRECATED must be annotated with @Deprecated.");
    }
    if (hasDeprecatedAnnotation && !hasDeprecatedMetadataTag) {
      throw new OptionProcessorException(
          method, "Options annotated with @Deprecated must have metadata tag DEPRECATED.");
    }
  }

  private void checkConverter(ExecutableElement method) throws OptionProcessorException {

View on GitHub (pinned to e6e199d060)

Solutions

  1. If the option is being retired, add @Deprecated to the method AND metadataTags = {OptionMetadataTag.DEPRECATED} (both are required by the paired checks).
  2. If the option is intentionally hidden infrastructure, add OptionMetadataTag.HIDDEN or OptionMetadataTag.INTERNAL and set documentationCategory = UNDOCUMENTED.
  3. If the option is supposed to actually do something, replace NO_OP with a real effect tag such as EAGER.
  4. For test classes genuinely needing an unannotated no-op, add the fully-qualified enclosing class name plus trailing dot to NO_OP_OPTION_ALLOWLIST in OptionsClassProcessor.java — as a last resort, since the allowlist is scheduled for removal.

Example fix

// before
@Option(
  name = "stub_flag",
  defaultValue = "false",
  documentationCategory = OptionDocumentationCategory.MISC,
  effectTags = {OptionEffectTag.NO_OP}
)
// after
@Option(
  name = "stub_flag",
  defaultValue = "false",
  documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
  effectTags = {OptionEffectTag.NO_OP},
  metadataTags = {OptionMetadataTag.DEPRECATED}
)
@Deprecated
Defensive patterns

Strategy: validation

Validate before calling

static void checkNoOpIsMarked(boolean isNoOp,
                              boolean hidden, boolean internal,
                              boolean deprecated,
                              String enclosingClass) {
  if (isNoOp && !hidden && !internal && !deprecated) {
    Preconditions.checkState(NO_OP_OPTION_ALLOWLIST.stream().anyMatch(enclosingClass::startsWith),
        "Unmarked no-op option in %s: add @Deprecated or HIDDEN/INTERNAL", enclosingClass);
  }
}

Prevention

When it happens

Trigger: An @Option method with effectTags = {OptionEffectTag.NO_OP} (alone or otherwise), whose class lacks @Deprecated, whose metadataTags contain neither HIDDEN nor INTERNAL, and whose fully-qualified enclosing class name does not start with one of the allowlist prefixes (e.g. com.google.devtools.build.lib.analysis.AnalysisCachingTest.).

Common situations: Introducing a new flag stubbed to do nothing while developing a feature; neutralizing a flag's implementation but leaving it publicly documented; upstream policy tightening (the allowlist is explicitly temporary, so previously-passing test classes can start failing when the allowlist shrinks).

Related errors


AI-assisted analysis of bazelbuild/bazel@e6e199d060 (2026-08-14). Data as JSON: /api/errors/b3764e799d932478. Report an issue: GitHub.