Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ProjectModifier and remove calls to Directory.EnumerateFiles #1782

Merged
merged 7 commits into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.DotNet.MSIdentity.AuthenticationParameters;
using Microsoft.DotNet.MSIdentity.Properties;
using Microsoft.DotNet.MSIdentity.Shared;
using Microsoft.DotNet.MSIdentity.Tool;
using Microsoft.DotNet.Scaffolding.Shared.CodeModifier;
Expand All @@ -23,13 +19,11 @@ namespace Microsoft.DotNet.MSIdentity.CodeReaderWriter
internal class ProjectModifier
{
private readonly ProvisioningToolOptions _toolOptions;
private readonly ApplicationParameters _appParameters;
private readonly IConsoleLogger _consoleLogger;

public ProjectModifier(ApplicationParameters applicationParameters, ProvisioningToolOptions toolOptions, IConsoleLogger consoleLogger)
public ProjectModifier(ProvisioningToolOptions toolOptions, IConsoleLogger consoleLogger)
{
_toolOptions = toolOptions ?? throw new ArgumentNullException(nameof(toolOptions));
_appParameters = applicationParameters ?? throw new ArgumentNullException(nameof(applicationParameters));
_consoleLogger = consoleLogger ?? throw new ArgumentNullException(nameof(consoleLogger));
}

Expand Down Expand Up @@ -60,6 +54,10 @@ public async Task AddAuthCodeAsync()

//Initialize CodeAnalysis.Project wrapper
CodeAnalysis.Project project = await CodeAnalysisHelper.LoadCodeAnalysisProjectAsync(_toolOptions.ProjectFilePath);
if (project is null)
{
return;
}

var isMinimalApp = await ProjectModifierHelper.IsMinimalApp(project);
CodeChangeOptions options = new CodeChangeOptions
Expand All @@ -69,50 +67,74 @@ public async Task AddAuthCodeAsync()
IsMinimalApp = isMinimalApp
};

var filteredFiles = codeModifierConfig.Files.Where(f => ProjectModifierHelper.FilterOptions(f.Options, options));
//Go through all the files, make changes using DocumentBuilder.
var filteredFiles = codeModifierConfig.Files.Where(f => ProjectModifierHelper.FilterOptions(f.Options, options));
foreach (var file in filteredFiles)
{
await HandleCodeFileAsync(file, project, options);
}
}

private async Task HandleCodeFileAsync(CodeFile file, CodeAnalysis.Project project, CodeChangeOptions options)
private CodeModifierConfig? GetCodeModifierConfig()
{
if (string.IsNullOrEmpty(file.FileName))
if (string.IsNullOrEmpty(_toolOptions.ProjectType))
{
return;
return null;
}

if (!string.IsNullOrEmpty(file.AddFilePath))
var propertyInfo = AppProvisioningTool.Properties.Where(
p => p.Name.StartsWith("cm") && p.Name.Contains(_toolOptions.ProjectType)).FirstOrDefault();
if (propertyInfo is null)
{
AddFile(file);
return;
return null;
}

var fileName = file.FileName.Equals("Startup.cs", StringComparison.OrdinalIgnoreCase)
? await ProjectModifierHelper.GetStartupClass(_toolOptions.ProjectPath, project)
: file.FileName;
byte[] content = (propertyInfo.GetValue(null) as byte[])!;
CodeModifierConfig? codeModifierConfig = ReadCodeModifierConfigFromFileContent(content);
if (codeModifierConfig is null)
{
throw new FormatException($"Resource file { propertyInfo.Name } could not be parsed. ");
}

if (fileName.Equals("Program.cs"))
if (!codeModifierConfig.Identifier.Equals(_toolOptions.ProjectTypeIdentifier, StringComparison.OrdinalIgnoreCase))
{
// only modify Program.cs file if it's a minimal hosting app (as we made changes to the Startup.cs file).
if (options.IsMinimalApp)
{
await ModifyProgramCs(file, project, options);
}
return null;
}

return codeModifierConfig;
}

private CodeModifierConfig? ReadCodeModifierConfigFromFileContent(byte[] fileContent)
{
try
{
string jsonText = Encoding.UTF8.GetString(fileContent);
return JsonSerializer.Deserialize<CodeModifierConfig>(jsonText);
}
else if (fileName.EndsWith(".cs"))
catch (Exception e)
{
await ModifyCsFile(fileName, file, project, options);
_consoleLogger.LogMessage($"Error parsing Code Modifier Config for project type { _toolOptions.ProjectType }, exception: { e.Message }");
return null;
}
else if (fileName.EndsWith(".cshtml"))
}

private async Task HandleCodeFileAsync(CodeFile file, CodeAnalysis.Project project, CodeChangeOptions options)
{
if (!string.IsNullOrEmpty(file.AddFilePath))
{
await ModifyCshtmlFile(fileName, file, project, options);
AddFile(file);
}
else if (fileName.EndsWith(".razor"))
else if (file.FileName.EndsWith(".cs"))
{
await ModifyRazorFile(fileName, file, project, options);
await ModifyCsFile(file, project, options);
}
else if (file.FileName.EndsWith(".cshtml"))
{
await ModifyCshtmlFile(file, project, options);
}
else if (file.FileName.EndsWith(".razor"))
{
await ModifyRazorFile(file, project, options);
}
}

Expand All @@ -132,7 +154,7 @@ private void AddFile(CodeFile file)
}

// Resource names for addFiles prefixed with "add" and contain '_' in place of '.'
// fileName: "ShowProfile.razor" -> resourceName: "add_ShowProfile_razor"
// e.g. fileName: "ShowProfile.razor" -> resourceName: "add_ShowProfile_razor"
var resourceName = file.FileName.Replace('.', '_');
var propertyInfo = AppProvisioningTool.Properties.Where(
p => p.Name.StartsWith("add") && p.Name.EndsWith(resourceName)).FirstOrDefault();
Expand All @@ -157,107 +179,66 @@ private void AddFile(CodeFile file)
}
}

internal async Task ModifyRazorFile(string fileName, CodeFile file, CodeAnalysis.Project project, CodeChangeOptions toolOptions)
internal async Task ModifyCsFile(CodeFile file, CodeAnalysis.Project project, CodeChangeOptions options)
{
var document = project.Documents.Where(d => d.Name.EndsWith(fileName)).FirstOrDefault();
if (document is null)
if (file.FileName.Equals("Startup.cs"))
{
// Startup class file name may be different
file.FileName = await ProjectModifierHelper.GetStartupClass(project) ?? file.FileName;
}

var fileDoc = project.Documents.Where(d => d.Name.Equals(file.FileName)).FirstOrDefault();
if (fileDoc is null || string.IsNullOrEmpty(fileDoc.FilePath))
{
return;
}

var razorChanges = file?.RazorChanges.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, toolOptions));
var editedDocument = await ProjectModifierHelper.ModifyDocumentText(document, razorChanges);
await ProjectModifierHelper.UpdateDocument(editedDocument, _consoleLogger);
}
//get the file document to get the document root for editing.
DocumentEditor documentEditor = await DocumentEditor.CreateAsync(fileDoc);
if (documentEditor is null)
{
return;
}

internal async Task ModifyCshtmlFile(string fileName, CodeFile cshtmlFile, CodeAnalysis.Project project, CodeChangeOptions options)
{
string? filePath = Directory.EnumerateFiles(_toolOptions.ProjectPath, fileName, SearchOption.AllDirectories).FirstOrDefault();
DocumentBuilder documentBuilder = new DocumentBuilder(documentEditor, file, _consoleLogger);
var modifiedRoot = ModifyRoot(documentBuilder, options, file);

if (!string.IsNullOrEmpty(filePath))
if (modifiedRoot != null)
{
var fileDoc = project.Documents.Where(d => d.Name.Equals(filePath)).FirstOrDefault();
if (fileDoc != null)
{
//add code snippets/changes.
if (cshtmlFile.Methods != null && cshtmlFile.Methods.Any())
{
var globalChanges = cshtmlFile.Methods.TryGetValue("Global", out var globalMethod);
if (globalMethod != null)
{
var filteredCodeFiles = globalMethod.CodeChanges.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, options));
var editedDocument = await ProjectModifierHelper.ModifyDocumentText(fileDoc, filteredCodeFiles);
//replace the document
await ProjectModifierHelper.UpdateDocument(editedDocument, _consoleLogger);
}
}
}
documentEditor.ReplaceNode(documentEditor.OriginalRoot, modifiedRoot);
}

await documentBuilder.WriteToClassFileAsync(fileDoc.FilePath);
}

/// <summary>
/// Modify Program.cs file to add changes specified in the CodeFile.
/// </summary>
/// <param name="programCsFile"></param>
/// <param name="project"></param>
/// <returns></returns>
internal async Task ModifyProgramCs(CodeFile programCsFile, CodeAnalysis.Project project, CodeChangeOptions toolOptions)
private static CompilationUnitSyntax? ModifyRoot(DocumentBuilder documentBuilder, CodeChangeOptions options, CodeFile file)
{
string? programCsFilePath = Directory.EnumerateFiles(_toolOptions.ProjectPath, "Program.cs", SearchOption.AllDirectories).FirstOrDefault();
if (!string.IsNullOrEmpty(programCsFilePath))
var newRoot = documentBuilder.AddUsings(options);
if (file.FileName.Equals("Program.cs"))
{
var programDocument = project.Documents.Where(d => d.Name.Equals(programCsFilePath)).FirstOrDefault();
DocumentEditor documentEditor = await DocumentEditor.CreateAsync(programDocument);
DocumentBuilder documentBuilder = new DocumentBuilder(documentEditor, programCsFile, _consoleLogger);
if (documentEditor.OriginalRoot is CompilationUnitSyntax docRoot && programDocument != null)
var variableDict = ProjectModifierHelper.GetBuilderVariableIdentifier(newRoot.Members);
if (file.Methods.TryGetValue("Global", out var globalMethod))
{
//get builder variable
var variableDict = ProjectModifierHelper.GetBuilderVariableIdentifier(docRoot.Members);
//add usings
var newRoot = documentBuilder.AddUsings(toolOptions);
//add code snippets/changes.
if (programCsFile.Methods != null && programCsFile.Methods.Any())
var filteredChanges = globalMethod.CodeChanges.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, options));
foreach (var change in filteredChanges)
{
var globalChanges = programCsFile.Methods.TryGetValue("Global", out var globalMethod);
if (globalMethod != null)
{
var filteredCodeFiles = globalMethod.CodeChanges.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, toolOptions));
foreach (var change in filteredCodeFiles)
{
//Modify CodeSnippet to have correct variable identifiers present.
var formattedChange = ProjectModifierHelper.FormatCodeSnippet(change, variableDict);
newRoot = DocumentBuilder.AddGlobalStatements(formattedChange, newRoot);
}
}
//Modify CodeSnippet to have correct variable identifiers present.
var formattedChange = ProjectModifierHelper.FormatCodeSnippet(change, variableDict);
newRoot = DocumentBuilder.AddGlobalStatements(formattedChange, newRoot);
}
//replace root node with all the updates.
documentEditor.ReplaceNode(docRoot, newRoot);
//write to Program.cs file
await documentBuilder.WriteToClassFileAsync(programCsFilePath);
}
}
}

internal async Task ModifyCsFile(string fileName, CodeFile file, CodeAnalysis.Project project, CodeChangeOptions options)
{
string className = ProjectModifierHelper.GetClassName(fileName);
string? filePath = Directory.EnumerateFiles(_toolOptions.ProjectPath, fileName, SearchOption.AllDirectories).FirstOrDefault();
//get the file document to get the document root for editing.

if (!string.IsNullOrEmpty(filePath))
else
{
var fileDoc = project.Documents.Where(d => d.Name.Equals(filePath)).FirstOrDefault();
DocumentEditor documentEditor = await DocumentEditor.CreateAsync(fileDoc);
DocumentBuilder documentBuilder = new DocumentBuilder(documentEditor, file, _consoleLogger);
var newRoot = documentBuilder.AddUsings(options);
//adding usings
var namespaceNode = newRoot?.Members.OfType<NamespaceDeclarationSyntax>()?.FirstOrDefault();
FileScopedNamespaceDeclarationSyntax? fileScopedNamespace = null;
if (namespaceNode == null)
if (namespaceNode is null)
{
fileScopedNamespace = newRoot?.Members.OfType<FileScopedNamespaceDeclarationSyntax>()?.FirstOrDefault();
}

string className = ProjectModifierHelper.GetClassName(file.FileName);
//get classNode. All class changes are done on the ClassDeclarationSyntax and then that node is replaced using documentEditor.
var classNode =
namespaceNode?.DescendantNodes()?.Where(node =>
Expand Down Expand Up @@ -288,60 +269,40 @@ node is ClassDeclarationSyntax cds &&
newRoot = newRoot.ReplaceNode(classDeclarationSyntax, modifiedClassDeclarationSyntax);
#pragma warning restore CS8631 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match constraint type.
}

if (documentEditor.OriginalRoot is CompilationUnitSyntax docRoot && newRoot != null)
{
documentEditor.ReplaceNode(docRoot, newRoot);
}

await documentBuilder.WriteToClassFileAsync(filePath);
}

return newRoot;
}

private CodeModifierConfig? GetCodeModifierConfig()
internal async Task ModifyCshtmlFile(CodeFile file, CodeAnalysis.Project project, CodeChangeOptions options)
{
List<CodeModifierConfig> codeModifierConfigs = new List<CodeModifierConfig>();
if (!string.IsNullOrEmpty(_toolOptions.ProjectType))
var fileDoc = project.Documents.Where(d => d.Name.EndsWith(file.FileName)).FirstOrDefault();
if (fileDoc is null || file.Methods is null || !file.Methods.Any())
{
var properties = typeof(Resources).GetProperties(BindingFlags.Static | BindingFlags.NonPublic)
.Where(p => p.PropertyType == typeof(byte[]))
.ToArray();

foreach (PropertyInfo propertyInfo in properties)
{
if (propertyInfo.Name.StartsWith("cm") && propertyInfo.Name.Contains(_toolOptions.ProjectType))
{
byte[] content = (propertyInfo.GetValue(null) as byte[])!;
CodeModifierConfig? projectDescription = ReadCodeModifierConfigFromFileContent(content);

if (projectDescription == null)
{
throw new FormatException($"Resource file { propertyInfo.Name } could not be parsed. ");
}
codeModifierConfigs.Add(projectDescription);
}
}
return;
}

var codeModifierConfig = codeModifierConfigs
.Where(x => x.Identifier != null &&
x.Identifier.Equals(_toolOptions.ProjectTypeIdentifier, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();

// CodeModifierConfig, .csproj path cannot be null
if (codeModifierConfig != null &&
codeModifierConfig.Files != null &&
codeModifierConfig.Files.Any())
//add code snippets/changes.
if (file.Methods.TryGetValue("Global", out var globalMethod))
{
return codeModifierConfig;
var filteredCodeChanges = globalMethod.CodeChanges.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, options));
var editedDocument = await ProjectModifierHelper.ModifyDocumentText(fileDoc, filteredCodeChanges);
//replace the document
await ProjectModifierHelper.UpdateDocument(editedDocument, _consoleLogger);
}
return null;
}

private CodeModifierConfig? ReadCodeModifierConfigFromFileContent(byte[] fileContent)
internal async Task ModifyRazorFile(CodeFile file, CodeAnalysis.Project project, CodeChangeOptions toolOptions)
{
string jsonText = Encoding.UTF8.GetString(fileContent);
return JsonSerializer.Deserialize<CodeModifierConfig>(jsonText);
var document = project.Documents.Where(d => d.Name.EndsWith(file.FileName)).FirstOrDefault();
if (document is null)
{
return;
}

var razorChanges = file.RazorChanges.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, toolOptions));
var editedDocument = await ProjectModifierHelper.ModifyDocumentText(document, razorChanges);
await ProjectModifierHelper.UpdateDocument(editedDocument, _consoleLogger);
}
}
}
Loading