AArnott / CodeGeneration.Roslyn

Assists in performing Roslyn-based code generation during a build.
Microsoft Public License
408 stars 59 forks source link

Generate files in project instead of obj directory #95

Open Pzixel opened 5 years ago

Pzixel commented 5 years ago

In many cases it's desirable to save generated code in repository. It's a common practice for binaries, see recomendation for different lock files in different ecosystems.

It seems reasonable to have some setting like bool GenerateFilesInProject that could enable in-project generation, similar to how WinForm does. It would be nice to have access to generated files in repository, without rebuilding the whole solution.

I can make it myself if we are agreed on that, so my changes wouldn't be futile.

amis92 commented 5 years ago

I'm not the project owner so I can't decide whether it'll be accepted or not, but I'll share my thoughts nonetheless, assuming it'll be.

I think it should be tool-wide - so it'd rather be an MSBuild property to be set in .csproj, passed then into parameters of dotnet-codegen. I don't think doing it as a per-generator property has any purpose.

Making it an MSBuild parameter could be as simple as exposing another specific Intermediate output property, which would default to the current value.

AArnott commented 5 years ago

The way to do this "right" (if there is a right way to check in generated code at all, which I doubt), may be to integrate with Visual Studio Single File Generators. That's the precedent that winforms, resx, and others have set for checking in generated code. A very long time ago this project offered an SFG option, but I took it out long ago.

Why do you want to check in generated code?

Pzixel commented 5 years ago

I was thinking about versioning issues as well as sharing issue, i.e. I cannot send a code snippet without having compiler.

The code issue is more severe, but IIRC NuGet packages always reference exact version so you will always have reproducible builds. But if someone uses semver like CodeGenerator.Roslyn ^0.4.0 then they could be large difference in versions that may lead to strange bugs.

However, when I write these lines I see that it's quite artificial so I start doubt if it worth it at all.

AArnott commented 5 years ago

I don't think nuget even supports a ^0.4.0 syntax. They may support wildcards at the end, but ya, folks who do so do it at their own risk. I never do it.

Let's wait for a real use case to discuss.

notanaverageman commented 5 years ago

I think a valid case would be ReSharper support. If you use the generated code, ReSharper shows red squiggles everywhere. Since ReSharper does not use Roslyn, I think the only way for it to recognize the generated code is writing it to a file in the project.

weltkante commented 5 years ago

ReSharper doesn't need to support Roslyn, they could do with supporting MsBuild. Do they support WPF by now? WPF also generates partial classes into obj but provides certain MsBuild patterns to allow intellisense support without doing a full build.

IMHO if ReSharper doesn't support WPF they should do their homework first, WPF and its code generation pattern has been around long enough to be considered established.

Just my opinion though.

notanaverageman commented 5 years ago

Thanks for the answer. Seems like ReSharper was behaving oddly. I deleted the caches and the ReSharper intellisense is working correctly now.

Pzixel commented 5 years ago

R# works with this, but a bit buggy. It indeed requires reruns and cache cleanup. VS itself goes crazy sometimes, not to say about R# which is much less stable.

robkroll commented 4 years ago

Would it be possible to re-open this issue? For my use case, I really want to save generated files to my repo. This would mean using the file source hash in the filename (filename.3kt2df.generated.cs) would be a bit cumbersome. Ideally, it should be simply filename.generated.cs.

My reasoning for wanting to check in generated code is that I'm trying to replace an existing "code generation" feature that uses reflection at run-time to find attributed fields, and uses LINQ expression trees to write the code. I want to take as much 'magic' out of that process as possible, and seeing a code diff of changes in a check-in would facilitate that.

I would even have potentially a few generators, but these would all generate partial classes of the original so could go into one single file or one per generator. The Roslyn Source Generators proposal also lists checking in generated files as a use case.

Reading about SFGs seems also very interesting, but I would prefer that not all members of my team, and all build agents, would require installing a VSIX. At least one other product, SpecFlow has deprecated using SFG in favour of using MSBuild task.

If agreed, I would try submit a PR for this feature.

amis92 commented 4 years ago

The hash is not of the file source, but for the file path. It's there to enable saving files with the same name but from different subfolders into single output folder:

Source file Generated file
~/Class.cs ~/obj/Class.hash1.cs
~/Folder/Class.cs ~/obj/Class.hash2.cs

I think the first step would be to allow setting the property passed as "output" to a different value than IntermediateOutputDirectory. It's a quick and easy feature.

https://github.com/AArnott/CodeGeneration.Roslyn/blob/d8cb83e0e0029085dba4b1234b561a43de8bcc8c/src/CodeGeneration.Roslyn.Tool/build/CodeGeneration.Roslyn.Tool.targets#L76

The second step is outputting the files without hashes, probably also preserving their relative directory (subpath). This will be much more complex and will need broader design discussion.

robkroll commented 4 years ago

My proposal would be to support the following properties:

<PropertyGroup>
    <CodeGenerationRoslynFilesOutputPath>C:\temp\gen\</CodeGenerationRoslynFilesOutputPath>
    <CodeGenerationMirrorProjectStructure>True</CodeGenerationMirrorProjectStructure>
</PropertyGroup>

The first property CodeGenerationRoslynFilesOutputPath would simply set the directory that the files should output to, and would default to $(IntermediateOutputPath). The other property would cause the output file paths to mirror the path to the file when rooted from the project directory.

I imagine the following use cases:

  1. You want to output to the same project that generated the files (e.g. producing partials) as in my case and the OP's. In this case, with generators that generate files containing the generator attribute (e.g. the DuplicatorSuffix examples) we need to exclude **\*.generated.* from the input.
  2. You want to output to a separate folder (e.g. other project). Replicating the folder structure may be a nice to have.

Given the following folder structure:

+ C:\Projects\ProjectDir
    + SomeDir
        - SomeFile.cs
    + OtherDir
       - OtherFile.cs

Setting only the property CodeGenerationRoslynFilesOutputPath (to C:\temp\gen) would output:

C:\temp\gen\SomeFile.hash.generated.cs
C:\temp\gen\OtherFile.hash.generated.cs

Setting the property CodeGenerationRoslynFilesOutputPath (to C:\temp\gen) and CodeGenerationMirrorProjectStructure would output:

C:\temp\gen\SomeDir\SomeFile.generated.cs
C:\temp\gen\OtherDir\OtherFile.generated.cs

This would however require that CodeGenerationMirrorProjectStructure is passed to CGR.Roslyn.Tool and therefore would need a new commandline arg.

Another easier alternative would be to just check if the project directory and the output directory are the same, and assume that mirroring should happen. Or just drop it, in the worst case I can live with the hashes given that they are only based off the file name :)

amis92 commented 4 years ago

I'll accept a PR for CodeGenerationRoslynFilesOutputPath quickly, it's just a new extension/configurability point.

For the other thing, it's much more complicated. Currently we don't care about source location, we just generate content for it in the intermediate directory under some semi-unique name - collisions are almost impossible. This will drastically change, and just off the top of my head:

  1. Files are generated per compilation: DefineConstants (e.g.DEBUG, NET4, TRACE), target framework, configuration (debug/release) - those are all unique inputs. If you save your files when building in Debug, they will be re-generated (possibly with different content!) in a Release build; when multi-targeting (2+ TargetFrameworks), if a generator uses that in it's output, it'll cause undefined behavior, since the builds may run in parallel, creating race condition on reads/writes to the same file.
  2. What about files linked from outside of project root? (../../MyApp.Shared/Class.cs) Where do you save that, or how do you calculate output?
  3. Currently we're saving empty generated files for every single compilation output. That'd definitely need to change if we saved to a versioned location.