Allow msbuild to pass properties and metadata as analyzerconfig:#43617
Allow msbuild to pass properties and metadata as analyzerconfig:#43617chsienki merged 4 commits intodotnet:masterfrom
Conversation
- Add new target to evaluate requested properties and metadata - Add new task to turn collected evaluations into a global analyzer config - Add tests
|
Open issue: We use the Is that actually a problem? The retrieval mechanism in the compiler requires a file path to ask about, so it doesn't make any sense to have metadata about non-items on disk. Should we block this at the target level? I considered filtering based on existence, but it seems possible that a file could get dropped on disk between the target and compilation so that the file would exist by the time lookup occured. |
|
@rainersigwald and @dsplaisted Could you check the MSBuild foo over for me? Happy to walk over it in a call or can send binary logs that show how it runs. |
dsplaisted
left a comment
There was a problem hiding this comment.
Looks good, though I'd like @rainersigwald to take a look too.
Also it would be better to have tests that exercise the logic in the .targets file too. I'm not sure how hard that is with your infrastructure.
| <Target Name="GenerateMSBuildAnalyzerConfigFile" | ||
| BeforeTargets="BeforeCompile;CoreCompile" | ||
| DependsOnTargets="PrepareForBuild;CoreGenerateAssemblyInfo" | ||
| Condition="'$(_GeneratedAnalyzerConfigShouldRun)' == 'true'" |
There was a problem hiding this comment.
I don't think your ordering around this will work--did you validate that the no-items-defined case skips this target?
Generally the condition is evaluated before DependsOn or BeforeTargets run. It doesn't stop BeforeTargets from running . . . but it checks before running them. https://docs.microsoft.com/en-us/visualstudio/msbuild/target-build-order?view=vs-2019#determine-the-target-build-order
<Project>
<PropertyGroup>
<Condition>true</Condition>
</PropertyGroup>
<Target Name="entry" Condition="$(Condition) == true" />
<Target Name="before" BeforeTargets="entry">
<PropertyGroup>
<Condition>false</Condition>
</PropertyGroup>
</Target>
</Project>$ msbuild -v:d .\foo.proj
Microsoft (R) Build Engine version 16.7.0-dev-20221-01+73c84c6bd for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.
Build started 4/23/2020 4:22:53 PM.
0>Process = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Current\Bin\MSBuild.exe"
MSBuild executable path = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Current\Bin\MSBuild.exe"
Command line arguments = ""C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\\MSBuild\Current\Bin\MSBuild.exe" -v:d .\foo.proj"
Current directory = "S:\msbuild"
MSBuild version = "16.7.0-dev-20221-01+73c84c6bd"
1>Project "S:\msbuild\foo.proj" on node 1 (default targets).
1>Building with tools version "Current".
1>Target "before" in project "S:\msbuild\foo.proj" (target "entry" depends on it):
1>Done building target "before" in project "foo.proj".
1>Target "entry" in project "S:\msbuild\foo.proj" (entry point):
1>Done building target "entry" in project "foo.proj".
1>Done Building Project "S:\msbuild\foo.proj" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.23Instead, you can order via DependsOn:
<Project>
<PropertyGroup>
<Condition>true</Condition>
</PropertyGroup>
<Target Name="OrderingOnly" DependsOnTargets="SetCondition;DoWork" />
<Target Name="DoWork" Condition="$(Condition) == true" />
<Target Name="SetCondition"><!-- Must run before DoWork -->
<PropertyGroup>
<Condition>false</Condition>
</PropertyGroup>
</Target>
</Project>$ msbuild -v:d .\bar.proj
Microsoft (R) Build Engine version 16.7.0-dev-20221-01+73c84c6bd for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.
Build started 4/23/2020 4:24:02 PM.
0>Process = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Current\Bin\MSBuild.exe"
MSBuild executable path = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Current\Bin\MSBuild.exe"
Command line arguments = ""C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\\MSBuild\Current\Bin\MSBuild.exe" -v:d .\bar.proj"
Current directory = "S:\msbuild"
MSBuild version = "16.7.0-dev-20221-01+73c84c6bd"
1>Project "S:\msbuild\bar.proj" on node 1 (default targets).
1>Building with tools version "Current".
1>Target "SetCondition" in project "S:\msbuild\bar.proj" (target "OrderingOnly" depends on it):
1>Done building target "SetCondition" in project "bar.proj".
Target "DoWork" skipped, due to false condition; ($(Condition) == true) was evaluated as (false == true).
1>Target "OrderingOnly" in project "S:\msbuild\bar.proj" (entry point):
1>Done building target "OrderingOnly" in project "bar.proj".
1>Done Building Project "S:\msbuild\bar.proj" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.24#Resolved
There was a problem hiding this comment.
Interesting, I did try and it seems to work as expected, but it took a few iterations/variations. Using a root target that depends on both seems better. #Resolved
| </_GeneratedAnalyzerConfigItem> | ||
|
|
||
| <!-- Transform the collected items into their full path --> | ||
| <_GeneratedAnalyzerConfigMetadata Include="@(_GeneratedAnalyzerConfigItem->'%(FullPath)')" /> |
There was a problem hiding this comment.
Is this better than extracting FullPath inside your task? #Resolved
There was a problem hiding this comment.
I was originally trying to do as much as possible outside the task, but yeah, it seems like given we're always going to run the task always anyway it would be more efficient to handle it in there. #Resolved
| </ItemGroup> | ||
|
|
||
| <!-- Transform the collected properties and items into an editor config file --> | ||
| <GenerateMSBuildAnalyzerConfig |
There was a problem hiding this comment.
I don't love that this is unconditional, but it's pretty simple and I can't think of a better way to do it. #Resolved
- Fix target ordering - Do full path in task - Add full path tests
|
@dotnet/roslyn-compiler for compiler side review please :) |
|
|
||
| namespace Microsoft.CodeAnalysis.BuildTasks | ||
| { | ||
| public class GenerateMSBuildAnalyzerConfig : Task |
There was a problem hiding this comment.
GenerateMSBuildAnalyzerConfig [](start = 17, length = 29)
I would be great to capture the documentation you wrote in OP description here. #Closed
| using Microsoft.Build.Framework; | ||
| using Microsoft.Build.Utilities; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.BuildTasks |
There was a problem hiding this comment.
namespace [](start = 0, length = 9)
nit: #nullable enable? #Closed
There was a problem hiding this comment.
Nullable is enabled at the project level for this assembly. Stylistically, are we still putting in source?
In reply to: 419572135 [](ancestors = 419572135)
There was a problem hiding this comment.
Interesting, didn't realize we'd done that. #Closed
There was a problem hiding this comment.
Project-level sounds good to me. Thanks #Closed
| builder.Append("msbuild_property.") | ||
| .Append(prop.ItemSpec) | ||
| .Append(" = ") | ||
| .AppendLine(prop.GetMetadata("Value")); //TODO: do we need to encode/escape this? |
There was a problem hiding this comment.
TODO [](start = 65, length = 4)
I agree that there is an encoding/escaping question here (and other concats below). But we don't add TODO markers in the code. Some options: (1) we add encoding, or (2) add a check (fail gracefully), or (3) create a tracking issue (link here). #Closed
There was a problem hiding this comment.
Good catch, I didn't notice that was still even in there. I'll write some tests to verify the behavior and either fix or open an issue.
In reply to: 419574615 [](ancestors = 419574615)
| Requested properties/items are requested via item groups like: | ||
|
|
||
| <AnalyzerProperty Include="PropertyNameToEval" /> | ||
| <AnalyzerItemMetadata Include="ItemType" MetadataName="MetaDataToRetrieve" /> |
There was a problem hiding this comment.
MetaData [](start = 61, length = 8)
nit: "Metadata" #Closed
|
@chsienki The editorconfig spec says that all directory separators in section names must be forward slashes. I think we should abide by that, even though using absolute paths is technically illegal, just to prevent other parsers potentially falling over. #Resolved |
|
|
||
| <Target Name="GenerateMSBuildAnalyzerConfigFileShouldRun" | ||
| BeforeTargets="GenerateMSBuildAnalyzerConfigFile" | ||
| Condition="'$(Language)'=='VB' or '$(Language)'=='C#'"> |
There was a problem hiding this comment.
Just curious, why do we have these language checks? Would F# potentially also want to benefit from generating such editorconfig files, by re-using our task and/or targets? #Closed
There was a problem hiding this comment.
Hmm, good point, I'm not sure that we actually need them.
In reply to: 419677677 [](ancestors = 419677677)
| </PropertyGroup> | ||
|
|
||
| <Target Name="GenerateMSBuildAnalyzerConfigFileShouldRun" | ||
| BeforeTargets="GenerateMSBuildAnalyzerConfigFile" |
There was a problem hiding this comment.
BeforeTargets [](start = 10, length = 13)
Sorry if this is a basic question, but do we need this BeforeTargets given that GenerateMSBuildAnalyzerConfigFile lists this target (GenerateMSBuildAnalyzerConfigFileShouldRun) in its DependsOnTargets list already? #Resolved
There was a problem hiding this comment.
Nope, just a hangover from the first commit. Fixed.
In reply to: 419679214 [](ancestors = 419679214)
| </PropertyGroup> | ||
| </Target> | ||
|
|
||
| <Target Name="GenerateMSBuildAnalyzerConfigFile" |
There was a problem hiding this comment.
GenerateMSBuildAnalyzerConfigFile [](start = 16, length = 33)
The name of this target seems confusing, since it's also used as a property above. Or is this dual meaning how things are typically done in MSBuild? #Closed
There was a problem hiding this comment.
Yeah, its weird, but there is precedent in other targets to have a target/property named the same thing.
In reply to: 419682753 [](ancestors = 419682753)
|
|
||
| <!-- Record that we'll write a file, and add it to the editorconfig inputs --> | ||
| <FileWrites Include="$(GeneratedMSBuildAnalyzerConfigFile)" /> | ||
| <EditorConfigFiles Include="$(GeneratedMSBuildAnalyzerConfigFile)" /> |
There was a problem hiding this comment.
/> [](start = 74, length = 2)
formatting nit: double space before #Closed
| DependsOnTargets="PrepareForBuild;GenerateMSBuildAnalyzerConfigFileShouldRun;GenerateMSBuildAnalyzerConfigFileCore" | ||
| Condition="'$(Language)'=='VB' or '$(Language)'=='C#'" /> | ||
|
|
||
| <Target Name="GenerateMSBuildAnalyzerConfigFileShouldRun"> |
There was a problem hiding this comment.
GenerateMSBuildAnalyzerConfigFileShouldRun [](start = 16, length = 42)
It seems we have two targets named GenerateMSBuildAnalyzerConfigFileShouldRun with similar definitions. Is that intentional? Not sure how that works #Closed
There was a problem hiding this comment.
Copy paste mistake. (the second one just overrides the first so works the same). Fixed.
In reply to: 419693689 [](ancestors = 419693689)
| [Fact] | ||
| public void DuplicateItemSpecsAreCombinedInSections() | ||
| { | ||
| TaskItem item1 = new TaskItem("c:\\file1.cs", new Dictionary<string, string> { { "ItemType", "Compile" }, { "MetadataName", "ToRetrieve" }, { "ToRetrieve", "abc123" } }); |
There was a problem hiding this comment.
MetadataName [](start = 121, length = 12)
Just to confirm, "ItemType" and "MetadataName" are handled/provided by MSBuild infrastructure, so we don't have to worry about casing variations, correct? #Closed
There was a problem hiding this comment.
| <EditorConfigFiles Include="@(PotentialEditorConfigFiles->Exists())" Condition="'$(DiscoverEditorConfigFiles)' != 'false'" /> | ||
| </ItemGroup> | ||
|
|
||
| <!-- |
There was a problem hiding this comment.
Tested with a test project, lots of combinations of values and the msbuild log viewer. Basically copy the updated targets into a directory.build.targets, and change the task assembly path to the Roslyn source version. We should definitely think about investing in some sort of structured tests for these files.
In reply to: 419698161 [](ancestors = 419698161)
jcouv
left a comment
There was a problem hiding this comment.
Done with review pass (iteration 2)
| using Microsoft.Build.Framework; | ||
| using Microsoft.Build.Utilities; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.BuildTasks |
There was a problem hiding this comment.
Interesting, didn't realize we'd done that. #Closed
|
|
||
| namespace Microsoft.CodeAnalysis.BuildTasks | ||
| { | ||
| public class GenerateMSBuildAnalyzerConfig : Task |
There was a problem hiding this comment.
| public class GenerateMSBuildAnalyzerConfig : Task | |
| public sealed class GenerateMSBuildAnalyzerConfig : Task | |
| ``` #Resolved |
| MetadataItems = new ITaskItem[0]; | ||
| PropertyItems = new ITaskItem[0]; |
There was a problem hiding this comment.
| MetadataItems = new ITaskItem[0]; | |
| PropertyItems = new ITaskItem[0]; | |
| MetadataItems = Array.Empty<ITaskItem>(); | |
| PropertyItems = Array.Empty<ITaskItem>(); | |
| ``` #Resolved |
| } | ||
|
|
||
| // group the metadata items by their full path | ||
| var groupedItems = MetadataItems.GroupBy(i => i.GetMetadata("FullPath")); |
There was a problem hiding this comment.
Is it possible for GetMetadata("FullPath") to return null?
There was a problem hiding this comment.
My understanding is that it's 'well known metadata item' (https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-well-known-item-metadata?view=vs-2019) so will always have a value. @rainersigwald to verify that though.
There is a test ItemIsNotFullyQualifiedPath that codifies what happens when there isn't a real item on disk.
In reply to: 419752014 [](ancestors = 419752014)
There was a problem hiding this comment.
Yes, follows the "you asked for meaningless garbage? I'll give you string.Empty" philosophy of MSBuild.
(It's not a good philosophy. But it's the one we have.)
| foreach (var prop in PropertyItems) | ||
| { | ||
| builder.Append("msbuild_property.") | ||
| .Append(prop.ItemSpec) |
There was a problem hiding this comment.
What does ItemSpec return in this context? #Resolved
There was a problem hiding this comment.
The name of the actual property. ItemSpec is what's in the Include attribute. We include the properties on line 103 of Microsoft.Managed.Core.targets split out by their values.
In reply to: 419752343 [](ancestors = 419752343)
|
Done. The actual parsing of these is in the other PR #42219. There we treat the sections differently if it's a global config; really we're building a new thing (a global analyzer config) that just happens to look a bit like an editor config. But I agree we should make it as interoperable as possible. In reply to: 623657653 [](ancestors = 623657653) |
| /// | ||
| /// Each of the <see cref="MetadataItems"/> will be transformed into a new section in the generated config file. The section | ||
| /// header will be the full path of the item (generated via its<see cref="ITaskItem.ItemSpec"/>), and each section will have a | ||
| /// set of <c>msbuild_metadata.<em>ItemType</em>.<em>MetadataName</em> = <em>RetreivedMetadataValue</em></c>, one per <c>ItemType</c> |
There was a problem hiding this comment.
Retreived [](start = 81, length = 9)
nit: typo "Retrieved" #Resolved
| /// in the given string with '/'. Otherwise, returns the string. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This method is equivalent to Microsoft.CodeAnalysis.BuildTasks.GenerateMSBuildAnalyzerConfig.NormalizeWithForwardSlash |
There was a problem hiding this comment.
Microsoft.CodeAnalysis.BuildTasks.GenerateMSBuildAnalyzerConfig.NormalizeWithForwardSlash [](start = 41, length = 89)
nit: consider making it a link (<see I think). Same on other end #WontFix
There was a problem hiding this comment.
You can't sadly, because they don't reference each other (why we have to copy). It issues a warning that it can't be resolved.
In reply to: 419837629 [](ancestors = 419837629)
jcouv
left a comment
There was a problem hiding this comment.
LGTM Thanks (iteration 3) with couple nits
| builder.Append("msbuild_item.") | ||
| .Append(item.GetMetadata("ItemType")) | ||
| .Append(".") | ||
| .Append(metadataName) |
There was a problem hiding this comment.
Will metadataName always be a valid identifier for a editor config value name here? #Resolved
There was a problem hiding this comment.
Yes. Editorconfig keys are opaque strings; the only disallowed characters are whitespace and '=' (see https://editorconfig-specification.readthedocs.io/en/latest/#id3).
metadataName is the value of either an xml element or attribute name (note: the name, not it's value). The XML spec disallows '=' or ' ' as identifier characters (https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Name) which means it will always be valid.
In reply to: 419844440 [](ancestors = 419844440)
| Assert.Equal(@"is_global = true | ||
|
|
||
| [c:/file1.cs] | ||
| msbuild_item.. = |
There was a problem hiding this comment.
Is this legal editor config? #Resolved
There was a problem hiding this comment.
Yes. It's just a string. It's semantically uninteresting to us, but valid.
In reply to: 419847799 [](ancestors = 419847799)
| /// The Microsoft.Managed.Core.targets calls this task with the collected results of the <c>AnalyzerProperty</c> and | ||
| /// <c>AnalyzerItemMetadata</c> item groups. | ||
| /// </remarks> | ||
| public sealed class GenerateMSBuildAnalyzerConfig : Task |
There was a problem hiding this comment.
One part I'm unclear on is what is the responsibility of this type with respect to generating a well formed / correct editorconfig file? Is the expectation that this Task is best effort, guaranteed to produce correct editor config, etc ...?
There was a problem hiding this comment.
It should be a valid config, although there is the newline bug that means the files it produces can be incorrect. Correct parsing of editorconfig is supposed to just ignore lines it can't understand though, so even then, it'll be valid you just can't read the whole property.
Want me to add something to the task description that explains it?
In reply to: 419847995 [](ancestors = 419847995)
There was a problem hiding this comment.
Possibly add a remarks section essentially noting that there will always be cases where we can produce invalid editorconfig files. We can definitely get the 99% case covered but I'm sure there are ways that developers could do evil things to produce an invalid ones.
Another question, @jasonmalinowski brought this up in chat. When the editor config is invalid how will the customer find out? Will there just be an error message in the diagonstics for example?
There was a problem hiding this comment.
An editorconfig parsed is supposed to ignore any lines it doesn't understand when parsing: I've checked and we can handle a malformed config; we just ignore anything we can't parse. Of course that isn't particularly helpful for the user to understand when things aren't working. We 'blessed' the diagnostic options as having warnings when we don't understand them, and I expect we'll do the same for the msbuild properties generated here. The task should always generate something we can recognize (msbuild_property / msbuild_metadata) so when it comes to parsing those (another PR) will probably issue warnings for things we can't understand.
In reply to: 420302735 [](ancestors = 420302735,419847995)
There was a problem hiding this comment.
An editorconfig parsed is supposed to ignore any lines it doesn't understand when parsing: I've checked and we can handle a malformed config; we just ignore anything we can't parse. Of course that isn't particularly helpful for the user to understand when things aren't working. We 'blessed' the diagnostic options as having warnings when we don't understand them, and I expect we'll do the same for the msbuild properties generated here. The task should always generate something we can recognize (msbuild_property / msbuild_metadata) so when it comes to parsing those (another PR) will probably issue warnings for things we can't understand.
In reply to: 420351311 [](ancestors = 420351311)
| <GeneratedMSBuildAnalyzerConfigFile Condition="'$(GeneratedMSBuildAnalyzerConfigFile)' ==''">$(IntermediateOutputPath)$(MSBuildProjectName).GeneratedMSBuildAnalyzerConfig.editorconfig</GeneratedMSBuildAnalyzerConfigFile> | ||
| <GenerateMSBuildAnalyzerConfigFile Condition="'$(GenerateMSBuildAnalyzerConfigFile)' == ''">true</GenerateMSBuildAnalyzerConfigFile> |
There was a problem hiding this comment.
| <GeneratedMSBuildAnalyzerConfigFile Condition="'$(GeneratedMSBuildAnalyzerConfigFile)' ==''">$(IntermediateOutputPath)$(MSBuildProjectName).GeneratedMSBuildAnalyzerConfig.editorconfig</GeneratedMSBuildAnalyzerConfigFile> | |
| <GenerateMSBuildAnalyzerConfigFile Condition="'$(GenerateMSBuildAnalyzerConfigFile)' == ''">true</GenerateMSBuildAnalyzerConfigFile> | |
| <GeneratedMSBuildAnalyzerConfigFile Condition="'$(GeneratedMSBuildAnalyzerConfigFile)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).GeneratedMSBuildAnalyzerConfig.editorconfig</GeneratedMSBuildAnalyzerConfigFile> | |
| <GenerateMSBuildAnalyzerConfigFile Condition="'$(GenerateMSBuildAnalyzerConfigFile)' == | |
| ''">true</GenerateMSBuildAnalyzerConfigFile> |
Sorry, that was driving me crazy. #Resolved
| [{Path.GetFullPath("..\\file2.cs").Replace('\\', '/')}] | ||
| msbuild_item.Compile.ToRetrieve = abc123 | ||
|
|
||
| [{Path.GetFullPath("someDir\\otherDir\\thirdDir\\..\\file3.cs").Replace('\\', '/')}] |
There was a problem hiding this comment.
How is this test functioning? I can't see where these files are written to disk hence can't see how this not throwing. #Resolved
There was a problem hiding this comment.
It's not writing anything to disk. MSBuild takes the value of the ItemSpec and appends the Working Directory to it when you ask for the full path. The tests here are just doing that same calculation so it works without having to hard code absolute paths.
I'll update the test to make it a little clearer what's going on.
In reply to: 419849389 [](ancestors = 419849389)
| /// The Microsoft.Managed.Core.targets calls this task with the collected results of the <c>AnalyzerProperty</c> and | ||
| /// <c>AnalyzerItemMetadata</c> item groups. | ||
| /// </remarks> | ||
| public sealed class GenerateMSBuildAnalyzerConfig : Task |
There was a problem hiding this comment.
Possibly add a remarks section essentially noting that there will always be cases where we can produce invalid editorconfig files. We can definitely get the 99% case covered but I'm sure there are ways that developers could do evil things to produce an invalid ones.
Another question, @jasonmalinowski brought this up in chat. When the editor config is invalid how will the customer find out? Will there just be an error message in the diagonstics for example?
* upstream/master: (495 commits) Simplify PEPropertySymbol constructor (dotnet#43990) Remove duplicate calls to ThrowIfCancellationRequested Allow msbuild to pass properties and metadata as analyzerconfig: (dotnet#43617) Use a private exception type for error types encountered instead of ArgumentException. Remove unnecessary check Code review feedback Adding tests and expanding side effect cases Expose specific members of System.[U]IntPtr from native integer types (dotnet#43766) Remove trailing space from BoundDecisionDagNode Don't cache FileInfo instances. Add 16.7 Preview 1 to the publish data config Bump prerelease version for 16.7 preview 2 Only run code style analyzers when nullable warnings are enabled Remove Code review feedback Lint Undo Fix regression in global:: qualified constant in a switch case. Fixes dotnet#43960 Undo Simplify ...
This PR aims to answer the question of how analyzers / generators get information from MSBuild.
This implements the MSBuild -> Editorconfig portion. Consuming the generated editorconfig will be done in a different PR.
The basic idea is that an analyzer/generator can declare the properties and metadata it is interested in via an item group in a targets file (added via the usual nuget mechanism):
For instance, to access the
TargetFrameworkandLangVersionproperties:Or to access the
%(AccessedTime)of@(Compile)items:AnalyzerPropertyitems end up the in the global section of the generated config. Metadata items are placed in merged sections based on the full path of theItemSpec, with a combination of the ItemType that produced it and the name of the metadata.Eg:
For a project like:
Would produce an editor config like: