Skip to content
This repository has been archived by the owner on Feb 25, 2021. It is now read-only.

Commit

Permalink
Encourage encapsulation of component parameter properties (#713)
Browse files Browse the repository at this point in the history
* Before refactoring ParameterCollection assignment logic, add more test coverage

* Begin caching parameter assignment info

* Factor out some reflection code to a reusable location

* Use IPropertySetter to avoid all per-property-assignment reflection

* More error cases and tests for parameter assignment

* Enable binding to nonpublic properties

* Add analyzer to warn and provide fix for public component parameters

* Unit test for analyzer

* Component tag helper now includes private properties if they have [Parameter]

* CR feedback: Remove garbage from csproj

* CR feedback: Rename .Build.Analyzers to .Analyzers

* CR feedback: Move BlazorApi.cs to shared; use it from Analyzers test

* Fix incorrect test name

* Make as many parameters private as possible. Replace ILayoutComponent with BlazorLayoutComponent.

* In component tag helper discovery, consider private members too

* Reduce the work in component parameter discovery by not inspecting the BlazorComponent base class (or System.Object)
  • Loading branch information
SteveSandersonMS committed May 1, 2018
1 parent a178cd4 commit e90432c
Show file tree
Hide file tree
Showing 60 changed files with 2,004 additions and 239 deletions.
22 changes: 22 additions & 0 deletions Blazor.sln
Expand Up @@ -95,6 +95,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorLibrary-CSharp", "src\Microsoft.AspNetCore.Blazor.Templates\content\BlazorLibrary-CSharp\BlazorLibrary-CSharp.csproj", "{3A457B14-D91B-4FFF-A81A-8F350BDB911F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers", "src\Microsoft.AspNetCore.Blazor.Analyzers\Microsoft.AspNetCore.Blazor.Analyzers.csproj", "{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers.Test", "test\Microsoft.AspNetCore.Blazor.Analyzers.Test\Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj", "{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -330,6 +334,22 @@ Global
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Release|Any CPU.Build.0 = Release|Any CPU
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Release|Any CPU.Build.0 = Release|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -372,6 +392,8 @@ Global
{C57382BC-EE93-49D5-BC40-5C98AF8AA048} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
{50F6820F-D058-4E68-9E15-801F893F514E} = {36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}
{3A457B14-D91B-4FFF-A81A-8F350BDB911F} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
Expand Down
7 changes: 1 addition & 6 deletions samples/StandaloneApp/Shared/MainLayout.cshtml
@@ -1,4 +1,4 @@
@implements ILayoutComponent
@inherits BlazorLayoutComponent

<div class="sidebar">
<NavMenu />
Expand All @@ -13,8 +13,3 @@
@Body
</div>
</div>

@functions {
[Parameter]
public RenderFragment Body { get; set; }
}
@@ -0,0 +1,66 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Linq;

namespace Microsoft.AspNetCore.Blazor.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ComponentParametersShouldNotBePublicAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "BL9993";
private const string Category = "Encapsulation";

private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Title), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Format), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Description), Resources.ResourceManager, typeof(Resources));
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.PropertyDeclaration);
}

private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
var semanticModel = context.SemanticModel;
var declaration = (PropertyDeclarationSyntax)context.Node;

var parameterAttribute = declaration.AttributeLists
.SelectMany(list => list.Attributes)
.Where(attr => semanticModel.GetTypeInfo(attr).Type?.ToDisplayString() == BlazorApi.ParameterAttribute.FullTypeName)
.FirstOrDefault();

if (parameterAttribute != null && IsPublic(declaration))
{
var identifierText = declaration.Identifier.Text;
if (!string.IsNullOrEmpty(identifierText))
{
context.ReportDiagnostic(Diagnostic.Create(
Rule,
declaration.GetLocation(),
identifierText));
}
}
}

private static bool IsPublic(PropertyDeclarationSyntax declaration)
=> declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword));
}
}
@@ -0,0 +1,73 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Blazor.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ComponentParametersShouldNotBePublicCodeFixProvider)), Shared]
public class ComponentParametersShouldNotBePublicCodeFixProvider : CodeFixProvider
{
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_FixTitle), Resources.ResourceManager, typeof(Resources));

public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(ComponentParametersShouldNotBePublicAnalyzer.DiagnosticId);

public sealed override FixAllProvider GetFixAllProvider()
{
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;

// Find the type declaration identified by the diagnostic.
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().First();

// Register a code action that will invoke the fix.
var title = Title.ToString();
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => GetTransformedDocumentAsync(context.Document, root, declaration),
equivalenceKey: title),
diagnostic);
}

private Task<Document> GetTransformedDocumentAsync(
Document document,
SyntaxNode root,
PropertyDeclarationSyntax declarationNode)
{
var updatedDeclarationNode = HandlePropertyDeclaration(declarationNode);
var newSyntaxRoot = root.ReplaceNode(declarationNode, updatedDeclarationNode);
return Task.FromResult(document.WithSyntaxRoot(newSyntaxRoot));
}

private SyntaxNode HandlePropertyDeclaration(PropertyDeclarationSyntax node)
{
TypeSyntax type = node.Type;
if (type == null || type.IsMissing)
{
return null;
}

var publicModifier = node.Modifiers.FirstOrDefault(m => m.IsKind(SyntaxKind.PublicKeyword));
node = node.WithModifiers(
node.Modifiers.Remove(publicModifier));
return node;
}
}
}
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard1.3</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<NoPackageAnalysis>true</NoPackageAnalysis>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.4.0" PrivateAssets="all" />
<PackageReference Update="NETStandard.Library" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>

<ItemGroup>
<Compile Update="Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>

<EmbeddedResource Update="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<Compile Include="..\shared\BlazorApi.cs" Link="shared\%(Filename)%(Extension)" />
</ItemGroup>

</Project>
100 changes: 100 additions & 0 deletions src/Microsoft.AspNetCore.Blazor.Analyzers/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit e90432c

Please sign in to comment.