using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using MonoDevelop.Core;
using MonoDevelop.Ide;
using MonoDevelop.Ide.TypeSystem;
using Xwt.Drawing;
namespace MonoDevelop.ConnectedServices
{
///
/// A service dependency that represents some code that is added to the project
///
public abstract class CodeDependency : ConnectedServiceDependency
{
///
/// Sets the default timeout for obtaining the compilcation for a project
///
public static int DefaultCompilationTimeout = 5000;
///
/// Determines how many times we should attempt to apply the code dependency if we get a version mismatch
///
const int RetryCount = 3;
readonly Dictionary lookupTypes;
IList allTypes;
IList sourceTypes;
Image icon;
protected CodeDependency (IConnectedService service, string displayName, string [] lookupTypes) : base (service, ConnectedServiceDependency.CodeDependencyCategory, displayName)
{
this.lookupTypes = new Dictionary ();
foreach (var type in lookupTypes) {
this.lookupTypes [type] = null;
}
}
public override Image Icon {
get {
if (icon == null)
icon = ImageService.GetIcon ("md-code");
return icon;
}
}
///
/// Returns a that represents the current .
///
public override string ToString ()
{
return this.GetType ().ToString ();
}
///
/// Gets the types defined in source files for the compilation
///
protected IList SourceTypes {
get {
if (this.sourceTypes == null) {
this.sourceTypes = this.allTypes.Where (t => t.IsDefinedInSource ()).ToList ();
}
return this.sourceTypes;
}
}
///
/// Adds the dependency to the project and returns true if the dependency was added to the project
///
protected sealed override async Task OnAddToProject (CancellationToken token)
{
int tryCount = 1;
bool keepTrying = true;
while (keepTrying) {
try {
if (tryCount > 1) {
LoggingService.LogInfo ("Retrying to add code dependency...");
} else {
LoggingService.LogInfo ("Adding code dependency '{0}' to '{1}'...", this, this.Service.Project.Name);
}
var compilation = await IdeApp.TypeSystemService.GetCompilationAsync (this.Service.Project).ConfigureAwait (false);
if (compilation == null) {
LoggingService.LogInternalError ("Could not get compilation object.", null);
return false;
}
this.InitLookupTypes (compilation, token, this.lookupTypes.Keys.ToArray ());
var result = await Runtime.RunInMainThread (
() => this.AddCodeToProject (token)
);
LoggingService.LogInfo (result ? "Code dependency added." : "Code dependency was not added.");
return result;
} catch (SolutionVersionMismatchException) {
tryCount++;
}
keepTrying = tryCount <= 3;
}
return false;
}
///
/// Removes the dependency from the project and returns true if the dependency was removed
///
protected sealed override async Task OnRemoveFromProject (CancellationToken token)
{
int tryCount = 1;
bool keepTrying = true;
while (keepTrying) {
try {
if (tryCount > 1) {
LoggingService.LogInfo ("Retrying to remove code dependency...");
} else {
LoggingService.LogInfo ("Removing code dependency '{0}' from '{1}'...", this, this.Service.Project.Name);
}
var compilation = await IdeApp.TypeSystemService.GetCompilationAsync (this.Service.Project).ConfigureAwait (false);
if (compilation == null) {
LoggingService.LogInternalError ("Could not get compilation object.", null);
return false;
}
this.InitLookupTypes (compilation, token, this.lookupTypes.Keys.ToArray ());
var result = await this.RemoveCodeFromProject (token).ConfigureAwait (false);
LoggingService.LogInfo (result ? "Code dependency removed." : "Code dependency was not removed.");
return result;
} catch (SolutionVersionMismatchException) {
tryCount++;
}
keepTrying = tryCount <= 3;
}
return false;
}
///
/// Performs the task of adding the code to the project
///
protected abstract Task AddCodeToProject (CancellationToken token);
///
/// Performs the task of removing the code from the project. By default we do not assume that code can be removed from
/// project correctly. Therefore the default implementation of this is to simply return true and to do nothing to
/// the project.
///
protected virtual Task RemoveCodeFromProject (CancellationToken token)
{
return Task.FromResult (true);
}
///
/// Updates the given method region with the code required for this dependency.
///
protected virtual void UpdateMethodWithCodeDependency (Location methodRegion)
{
if (methodRegion == null)
return;
var sourceTree = methodRegion.SourceTree;
if (sourceTree == null)
return;
var proj = this.Service.Project.GetCodeAnalysisProject ();
if (proj == null) {
// this can happen if the TypeSystemService doesn't have the project in it (yet?)
LoggingService.LogWarning ("Could not get CodeAnalysisProject for the given project, cannot modify code.");
return;
}
var docID = proj.GetDocumentId (sourceTree);
var root = sourceTree.GetRoot ();
var methodNode = root.FindNode (methodRegion.SourceSpan) as MethodDeclarationSyntax;
if (methodNode == null)
return;
var newMethodNode = this.RemoveExistingCodeDependencyFromMethod (methodNode);
var newMethodStatements = newMethodNode.Body.Statements;
var codeStatements = this.CreateCodeDependencyStatements ();
for (int i = 0; i < codeStatements.Count; i++) {
newMethodStatements = newMethodStatements.Insert (i, codeStatements [i]);
}
newMethodNode = newMethodNode.WithBody (newMethodNode.Body.WithStatements (newMethodStatements));
if (newMethodNode != methodNode) {
var newRoot = root.ReplaceNode (methodNode, newMethodNode);
newRoot = Formatter.Format (newRoot, proj.Solution.Workspace);
var newSolution = proj.Solution.WithDocumentSyntaxRoot (docID, newRoot);
if (!proj.Solution.Workspace.TryApplyChanges (newSolution)) {
LoggingService.LogWarning ("Failed to add code dependency changes to the workspace.");
// lets check the version (which is one reason why TryApplyChanges will return false
if (proj.Solution.Workspace.CurrentSolution.Version != newSolution.Version) {
LoggingService.LogWarning ("Solution version is different.");
throw new SolutionVersionMismatchException ();
}
}
}
}
///
/// Adds a method created by newMethod to the param name="classRegion", the newMethod should contain the required code dependency
///
protected void AddMethodWithCodeDependencyToClass (Location classRegion, Func newMethod)
{
if (classRegion == null)
return;
var sourceTree = classRegion.SourceTree;
if (sourceTree == null)
return;
var proj = this.Service.Project.GetCodeAnalysisProject ();
var docID = proj.GetDocumentId (sourceTree);
var root = sourceTree.GetRoot ();
var classNode = root.FindNode (classRegion.SourceSpan) as ClassDeclarationSyntax;
if (classNode == null)
return;
var newClassNode = classNode;
newClassNode = newClassNode.WithMembers (newClassNode.Members.Add (newMethod ()));
if (newClassNode != classNode) {
var newRoot = root.ReplaceNode (classNode, newClassNode);
newRoot = Formatter.Format (newRoot, proj.Solution.Workspace);
var newSolution = proj.Solution.WithDocumentSyntaxRoot (docID, newRoot);
if (!proj.Solution.Workspace.TryApplyChanges (newSolution)) {
LoggingService.LogWarning ("Failed to add code dependency changes to the workspace.");
// lets check the version (which is one reason why TryApplyChanges will return false
if (proj.Solution.Workspace.CurrentSolution.Version != newSolution.Version) {
LoggingService.LogWarning ("Solution version is different.");
throw new SolutionVersionMismatchException ();
}
}
}
}
///
/// Create the code statements that are required for this code dependency
///
protected virtual SyntaxList CreateCodeDependencyStatements ()
{
return new SyntaxList ();
}
///
/// Removes any existing code that was added previously for this dependency
///
protected virtual MethodDeclarationSyntax RemoveExistingCodeDependencyFromMethod (MethodDeclarationSyntax method)
{
var statements = method.Body.Statements;
var newStatements = statements;
foreach (var statement in statements) {
if (this.IsCodeDependencyStatement (statement)) {
newStatements = newStatements.Remove (statement);
}
}
return method.WithBody (method.Body.WithStatements (newStatements));
}
///
/// Returns true if the given statement is (or is part of) the code dependency.
/// Override this to be able to update added code.
///
protected virtual bool IsCodeDependencyStatement (StatementSyntax statement)
{
return false;
}
///
/// Searches the type lookup tabvle for the given type
///
protected INamedTypeSymbol GetLookupType (string typeName)
{
if (this.lookupTypes.ContainsKey (typeName)) {
return this.lookupTypes [typeName];
}
return null;
}
///
/// Returns true if the given type is a derived class of 'param name="class"' and has an attribute of type 'param name="attributeType"' applied
///
protected bool IsAttributedSubclass (INamedTypeSymbol type, string classType, string attributeType)
{
return type.IsAttributedSubclass (this.GetLookupType (classType), this.GetLookupType (attributeType));
}
///
/// Returns true if the given type has an attribute of type 'param name="attributeType"' applied
///
protected bool IsAttributed (INamedTypeSymbol type, string attributeType)
{
return type.IsAttributed (this.GetLookupType (attributeType));
}
///
/// Returns the list of members for a type that are defined in source.
///
protected IList GetMembersDefinedInSource (INamedTypeSymbol type)
{
return type.GetMembers ().Where (m => m.IsDefinedInSource ()).ToList ();
}
///
/// Initializes the type lookup table from the compilation.
///
void InitLookupTypes (Compilation compilation, CancellationToken cancel, string [] types)
{
this.allTypes = compilation.GetAllTypesInMainAssembly (cancel).ToList ();
this.lookupTypes.Clear ();
foreach (var type in types) {
this.lookupTypes [type] = compilation.GetTypeByMetadataName (type);
}
}
///
/// Thrown when we should attempt to generate the code dependency again
///
class SolutionVersionMismatchException : Exception
{
public SolutionVersionMismatchException () : base ("The Solution versions were different.")
{
}
}
}
}