Skip to content

Add eval for collecting final set instead of list addAll #48

Description

@martinfrancois

Problem

The Streams skill should catch a case where a stream already computes exactly the data a method returns, but the implementation first materializes a temporary list and then mutates a separate set with addAll(...).

This is eval-worthy because replacing stream().forEach(set::add) with toList() plus set.addAll(...) can still leave unnecessary intermediate state. The skill should prefer making the stream own the final collection result when mutability is not needed.

Code before the prompt was executed

The method scanned local workflow files, extracted optional server ports, collected them into a temporary list, and then copied that list into a mutable HashSet before returning it.

private Set<Integer> localWorkflowFilePortReservations(Options options, ConnectedBoard ignoredBoard) {
    Set<Integer> reserved = new HashSet<>();
    Path configDir = options.configDir();
    if (configDir == null || !Files.isDirectory(configDir)) {
        return reserved;
    }
    try (var files = Files.list(configDir)) {
        List<Integer> workflowPorts = files.filter(Files::isRegularFile)
                .filter(file -> PathNames.fileName(file).endsWith(".md"))
                .filter(file -> ignoredBoard.workflowPath() == null
                        || !PathsEqual.samePath(file, ignoredBoard.workflowPath()))
                .map(file -> workflowConfig.serverPort(
                        file, WorkflowEnvironmentResolver.resolver(environment, options.envPath())))
                .flatMap(Optional::stream)
                .toList();
        reserved.addAll(workflowPorts);
    } catch (IOException ignored) {
        // A config directory that cannot be listed leaves only the manifest and probe checks.
    }
    return reserved;
}

Prompt that caused the implementation

The earlier cleanup prompt asked for repeated Java Streams and Optional cleanup passes across the codebase, including replacing stream mutation patterns and checking for remaining stream antipatterns. The implementation focused on removing direct forEach(...) mutation, but still left this temporary-list-then-addAll(...) shape.

Later prompt that exposed the issue

A review comment asked:

Is there really no better way here? Use the streams skill

The comment pointed at reserved.addAll(workflowPorts); after workflowPorts had just been produced by a stream.

Prompt-produced code before maintainer correction

Same as the code before the prompt: the stream produced workflowPorts, then reserved.addAll(workflowPorts) copied it into the returned set.

Why the prompt-produced code is weak

The code is functionally correct, but the temporary list is unnecessary:

  1. The method returns the collected ports directly.
  2. The returned collection does not need to be mutable.
  3. The list allocation does not express the result type; it is only an intermediate copy source.
  4. Replacing external mutation with toList() is incomplete when the next line immediately mutates another collection with the list.

Behavior-equivalence analysis

The final code preserves behavior:

  • Missing or non-directory config dirs still return an empty set.
  • Unreadable config dirs still return an empty set.
  • Duplicate ports are still collapsed because the method returns a set.
  • The caller only merges the returned ports into another set, so the returned collection does not need to be mutable.
  • Encounter order is not part of the contract because the result is used for membership checks.

Maintainer-preferred code

private Set<Integer> localWorkflowFilePortReservations(Options options, ConnectedBoard ignoredBoard) {
    Path configDir = options.configDir();
    if (configDir == null || !Files.isDirectory(configDir)) {
        return Set.of();
    }
    try (var files = Files.list(configDir)) {
        return files.filter(Files::isRegularFile)
                .filter(file -> PathNames.fileName(file).endsWith(".md"))
                .filter(file -> ignoredBoard.workflowPath() == null
                        || !PathsEqual.samePath(file, ignoredBoard.workflowPath()))
                .map(file -> workflowConfig.serverPort(
                        file, WorkflowEnvironmentResolver.resolver(environment, options.envPath())))
                .flatMap(Optional::stream)
                .collect(toImmutableSet());
    } catch (IOException ignored) {
        // A config directory that cannot be listed leaves only the manifest and probe checks.
        return Set.of();
    }
}

Why the replacement is better

The stream now owns the final result. There is no temporary list and no extra mutable set inside the helper. toImmutableSet() makes it explicit that callers should not mutate the helper result, while still preserving the membership semantics the caller needs.

Desired eval behavior

  • Reward detecting stream output that is immediately copied into another collection with addAll(...).
  • Reward checking whether the returned collection must be mutable before choosing the collector.
  • Reward producing the final collection directly from the stream when behavior is preserved.
  • Reward explaining that toList() plus addAll(...) may still be weaker than collecting directly to the needed result type.
  • Reward preserving exception and empty-directory behavior around Files.list(...).

Anti-patterns the eval should reject

  • Replacing addAll(...) with stream().forEach(set::add).
  • Keeping toList() only to immediately copy it into a returned collection.
  • Using Collectors.toSet() in a codebase where the result mutability is intentionally avoided or where lint forbids ambiguous JDK collectors.
  • Changing IO exception handling or probing behavior while doing the stream cleanup.

Suggested eval name

collect-final-set-instead-of-list-addall

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions