Skip to content

Add dependency sources for cross-module inheritance - #363

Open
farkasseb wants to merge 2 commits into
uber:masterfrom
farkasseb:feat/dependency-sources
Open

farkasseb wants to merge 2 commits into
uber:masterfrom
farkasseb:feat/dependency-sources

Conversation

@farkasseb

Copy link
Copy Markdown
Contributor

Stacked on #362 (the first commit): it resolves module-qualified parents such as ModuleA.Foo, and its recursion guard also covers the cycles that same-named protocols in two modules can form.

When mocks are generated per module, a protocol often refines a protocol from another module:

// ModuleA
/// @mockable
public protocol Foo {
    associatedtype Value
    func reset()
}

// ModuleB
import ModuleA

/// @mockable
public protocol NarrowFoo: Foo where Value == Int {}

Running mockolo on ModuleB alone, it cannot see Foo, so NarrowFooMock misses reset() and keeps the where clause on a non-generic class:

public class NarrowFooMock: NarrowFoo where Value == Int { … }
// error: 'where' clause cannot be applied to a non-generic top-level declaration

What works today, and where it stops

Change

Three options that mirror the source inputs, with the same precedence:

  • --dependency-sourcedirs
  • --dependency-filelist
  • --dependency-sourcefiles
mockolo -s ModuleB -d ModuleBMocks.swift --dependency-sourcedirs ModuleA

A #344-style setup can move its dependencies from -s to --dependency-sourcedirs and stop mocking them. The #146/#167 case no longer needs parent mocks, so removing them as #302 proposes wouldn't leave per-module users with only -s. This PR leaves the parent mock feature unchanged; #302 notes Uber still uses it.

Design choices:

  • An input, not a new generation pattern. Dependency files go through the same parser and --exclude-suffixes as the sources. Their protocols, annotated or not, are added to the protocol map used for inheritance resolution, never to the set of types to mock. No templates change, and without the new options the output is unchanged.
  • Sources win. Dependencies only fill names the sources don't declare, so a source declaration is never replaced. Names stay bare, as in the existing protocol map, so (as today) two declarations with the same name can't be told apart.
  • Dependencies win over parent mocks. The protocol map is checked before --mockfiles (existing order), so a parent found in the dependencies is used instead of its parent mock.

If you'd rather keep the surface small, I'm happy to cut this down to --dependency-sourcedirs alone.

Test

testPATWithSameTypeConstraintInDependency: the example above, with Foo passed as a dependency. It expects NarrowFooMock with reset(), no where clause and no FooMock. With the dependency input ignored, it fails on the kept where clause and the missing reset().

🤖 Generated with Claude Code

@sidepelican

Copy link
Copy Markdown
Collaborator

I’m a little hesitant to add three more CLI options, since they will be difficult to remove once released.

More broadly, a configuration file might be a better fit for describing sources and their dependencies. It could also leave room to associate source paths with module names, which may help with qualified lookup in the future.

farkasseb and others added 2 commits November 5, 2026 21:52
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep --dependency-sourcedirs as the only CLI input for dependency sources.
The generate(…) parameters stay, matching sourceDirs:/sourceFiles:.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@farkasseb
farkasseb force-pushed the feat/dependency-sources branch from 1d98d5f to 7c5b94e Compare November 6, 2026 05:02
@farkasseb

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I agree, adding multiple parameters can lead to bloat; originally I just tried to match the existing shape. I've now simplified it to one, but if you'd like to go the configuration way, I'm happy to take a look, though I'll need some input.

Option 1 - CLI

The PR is cut down to only --dependency-sourcedirs. (The generate(…) parameters stayed to mirror sourceDirs:/sourceFiles:.)

Option 2 - Config

A --config <file> option, one file per mockolo run, describing the inputs, e.g.:

module: ModuleB
sourceDirs: [Sources/ModuleB]
dependencies:
  ModuleA:
    sourceDirs: [Sources/ModuleA]
  RIBs:
    sourceDirs: [.build/checkouts/RIBs/ios/RIBs]
  • Paths are relative to the config file. Each entry uses sourceDirs or sourceFiles to match the existing vocabulary, and sources from all entries are combined.
  • Dependency sources must be listed explicitly, including sources of indirectly inherited protocols. Nothing is discovered from imports or packages.
  • Module names could support module-aware lookup later. For now, mockolo would still resolve types by name alone and ignore module names.
  • I'd probably choose YAML, as it's the convention, but it would need Yams as a parser dependency. We could use JSON without any dependency, or go the Swift manifest route, like Package.swift: it would be type-checked, but it needs a Swift toolchain at run time and code to compile and evaluate it.

How did you imagine it?

  • Would the config cover inputs only, or would all other options be ported into it over time?
  • Did you mean one project-wide file describing all modules? I don't have a strong preference, but I'm still proposing one file per run.
  • How would --config work together with -s/-f/-srcs? Error?

@farkasseb

Copy link
Copy Markdown
Contributor Author

@sidepelican gentle ping when you have a couple of minutes. Happy to work on this.

@sidepelican

Copy link
Copy Markdown
Collaborator

@farkasseb
Sorry for the late reply, and thanks for putting these options together!

This seems like a good opportunity to try Option 2 with a config file.
How about JSON5 for the format? I’d prefer to avoid adding a YAML parser dependency, and Foundation’s JSON5 support seems like a simple and practical option.

For the config structure, I wonder if we could use a single list of paths for each module, containing both directories and individual files:

{
  module: "ModuleB",
  sources: [
    "Sources/ModuleB",
    "Generated/AdditionalProtocol.swift",
  ],
  destination: "Tests/ModuleBMocks.swift",
  dependencies: {
    ModuleA: [
      "Sources/ModuleA",
      "Shared/SomeProtocol.swift",
    ],
  },
}

Mockolo could determine whether each path is a directory or a file at runtime and collect all the sources.
Since the config itself contains the list of paths, I don’t think we need a separate file list option in the config.
Paths relative to the config file sound good to me.

Would the config cover inputs only, or would all other options be ported into it over time?

I think of the config as an alternative to CLI arguments, so I’d like it to support the --destination and, eventually, as many of the other options as practical. That said, I don’t think we need to add support for everything at once.

Did you mean one project-wide file describing all modules? I don't have a strong preference, but I'm still proposing one file per run.

One file per run is what I had in mind too. Each file would describe the context for a single mockolo invocation, so the number of invocations would stay the same.

How would --config work together with -s/-f/-srcs? Error?

The existing input options already have a precedence order: -s takes precedence over -f, which takes precedence over -srcs. I suggest that if any of these options is provided on the CLI, it replaces the config’s sources list.
For other options, I’d generally expect explicitly provided CLI arguments to take precedence over config values.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants