Есть какие-либо приёмы запрещающие добавлять в код проекта, вызовы определенных методов? Кроме организационных, конечно.
В частности, хочу убрать возможность использовать все перегрузки System.Windows.Forms.MessageBox.Show()
не содержащие параметр IWin32Window owner
:
Show(String)
Show(String, String)
Show(String, String, MessageBoxButtons)
Show(String, String, MessageBoxButtons, MessageBoxIcon)
Show(String, String, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton)
Show(String, String, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton, MessageBoxOptions)
Show(String, String, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton, MessageBoxOptions, Boolean)
Show(String, String, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton, MessageBoxOptions, String)
Show(String, String, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton, MessageBoxOptions, String, HelpNavigator)
Show(String, String, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton, MessageBoxOptions, String, HelpNavigator, Object)
Show(String, String, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton, MessageBoxOptions, String, String)
Здесь упоминалось решение с Roslyn API, его тоже несложно сделать.
Для начала, создадим анализатор, как показано в этом ответе. Мы не можем придумать разумный code fix, поэтому у нас будет лишь анализ и подсвечивание красной волнистой линией.
Код нашего анализатора такой:
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace OverloadAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class OverloadAnalyzerAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "OverloadAnalyzer";
private static readonly string Title = "MessageBox.Show only with owner";
private static readonly string MessageFormat = "Use overload with owner";
private static readonly string Description =
"You must always specify an owner for MessageBox.Show";
private const string Category = "Framework usage";
// соберём описание нашего правила
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId, Title, MessageFormat, Category,
DiagnosticSeverity.Error, // укажен, что это ошибка, а не предупреждение
isEnabledByDefault: true, description: Description);
// список диагностик, которые мы выдаём
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);
// при инициализации подпишемся на анализ всех команд вызова функции
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeSymbol,
SyntaxKind.InvocationExpression);
}
// сам анализатор
private static void AnalyzeSymbol(SyntaxNodeAnalysisContext context)
{
// получаем синтаксический узел, приводим его к типу «вызов функции»
var invocationNode = (InvocationExpressionSyntax)context.Node;
// мелкая эвристика для ускорения: если среди имён в выражении нет Show,
// дальнейший дорогой семантический анализ не имеет смысла
if (!invocationNode.DescendantNodes()
.OfType<IdentifierNameSyntax>()
.Any(n => n.Identifier.ValueText == "Show"))
return;
// получаем символ у семантической модели
var symInfo = context.SemanticModel.GetSymbolInfo(invocationNode);
var methodSymbol = (IMethodSymbol)symInfo.Symbol;
// если его нет (например, у нас некомпилирующийся код), отваливаем
if (methodSymbol == null)
return;
// проверяем, что это действительно вызов MessageBox.Show
if (methodSymbol.ContainingAssembly.Name != "System.Windows.Forms")
return;
if (methodSymbol.ContainingType.ToDisplayString() !=
"System.Windows.Forms.MessageBox")
return;
// если в списке параметров есть параметр с типом IWin32Window, всё хорошо
if (methodSymbol.Parameters.Any(p => p.Type.Name == "IWin32Window"))
return;
// иначе говорим юзеру «атата!»
var diagnostic = Diagnostic.Create(Rule, invocationNode.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
Получаем вот такое поведение:
Если вы хотите ещё и проверять код во время построения (а то вдруг программист отключит анализатор или не использует Visual Studio?), вам нужно будет написать ещё и анализатор командной строки, аналогично тому, как описано здесь, и запускать его при построении на build-сервере (или в post-build step).
У меня получился вот такой анализатор командной строки:
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
namespace CommandLineOverloadAnalyzer
{
class Program
{
static void Usage()
{
Console.WriteLine("Usage: CommandLineOverloadAnalyzer.exe SolutionPath" +
" MSBuildPath [\"batch\"]");
Console.WriteLine("if batch specified, no extra output is done");
Console.WriteLine("if no batch specified, MSBuildPath may be omitted" +
" and will be asked for if needed");
}
static async Task<int> Main(string[] args)
{
if ((args.Length < 1 || args.Length > 3) ||
(args.Length == 3 && args[2] != "batch"))
{
Usage();
return 1;
}
var solutionPath = args[0];
var msBuildPath = (args.Length > 1) ? args[1] : null;
bool batchMode = args.Length == 3;
// найдём версию MSBuild
var visualStudioInstances =
MSBuildLocator.QueryVisualStudioInstances().ToArray();
var instance =
// указан путь, выбираем его
(msBuildPath != null) ? visualStudioInstances.SingleOrDefault(
i => i.MSBuildPath == msBuildPath) :
// только одна студия, выбираем её
visualStudioInstances.Length == 1 ? visualStudioInstances[0] :
// в пакетном режиме? падаем
batchMode ? null :
// спрашиваем у юзера
SelectVisualStudioInstance(visualStudioInstances);
if (instance == null)
{
Console.WriteLine("Cannot determine MSBuild path");
return 1;
}
void Out(string s)
{
if (!batchMode)
Console.WriteLine(s);
}
Out($"Using MSBuild at '{instance.MSBuildPath}' to load projects.");
// Экземпляр MSBuildLocator должен быть зарегистрирован
// перед MSBuildWorkspace.Create(), иначе MEF-композиция не сработает
MSBuildLocator.RegisterInstance(instance);
bool hadProblems = false;
using (var workspace = MSBuildWorkspace.Create())
{
// Напечатаем сообщение при приходе WorkspaceFailed, чтобы
// помочь с диагностикой проблем загрузки проектов
workspace.WorkspaceFailed += (o, e) => Out(e.Diagnostic.Message);
Out($"Loading solution '{solutionPath}'");
// печатаем прогресс при интерактивной загрузке
var solution = await workspace.OpenSolutionAsync(solutionPath,
batchMode ? null : new ConsoleProgressReporter());
Out($"Finished loading solution '{solutionPath}'");
foreach (var project in solution.Projects)
{
Out($"Processing project {project.Name}");
var compilation = await project.GetCompilationAsync();
// для каждого файла в проекте отдельное синтаксическое дерево
foreach (var syntaxTree in compilation.SyntaxTrees)
{
var root = await syntaxTree.GetRootAsync();
// создаём семантический анализатор
var model = compilation.GetSemanticModel(syntaxTree);
// получаем все типы переменных
foreach (var invocationNode in root.DescendantNodes()
.OfType<InvocationExpressionSyntax>())
{
if (!invocationNode.DescendantNodes()
.OfType<IdentifierNameSyntax>()
.Any(n => n.Identifier.ValueText == "Show"))
continue;
var symInfo = model.GetSymbolInfo(invocationNode);
var methodSymbol = (IMethodSymbol)symInfo.Symbol;
if (methodSymbol == null)
continue;
if (methodSymbol.ContainingAssembly.Name !=
"System.Windows.Forms")
continue;
if (methodSymbol.ContainingType.ToDisplayString() !=
"System.Windows.Forms.MessageBox")
continue;
if (methodSymbol.Parameters.Any(p => p.Type.Name ==
"IWin32Window"))
continue;
var location = invocationNode.GetLocation().GetLineSpan();
Console.WriteLine(
$"MessageBox.Show usage is wrong, location = {location}");
hadProblems = true;
}
}
}
}
return hadProblems ? 1 : 0;
}
private static VisualStudioInstance SelectVisualStudioInstance(
VisualStudioInstance[] visualStudioInstances)
{
Console.WriteLine("Multiple installs of MSBuild detected, please select one:");
for (int i = 0; i < visualStudioInstances.Length; i++)
{
Console.WriteLine($"Instance {i + 1}");
Console.WriteLine($" Name: {visualStudioInstances[i].Name}");
Console.WriteLine($" Version: {visualStudioInstances[i].Version}");
Console.WriteLine(
$" MSBuild Path: {visualStudioInstances[i].MSBuildPath}");
}
while (true)
{
var userResponse = Console.ReadLine();
if (int.TryParse(userResponse, out int instanceNumber) &&
instanceNumber > 0 &&
instanceNumber <= visualStudioInstances.Length)
{
return visualStudioInstances[instanceNumber - 1];
}
Console.WriteLine("Input not accepted, try again.");
}
}
private class ConsoleProgressReporter : IProgress<ProjectLoadProgress>
{
public void Report(ProjectLoadProgress loadProgress)
{
var projectDisplay = Path.GetFileName(loadProgress.FilePath);
if (loadProgress.TargetFramework != null)
{
projectDisplay += $" ({loadProgress.TargetFramework})";
}
Console.WriteLine(
$"{loadProgress.Operation,-15}" +
$" {loadProgress.ElapsedTime,-15:m\\:ss\\.fffffff}" +
$" {projectDisplay}");
}
}
private class SilentProgressReporter : IProgress<ProjectLoadProgress>
{
public void Report(ProjectLoadProgress loadProgress) { }
}
}
}
В Post-Build step укажите просто
"<путь к анализатору>\CommandLineOverloadAnalyzer.exe" "$(SolutionPath)" "$(MSBuildBinPath)" batch
У меня при пробном пробеге вывело:
MessageBox.Show usage is wrong, location = <полный путь>\RoslynTest\SampleApp\Program.cs: (13,12)-(13,46)
Стандартно нет. А вот например в решарпере есть External Annotations которые позволят добавить атрибуты, например Obsolete, к стороннему коду.
Можно написать свою программу для этих целей, воспользовавшись существующими наработками по парсингу MSIL-кода. Напишем вот такую программу, принимающую на вход путь к сборке, и возвращающую код 0, если она не содержит вызовов запрещенных методов, или код 1 при их наличии (код неэффективный по производительности, воспринимайте только как пример):
//Утилита для проверки сборки на наличие вызовов запрещенных методов
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.Reflection.Emit;
namespace AssValidator
{
class Program
{
public static OpCode FindOpCode(short val)
{
OpCode ret = OpCodes.Nop;
FieldInfo[] mas = typeof(OpCodes).GetFields();
for (int i = 0; i < mas.Length; i++)
{
if (mas[i].FieldType == typeof(OpCode))
{
OpCode opcode = (OpCode)mas[i].GetValue(null);
if (opcode.Value == val)
{
ret = opcode;
break;
}
}
}
return ret;
}
//получает список методов, вызываемых указанным методом
public static List<MethodBase> GetCalledMethods(MethodBase mi)
{
MethodBody mb = null;
//получаем тело метода
try
{
mb = mi.GetMethodBody();
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().ToString() + " " + ex.Message);
}
if (mb == null) return new List<MethodBase>();
//получаем IL-код
var msil = mb.GetILAsByteArray();
//получаем модуль, в котором расположен метод
var module = mi.Module;
List<MethodBase> methods = new List<MethodBase>();
short op;
int n = 0;
//парсим IL-код...
while (true)
{
if (n >= msil.Length) break;
//получаем код операции
if (msil[n] == 0xfe)
op = (short)(msil[n + 1] | 0xfe00);
else
op = (short)(msil[n]);
//найдем имя операции
OpCode opcode = FindOpCode(op);
string str = opcode.Name;
int size = 0;
//найдем размер операции
switch (opcode.OperandType)
{
case OperandType.InlineBrTarget: size = 4; break;
case OperandType.InlineField: size = 4; break;
case OperandType.InlineMethod: size = 4; break;
case OperandType.InlineSig: size = 4; break;
case OperandType.InlineTok: size = 4; break;
case OperandType.InlineType: size = 4; break;
case OperandType.InlineI: size = 4; break;
case OperandType.InlineI8: size = 8; break;
case OperandType.InlineNone: size = 0; break;
case OperandType.InlineR: size = 8; break;
case OperandType.InlineString: size = 4; break;
case OperandType.InlineSwitch: size = 4; break;
case OperandType.InlineVar: size = 2; break;
case OperandType.ShortInlineBrTarget: size = 1; break;
case OperandType.ShortInlineI: size = 1; break;
case OperandType.ShortInlineR: size = 4; break;
case OperandType.ShortInlineVar: size = 1; break;
default:
throw new Exception("Unknown operand type.");
}
size += opcode.Size;
int token = 0;
if (str == "call" || str == "callvirt")
{
//если это вызов метода, найдем токен
token = (((msil[n + 1] | (msil[n + 2] << 8)) |
(msil[n + 3] << 0x10)) | (msil[n + 4] << 0x18));
//найдем метод в модуле по токену
try
{
var method = module.ResolveMethod(token);
if (!methods.Contains(method)) methods.Add(method);
}
catch (Exception ex)
{
//MessageBox.Show(ex.ToString());
Console.WriteLine(ex.GetType().ToString() + " " + ex.Message);
}
}
n += size; //пропускаем нужное число байтов
}
return methods;
}
//получает список методов, вызываемых всеми классами в указанной сборке
public static List<MethodBase> GetCalledMethods(Assembly ass)
{
List<MethodBase> methods = new List<MethodBase>();
var types = ass.GetTypes();
StringBuilder sb = new StringBuilder();
foreach (var t in types)
{
//поиск по методам...
var mlist = t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static);
foreach (var m in mlist)
{
var arr = GetCalledMethods(m);
foreach (var x in arr)
{
if (!methods.Contains(x)) methods.Add(x);
}
}
//поиск по конструкторам...
var clist = t.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static);
foreach (var m in clist)
{
var arr = GetCalledMethods(m);
foreach (var x in arr)
{
if (!methods.Contains(x)) methods.Add(x);
}
}
}
return methods;
}
//проверяет указанную сборку на наличие вызовов запрещенных методов
static bool ValidateAssembly(string path)
{
Assembly ass = Assembly.LoadFrom(path); //загружаем сборку
var methods = GetCalledMethods(ass); //получаем все вызываемые методы
foreach (var x in methods)
{
var pars = x.GetParameters();
//вызов метода System.Windows.Forms.MessageBox.Show(string) запрещен
if (x.DeclaringType.ToString() == "System.Windows.Forms.MessageBox"
&& x.Name == "Show"
&& pars.Length == 1
&& pars[0].ParameterType.Name == "String"
)
{
Console.WriteLine("Method call not allowed: MessageBox.Show(String)");
return false;
}
}
return true; //не найдено запрещенных методов
}
//AssValidator - точка входа
static void Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("Error: too few arguments!");
Environment.Exit(0xff);
}
Console.WriteLine("Validating "+args[0]+" ...");
try
{
bool res = ValidateAssembly(args[0]);
if (!res) { Console.WriteLine("Assembly is invalid"); Environment.Exit(1); }
else { Console.WriteLine("Assembly is valid"); Environment.Exit(0); }
}
catch (Exception ex)
{
Console.WriteLine("Validation error!");
Console.WriteLine(ex.ToString());
Environment.Exit(0xff);
}
}
}
}
Соберем ее (желательно в режиме Release, так как процесс довольно тяжелый), и разместим полученный файл так, что путь к нему будет, допустим, D:\Distr\AssValidator\AssValidator.exe
.
В свойствах проекта, на вкладке "События построения", зададим событие после построения:
"D:\Distr\AssValidator\AssValidator.exe" $(TargetPath)
Теперь, при попытке собрать проект с запрещенным методом получим ошибку построения
error MSB3073: выход из команды ""D:\Distr\AssValidator\AssValidator.exe" d:\...\WindowsFormsApplication1.exe" с кодом 1.
Выглядит это как-то так:
Протестировано для .NET 4.5 / VS 2012.
Недостатки способа:
В ходе проверки сборки может быть выполнен код из нее (например, статические конструкторы). В том числе, этот код может упасть с исключением и нарушить все.
Чтобы работало для не-AnyCPU проектов, понадобится две версии проверяющей программы (32-битная и 64-битная)
Хотя при непрохождении проверки построение завершается с ошибкой, сам скомпилированный файл сборки остается. Опять же, можно создать BAT-файл, удаляющий сборку при неудачном результате проверки.
Виртуальный выделенный сервер (VDS) становится отличным выбором
У меня есть класс User в которого есть пустой конструктор и 2 public поля Id и Name:
У меня есть код синхронного чтения данных из потокаВ бесконечном цикле идет прослушка:
Задача: начиная с 27 уровня, каждый 5 раз, выводить некую реализацию, как такое реализовать ?
Имеется конвертер, который принимает некоторый объект и проанализировав его свойства возвращает объект VisiabilityОднако обновление должно...