mirror of
https://github.com/github/codeql.git
synced 2026-04-29 10:45:15 +02:00
Merge pull request #1174 from calumgrant/cs/extractor-diagnostics
C#: Log compiler and extractor diagnostics
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Globalization;
|
||||
using System.Collections.Generic;
|
||||
using Semmle.Util.Logging;
|
||||
using System;
|
||||
using Semmle.Extraction.Entities;
|
||||
|
||||
namespace Semmle.Extraction.CIL.Entities
|
||||
{
|
||||
@@ -70,12 +71,7 @@ namespace Semmle.Extraction.CIL.Entities
|
||||
}
|
||||
catch (InternalError e)
|
||||
{
|
||||
cx.cx.Extractor.Message(new Message
|
||||
{
|
||||
exception = e,
|
||||
message = "Error processing type definition",
|
||||
severity = Semmle.Util.Logging.Severity.Error
|
||||
});
|
||||
cx.cx.ExtractionError("Error processing type definition", e.Message, GeneratedLocation.Create(cx.cx), e.StackTrace);
|
||||
}
|
||||
|
||||
// Limitation of C#: Cannot yield return inside a try-catch.
|
||||
@@ -92,12 +88,7 @@ namespace Semmle.Extraction.CIL.Entities
|
||||
}
|
||||
catch (InternalError e)
|
||||
{
|
||||
cx.cx.Extractor.Message(new Message
|
||||
{
|
||||
exception = e,
|
||||
message = "Error processing bytecode",
|
||||
severity = Semmle.Util.Logging.Severity.Error
|
||||
});
|
||||
cx.cx.ExtractionError("Error processing bytecode", e.Message, GeneratedLocation.Create(cx.cx), e.StackTrace);
|
||||
}
|
||||
|
||||
if (product != null)
|
||||
@@ -139,7 +130,7 @@ namespace Semmle.Extraction.CIL.Entities
|
||||
trapFile = trapWriter.TrapFile;
|
||||
if (nocache || !System.IO.File.Exists(trapFile))
|
||||
{
|
||||
var cx = new Extraction.Context(extractor, null, trapWriter, null);
|
||||
var cx = extractor.CreateContext(null, trapWriter, null);
|
||||
ExtractCIL(cx, assemblyPath, extractPdbs);
|
||||
extracted = true;
|
||||
}
|
||||
|
||||
@@ -409,7 +409,7 @@ namespace Semmle.Extraction.CIL.Entities
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InternalError("Unable to create payload type {0} for opcode {1}", PayloadType, OpCode);
|
||||
throw new InternalError($"Unable to create payload type {PayloadType} for opcode {OpCode}");
|
||||
}
|
||||
break;
|
||||
case Payload.Arg8:
|
||||
@@ -430,7 +430,7 @@ namespace Semmle.Extraction.CIL.Entities
|
||||
// Some of these are handled by JumpContents().
|
||||
break;
|
||||
default:
|
||||
throw new InternalError("Unhandled payload type {0}", PayloadType);
|
||||
throw new InternalError($"Unhandled payload type {PayloadType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,7 +479,7 @@ namespace Semmle.Extraction.CIL.Entities
|
||||
// TODO: Find a solution to this.
|
||||
|
||||
// For now, just log the error
|
||||
cx.cx.Extractor.Message(new Message { message = "A CIL instruction jumps outside the current method", severity = Util.Logging.Severity.Warning });
|
||||
cx.cx.ExtractionError("A CIL instruction jumps outside the current method", "", Extraction.Entities.GeneratedLocation.Create(cx.cx), "", Util.Logging.Severity.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +497,7 @@ namespace Semmle.Extraction.CIL.Entities
|
||||
constructedTypeSignature = md.DecodeSignature(cx.TypeSignatureDecoder, this);
|
||||
break;
|
||||
default:
|
||||
throw new InternalError("Unexpected constructed method handle kind {0}", ms.Method.Kind);
|
||||
throw new InternalError($"Unexpected constructed method handle kind {ms.Method.Kind}");
|
||||
}
|
||||
|
||||
PopulateParameters(constructedTypeSignature.ParameterTypes);
|
||||
|
||||
@@ -197,7 +197,7 @@ namespace Semmle.Extraction.CIL
|
||||
case PrimitiveTypeCode.UIntPtr: return uintptrId;
|
||||
case PrimitiveTypeCode.Void: return voidId;
|
||||
case PrimitiveTypeCode.TypedReference: return typedReferenceId;
|
||||
default: throw new InternalError("Unhandled type code {0}", typeCode);
|
||||
default: throw new InternalError($"Unhandled type code {typeCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,12 +99,7 @@ namespace Semmle.Extraction.CSharp
|
||||
}
|
||||
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
|
||||
{
|
||||
extractor.Message(new Message
|
||||
{
|
||||
exception = ex,
|
||||
message = string.Format("Exception reading reference file {0}: {1}",
|
||||
reference.FilePath, ex)
|
||||
});
|
||||
extractor.Message(new Message("Exception reading reference file", reference.FilePath, null, ex.StackTrace));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +210,38 @@ namespace Semmle.Extraction.CSharp
|
||||
return options.Cache && FileIsUpToDate(src, dest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts compilation-wide entities, such as compilations and compiler diagnostics.
|
||||
/// </summary>
|
||||
public void AnalyseCompilation(string cwd, string[] args)
|
||||
{
|
||||
extractionTasks.Add(() => DoAnalyseCompilation(cwd, args));
|
||||
}
|
||||
|
||||
Entities.Compilation compilationEntity;
|
||||
IDisposable compilationTrapFile;
|
||||
|
||||
void DoAnalyseCompilation(string cwd, string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assemblyPath = extractor.OutputPath;
|
||||
var assembly = compilation.Assembly;
|
||||
var projectLayout = layout.LookupProjectOrDefault(assemblyPath);
|
||||
var trapWriter = projectLayout.CreateTrapWriter(Logger, assemblyPath, true);
|
||||
compilationTrapFile = trapWriter; // Dispose later
|
||||
var cx = extractor.CreateContext(compilation.Clone(), trapWriter, new AssemblyScope(assembly, assemblyPath, true));
|
||||
|
||||
compilationEntity = new Entities.Compilation(cx, cwd, args);
|
||||
}
|
||||
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
|
||||
{
|
||||
Logger.Log(Severity.Error, " Unhandled exception analyzing {0}: {1}", "compilation", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogPerformance(Entities.PerformanceMetrics p) => compilationEntity.PopulatePerformance(p);
|
||||
|
||||
/// <summary>
|
||||
/// Extract an assembly to a new trap file.
|
||||
/// If the trap file exists, skip extraction to avoid duplicating
|
||||
@@ -258,7 +285,7 @@ namespace Semmle.Extraction.CSharp
|
||||
|
||||
if (assembly != null)
|
||||
{
|
||||
var cx = new Context(extractor, c, trapWriter, new AssemblyScope(assembly, assemblyPath));
|
||||
var cx = extractor.CreateContext(c, trapWriter, new AssemblyScope(assembly, assemblyPath, false));
|
||||
|
||||
foreach (var module in assembly.Modules)
|
||||
{
|
||||
@@ -344,7 +371,7 @@ namespace Semmle.Extraction.CSharp
|
||||
|
||||
if (!upToDate)
|
||||
{
|
||||
Context cx = new Context(extractor, compilation.Clone(), trapWriter, new SourceScope(tree));
|
||||
Context cx = extractor.CreateContext(compilation.Clone(), trapWriter, new SourceScope(tree));
|
||||
Populators.CompilationUnit.Extract(cx, tree.GetRoot());
|
||||
cx.PopulateAll();
|
||||
cx.ExtractComments(cx.CommentGenerator);
|
||||
@@ -356,7 +383,7 @@ namespace Semmle.Extraction.CSharp
|
||||
}
|
||||
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
|
||||
{
|
||||
extractor.Message(new Message { exception = ex, message = string.Format("Unhandled exception processing {0}: {1}", tree.FilePath, ex), severity = Severity.Error });
|
||||
extractor.Message(new Message("Unhandled exception processing syntax tree", tree.FilePath, null, ex.StackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,6 +400,8 @@ namespace Semmle.Extraction.CSharp
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
compilationTrapFile?.Dispose();
|
||||
|
||||
stopWatch.Stop();
|
||||
Logger.Log(Severity.Info, " Peak working set = {0} MB", Process.GetCurrentProcess().PeakWorkingSet64 / (1024 * 1024));
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace Semmle.Extraction.CSharp.Entities
|
||||
break;
|
||||
// Strangely, these are reported as SingleLineCommentTrivia.
|
||||
case SyntaxKind.DocumentationCommentExteriorTrivia:
|
||||
cx.ModelError("Unhandled comment type {0} for {1}", trivia.Kind(), trivia);
|
||||
cx.ModelError($"Unhandled comment type {trivia.Kind()} for {trivia}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Semmle.Extraction.CSharp.Entities
|
||||
{
|
||||
class Compilation : FreshEntity
|
||||
{
|
||||
public Compilation(Context cx, string cwd, string[] args) : base(cx)
|
||||
{
|
||||
Extraction.Entities.Assembly.CreateOutputAssembly(cx);
|
||||
|
||||
cx.Emit(Tuples.compilations(this, Extraction.Entities.File.PathAsDatabaseString(cwd)));
|
||||
|
||||
// Arguments
|
||||
int index = 0;
|
||||
foreach(var arg in args)
|
||||
{
|
||||
cx.Emit(Tuples.compilation_args(this, index++, arg));
|
||||
}
|
||||
|
||||
// Files
|
||||
index = 0;
|
||||
foreach(var file in cx.Compilation.SyntaxTrees.Select(tree => Extraction.Entities.File.Create(cx, tree.FilePath)))
|
||||
{
|
||||
cx.Emit(Tuples.compilation_compiling_files(this, index++, file));
|
||||
}
|
||||
|
||||
// References
|
||||
index = 0;
|
||||
foreach(var file in cx.Compilation.References.OfType<PortableExecutableReference>().Select(r => Extraction.Entities.File.Create(cx, r.FilePath)))
|
||||
{
|
||||
cx.Emit(Tuples.compilation_referencing_files(this, index++, file));
|
||||
}
|
||||
|
||||
// Diagnostics
|
||||
index = 0;
|
||||
foreach(var diag in cx.Compilation.GetDiagnostics().Select(d => new Diagnostic(cx, d)))
|
||||
{
|
||||
cx.Emit(Tuples.diagnostic_for(diag, this, 0, index++));
|
||||
}
|
||||
}
|
||||
|
||||
public void PopulatePerformance(PerformanceMetrics p)
|
||||
{
|
||||
int index = 0;
|
||||
foreach(float metric in p.Metrics)
|
||||
{
|
||||
cx.Emit(Tuples.compilation_time(this, -1, index++, metric));
|
||||
}
|
||||
cx.Emit(Tuples.compilation_finished(this, (float)p.Total.Cpu.TotalSeconds, (float)p.Total.Elapsed.TotalSeconds));
|
||||
}
|
||||
|
||||
public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.NoLabel;
|
||||
}
|
||||
|
||||
class Diagnostic : FreshEntity
|
||||
{
|
||||
public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.NoLabel;
|
||||
|
||||
public Diagnostic(Context cx, Microsoft.CodeAnalysis.Diagnostic diag) : base(cx)
|
||||
{
|
||||
cx.Emit(Tuples.diagnostics(this, (int)diag.Severity, diag.Id, diag.Descriptor.Title.ToString(),
|
||||
diag.GetMessage(), Extraction.Entities.Location.Create(cx, diag.Location)));
|
||||
}
|
||||
}
|
||||
|
||||
public struct Timings
|
||||
{
|
||||
public TimeSpan Elapsed, Cpu, User;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The various performance metrics to log.
|
||||
/// </summary>
|
||||
public struct PerformanceMetrics
|
||||
{
|
||||
public Timings Frontend, Extractor, Total;
|
||||
public long PeakWorkingSet;
|
||||
|
||||
/// <summary>
|
||||
/// These are in database order (0 indexed)
|
||||
/// </summary>
|
||||
public IEnumerable<float> Metrics
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return (float)Frontend.Cpu.TotalSeconds;
|
||||
yield return (float)Frontend.Elapsed.TotalSeconds;
|
||||
yield return (float)Extractor.Cpu.TotalSeconds;
|
||||
yield return (float)Extractor.Elapsed.TotalSeconds;
|
||||
yield return (float)Frontend.User.TotalSeconds;
|
||||
yield return (float)Extractor.User.TotalSeconds;
|
||||
yield return PeakWorkingSet / 1024.0f / 1024.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
|
||||
return ExprKind.PARAMETER_ACCESS;
|
||||
|
||||
default:
|
||||
cx.ModelError(symbol, "Unhandled access kind '{0}'", symbol.Kind);
|
||||
cx.ModelError(symbol, $"Unhandled access kind '{symbol.Kind}'");
|
||||
return ExprKind.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
|
||||
case SyntaxKind.QuestionQuestionToken: return ExprKind.NULL_COALESCING;
|
||||
// !! And the rest
|
||||
default:
|
||||
cx.ModelError("Unhandled operator type {0}", kind);
|
||||
cx.ModelError($"Unhandled operator type {kind}");
|
||||
return ExprKind.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
|
||||
return IsPattern.Create(info);
|
||||
|
||||
default:
|
||||
info.Context.ModelError(info.Node, "Unhandled expression '{0}' of kind '{1}'", info.Node, info.Node.Kind());
|
||||
info.Context.ModelError(info.Node, $"Unhandled expression '{info.Node}' of kind '{info.Node.Kind()}'");
|
||||
return new Unknown(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
|
||||
new Expression(new ExpressionInfo(cx, Type, cx.Create(c.GetLocation()), ExprKind.STRING_LITERAL, this, child++, false, interpolatedText.TextToken.Text));
|
||||
break;
|
||||
default:
|
||||
throw new InternalError(c, "Unhandled interpolation kind {0}", c.Kind());
|
||||
throw new InternalError(c, $"Unhandled interpolation kind {c.Kind()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
|
||||
return Access.Create(info, target, false, Parameter.GetAlreadyCreated(info.Context, (IParameterSymbol)target));
|
||||
|
||||
default:
|
||||
throw new InternalError(info.Node, "Unhandled identifier kind '{0}'", target.Kind);
|
||||
throw new InternalError(info.Node, $"Unhandled identifier kind '{target.Kind}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ namespace Semmle.Extraction.CSharp.Entities.Expressions
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new InternalError(qc, "Unhandled query clause of kind {0}", qc.Kind());
|
||||
throw new InternalError(qc, $"Unhandled query clause of kind {qc.Kind()}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ namespace Semmle.Extraction.CSharp.Entities
|
||||
case MethodKind.LocalFunction:
|
||||
return LocalFunction.Create(cx, methodDecl);
|
||||
default:
|
||||
throw new InternalError(methodDecl, "Unhandled method '{0}' of kind '{1}'", methodDecl, methodDecl.MethodKind);
|
||||
throw new InternalError(methodDecl, $"Unhandled method '{methodDecl}' of kind '{methodDecl.MethodKind}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Semmle.Extraction.CSharp.Entities
|
||||
HasModifier(cx, type, "private");
|
||||
break;
|
||||
default:
|
||||
throw new InternalError("Unhandled Microsoft.CodeAnalysis.Accessibility value: {0}", access);
|
||||
throw new InternalError($"Unhandled Microsoft.CodeAnalysis.Accessibility value: {access}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Semmle.Extraction.CSharp.Entities.Statements
|
||||
case SyntaxKind.ForEachVariableStatement:
|
||||
return ForEachVariable.Create(cx, (ForEachVariableStatementSyntax)node, parent, child);
|
||||
default:
|
||||
throw new InternalError(node, "Unhandled statement of kind '{0}'", node.Kind());
|
||||
throw new InternalError(node, $"Unhandled statement of kind '{node.Kind()}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Semmle.Extraction.CSharp.Entities.Statements
|
||||
case SyntaxKind.None: return StmtKind.GOTO;
|
||||
case SyntaxKind.DefaultKeyword: return StmtKind.GOTO_DEFAULT;
|
||||
case SyntaxKind.CaseKeyword: return StmtKind.GOTO_CASE;
|
||||
default: throw new InternalError(node, "Unhandled goto statement kind {0}", node.CaseOrDefaultKeyword.Kind());
|
||||
default: throw new InternalError(node, $"Unhandled goto statement kind {node.CaseOrDefaultKeyword.Kind()}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace Semmle.Extraction.CSharp.Entities
|
||||
case TypeKind.Delegate: return Kinds.TypeKind.DELEGATE;
|
||||
case TypeKind.Pointer: return Kinds.TypeKind.POINTER;
|
||||
default:
|
||||
cx.ModelError(t, "Unhandled type kind '{0}'", t.TypeKind);
|
||||
cx.ModelError(t, $"Unhandled type kind '{t.TypeKind}'");
|
||||
return Kinds.TypeKind.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace Semmle.Extraction.CSharp.Entities
|
||||
case VarianceKind.Out: return Variance.Out;
|
||||
case VarianceKind.In: return Variance.In;
|
||||
default:
|
||||
throw new InternalError("Unexpected VarianceKind {0}", symbol.Variance);
|
||||
throw new InternalError($"Unexpected VarianceKind {symbol.Variance}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ namespace Semmle.Extraction.CSharp.Entities
|
||||
containingEntity = Create(Context, symbol.ContainingType);
|
||||
break;
|
||||
default:
|
||||
throw new InternalError(symbol, "Unhandled type parameter kind {0}", symbol.TypeParameterKind);
|
||||
throw new InternalError(symbol, $"Unhandled type parameter kind {symbol.TypeParameterKind}");
|
||||
}
|
||||
return new Key(containingEntity, "_", symbol.Ordinal, ";", kind);
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ namespace Semmle.Extraction.CSharp.Entities
|
||||
{
|
||||
string result;
|
||||
if (!OperatorSymbol(methodName, out result))
|
||||
cx.ModelError("Unhandled operator name in OperatorSymbol(): '{0}'", methodName);
|
||||
cx.ModelError($"Unhandled operator name in OperatorSymbol(): '{methodName}'");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace Semmle.Extraction.CSharp
|
||||
/// <returns><see cref="ExitCode"/></returns>
|
||||
public static ExitCode Run(string[] args)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
var commandLineArguments = Options.CreateWithEnvironment(args);
|
||||
var fileLogger = new FileLogger(commandLineArguments.Verbosity, GetCSharpLogPath());
|
||||
var logger = commandLineArguments.Console
|
||||
@@ -118,8 +120,8 @@ namespace Semmle.Extraction.CSharp
|
||||
compilerArguments.Encoding,
|
||||
syntaxTrees);
|
||||
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
var sw1 = new Stopwatch();
|
||||
sw1.Start();
|
||||
|
||||
Parallel.Invoke(
|
||||
new ParallelOptions { MaxDegreeOfParallelism = commandLineArguments.Threads },
|
||||
@@ -148,6 +150,7 @@ namespace Semmle.Extraction.CSharp
|
||||
);
|
||||
|
||||
analyser.Initialize(compilerArguments, compilation, commandLineArguments, compilerVersion.ArgsWithResponse);
|
||||
analyser.AnalyseCompilation(cwd, args);
|
||||
analyser.AnalyseReferences();
|
||||
|
||||
foreach (var tree in compilation.SyntaxTrees)
|
||||
@@ -155,13 +158,29 @@ namespace Semmle.Extraction.CSharp
|
||||
analyser.AnalyseTree(tree);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
logger.Log(Severity.Info, " Models constructed in {0}", sw.Elapsed);
|
||||
var currentProcess = Process.GetCurrentProcess();
|
||||
var cpuTime1 = currentProcess.TotalProcessorTime;
|
||||
var userTime1 = currentProcess.UserProcessorTime;
|
||||
sw1.Stop();
|
||||
logger.Log(Severity.Info, " Models constructed in {0}", sw1.Elapsed);
|
||||
|
||||
sw.Restart();
|
||||
var sw2 = new Stopwatch();
|
||||
sw2.Start();
|
||||
analyser.PerformExtraction(commandLineArguments.Threads);
|
||||
sw.Stop();
|
||||
logger.Log(Severity.Info, " Extraction took {0}", sw.Elapsed);
|
||||
sw2.Stop();
|
||||
var cpuTime2 = currentProcess.TotalProcessorTime;
|
||||
var userTime2 = currentProcess.UserProcessorTime;
|
||||
|
||||
var performance = new Entities.PerformanceMetrics()
|
||||
{
|
||||
Frontend = new Entities.Timings() { Elapsed = sw1.Elapsed, Cpu = cpuTime1, User = userTime1 },
|
||||
Extractor = new Entities.Timings() { Elapsed = sw2.Elapsed, Cpu = cpuTime2 - cpuTime1, User = userTime2 - userTime1 },
|
||||
Total = new Entities.Timings() { Elapsed = stopwatch.Elapsed, Cpu=cpuTime2, User = userTime2 },
|
||||
PeakWorkingSet = currentProcess.PeakWorkingSet64
|
||||
};
|
||||
|
||||
analyser.LogPerformance(performance);
|
||||
logger.Log(Severity.Info, " Extraction took {0}", sw2.Elapsed);
|
||||
|
||||
return analyser.TotalErrors == 0 ? ExitCode.Ok : ExitCode.Errors;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Semmle.Extraction.CSharp.Populators
|
||||
|
||||
public override void DefaultVisit(SyntaxNode node)
|
||||
{
|
||||
cx.ModelError(node, "Unhandled syntax node {0}", node.Kind());
|
||||
cx.ModelError(node, $"Unhandled syntax node {node.Kind()}");
|
||||
}
|
||||
|
||||
public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node)
|
||||
@@ -56,7 +56,7 @@ namespace Semmle.Extraction.CSharp.Populators
|
||||
}
|
||||
catch (System.Exception ex) // lgtm[cs/catch-of-all-exceptions]
|
||||
{
|
||||
cx.ModelError(node, "Exception processing syntax node of type {0}: {1}", node.Kind(), ex);
|
||||
cx.ModelError(node, $"Exception processing syntax node of type {node.Kind()}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace Semmle.Extraction.CSharp.Populators
|
||||
public override void VisitExternAliasDirective(ExternAliasDirectiveSyntax node)
|
||||
{
|
||||
// This information is not yet extracted.
|
||||
cx.Extractor.Message(new Message { severity = Severity.Info, message = "Ignoring extern alias directive" });
|
||||
cx.ExtractionError("Not implemented extern alias directive", node.ToFullString(), Extraction.Entities.Location.Create(cx, node.GetLocation()), "", Severity.Info);
|
||||
}
|
||||
|
||||
public override void VisitCompilationUnit(CompilationUnitSyntax compilationUnit)
|
||||
@@ -119,6 +119,9 @@ namespace Semmle.Extraction.CSharp.Populators
|
||||
{
|
||||
public static void Extract(Context cx, SyntaxNode unit)
|
||||
{
|
||||
// Ensure that the file itself is populated in case the source file is totally empty
|
||||
File.Create(cx, unit.SyntaxTree.FilePath);
|
||||
|
||||
((CSharpSyntaxNode)unit).Accept(new CompilationUnitVisitor(cx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Semmle.Extraction.CSharp.Populators
|
||||
this.cx = cx;
|
||||
}
|
||||
|
||||
public override IEntity DefaultVisit(ISymbol symbol) => throw new InternalError(symbol, "Unhandled symbol '{0}' of kind '{1}'", symbol, symbol.Kind);
|
||||
public override IEntity DefaultVisit(ISymbol symbol) => throw new InternalError(symbol, $"Unhandled symbol '{symbol}' of kind '{symbol.Kind}'");
|
||||
|
||||
public override IEntity VisitArrayType(IArrayTypeSymbol array) => ArrayType.Create(cx, array);
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Semmle.Extraction.CSharp.Populators
|
||||
}
|
||||
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
|
||||
{
|
||||
cx.ModelError(symbol, "Exception processing symbol '{2}' of type '{0}': {1}", symbol.Kind, ex, symbol);
|
||||
cx.ModelError(symbol, $"Exception processing symbol '{symbol.Kind}' of type '{ex}': {symbol}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace Semmle.Extraction.CSharp
|
||||
tb.Append("dynamic");
|
||||
return;
|
||||
default:
|
||||
throw new InternalError(type, "Unhandled type kind '{0}'", type.TypeKind);
|
||||
throw new InternalError(type, $"Unhandled type kind '{type.TypeKind}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,7 @@ namespace Semmle.Extraction.CSharp
|
||||
tb.Append("dynamic");
|
||||
return;
|
||||
default:
|
||||
throw new InternalError(type, "Unhandled type kind '{0}'", type.TypeKind);
|
||||
throw new InternalError(type, $"Unhandled type kind '{type.TypeKind}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,18 @@ namespace Semmle.Extraction.CSharp
|
||||
|
||||
internal static Tuple commentline_location(CommentLine commentLine, Location location) => new Tuple("commentline_location", commentLine, location);
|
||||
|
||||
internal static Tuple compilation_args(Compilation compilation, int index, string arg) => new Tuple("compilation_args", compilation, index, arg);
|
||||
|
||||
internal static Tuple compilation_compiling_files(Compilation compilation, int index, File file) => new Tuple("compilation_compiling_files", compilation, index, file);
|
||||
|
||||
internal static Tuple compilation_referencing_files(Compilation compilation, int index, File file) => new Tuple("compilation_referencing_files", compilation, index, file);
|
||||
|
||||
internal static Tuple compilation_finished(Compilation compilation, float cpuSeconds, float elapsedSeconds) => new Tuple("compilation_finished", compilation, cpuSeconds, elapsedSeconds);
|
||||
|
||||
internal static Tuple compilation_time(Compilation compilation, int num, int index, float metric) => new Tuple("compilation_time", compilation, num, index, metric);
|
||||
|
||||
internal static Tuple compilations(Compilation compilation, string cwd) => new Tuple("compilations", compilation, cwd);
|
||||
|
||||
internal static Tuple compiler_generated(IEntity entity) => new Tuple("compiler_generated", entity);
|
||||
|
||||
internal static Tuple conditional_access(Expression access) => new Tuple("conditional_access", access);
|
||||
@@ -59,6 +71,11 @@ namespace Semmle.Extraction.CSharp
|
||||
|
||||
internal static Tuple destructors(Destructor destructor, string name, Type containingType, Destructor original) => new Tuple("destructors", destructor, name, containingType, original);
|
||||
|
||||
internal static Tuple diagnostic_for(Diagnostic diag, Compilation comp, int fileNo, int index) => new Tuple("diagnostic_for", diag, comp, fileNo, index);
|
||||
|
||||
internal static Tuple diagnostics(Diagnostic diag, int severity, string errorTag, string errorMessage, string fullErrorMessage, Location location) =>
|
||||
new Tuple("diagnostics", diag, severity, errorTag, errorMessage, fullErrorMessage, location);
|
||||
|
||||
internal static Tuple dynamic_member_name(Expression e, string name) => new Tuple("dynamic_member_name", e, name);
|
||||
|
||||
internal static Tuple enum_underlying_type(Type @enum, Type type) => new Tuple("enum_underlying_type", @enum, type);
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Semmle.Extraction
|
||||
{
|
||||
if (idLabelCache.TryGetValue(id, out var originalEntity))
|
||||
{
|
||||
Extractor.Message(new Message { message = "Label collision for " + id, severity = Severity.Warning });
|
||||
ExtractionError("Label collision for " + id, entity.Label.ToString(), Entities.Location.Create(this, entity.ReportingLocation), "", Severity.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -218,11 +218,11 @@ namespace Semmle.Extraction
|
||||
}
|
||||
catch (InternalError ex)
|
||||
{
|
||||
Extractor.Message(ex.ExtractionMessage);
|
||||
ExtractionError(new Message(ex.Text, ex.EntityText, Entities.Location.Create(this, ex.Location), ex.StackTrace));
|
||||
}
|
||||
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
|
||||
{
|
||||
Extractor.Message(new Message { severity = Severity.Error, exception = ex, message = "Uncaught exception" });
|
||||
ExtractionError("Uncaught exception", ex.Message, GeneratedLocation.Create(this), ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,6 +247,8 @@ namespace Semmle.Extraction
|
||||
TrapWriter = trapWriter;
|
||||
}
|
||||
|
||||
public bool IsGlobalContext => Scope.IsGlobalScope;
|
||||
|
||||
public readonly ICommentGenerator CommentGenerator = new CommentProcessor();
|
||||
|
||||
readonly IExtractionScope Scope;
|
||||
@@ -378,7 +380,7 @@ namespace Semmle.Extraction
|
||||
{
|
||||
case TrapStackBehaviour.NeedsLabel:
|
||||
if (!tagStack.Any())
|
||||
Extractor.Message(new Message { message = "Tagstack unexpectedly empty", symbol = optionalSymbol, severity = Severity.Error });
|
||||
ExtractionError("TagStack unexpectedly empty", optionalSymbol, entity);
|
||||
duplicationGuard = false;
|
||||
deferred = false;
|
||||
break;
|
||||
@@ -448,6 +450,52 @@ namespace Semmle.Extraction
|
||||
var duplicationGuardKey = tagStack.Count > 0 ? tagStack.Peek() : null;
|
||||
CommentGenerator.RegisterElementLocation(entity.Label, duplicationGuardKey, l);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log an extraction error.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <param name="entityText">A textual representation of the failed entity.</param>
|
||||
/// <param name="location">The location of the error.</param>
|
||||
/// <param name="stackTrace">An optional stack trace of the error, or an empty string.</param>
|
||||
/// <param name="severity">The severity of the error.</param>
|
||||
public void ExtractionError(string message, string entityText, Entities.Location location, string stackTrace = "", Severity severity = Severity.Error)
|
||||
{
|
||||
var msg = new Message(message, entityText, location, stackTrace, severity);
|
||||
ExtractionError(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log an extraction error.
|
||||
/// </summary>
|
||||
/// <param name="message">The text of the message.</param>
|
||||
/// <param name="optionalSymbol">The symbol of the error, or null.</param>
|
||||
/// <param name="optionalEntity">The entity of the error, or null.</param>
|
||||
public void ExtractionError(string message, ISymbol optionalSymbol, IEntity optionalEntity)
|
||||
{
|
||||
if (!(optionalSymbol is null))
|
||||
{
|
||||
ExtractionError(message, optionalSymbol.ToDisplayString(), Entities.Location.Create(this, optionalSymbol.Locations.FirstOrDefault()));
|
||||
}
|
||||
else if(!(optionalEntity is null))
|
||||
{
|
||||
ExtractionError(message, optionalEntity.Label.ToString(), Entities.Location.Create(this, optionalEntity.ReportingLocation));
|
||||
}
|
||||
else
|
||||
{
|
||||
ExtractionError(message, "", GeneratedLocation.Create(this));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log an extraction message.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to log.</param>
|
||||
public void ExtractionError(Message msg)
|
||||
{
|
||||
new Entities.ExtractionMessage(this, msg);
|
||||
Extractor.Message(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static public class ContextExtensions
|
||||
@@ -457,12 +505,11 @@ namespace Semmle.Extraction
|
||||
/// </summary>
|
||||
/// <param name="cx">The context.</param>
|
||||
/// <param name="node">The syntax node causing the failure.</param>
|
||||
/// <param name="format">A string format.</param>
|
||||
/// <param name="args">Arguments for the format.</param>
|
||||
static public void ModelError(this Context cx, SyntaxNode node, string format, params object[] args)
|
||||
/// <param name="msg">The error message.</param>
|
||||
static public void ModelError(this Context cx, SyntaxNode node, string msg)
|
||||
{
|
||||
if (!cx.Extractor.Standalone)
|
||||
throw new InternalError(node, format, args);
|
||||
throw new InternalError(node, msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -470,24 +517,22 @@ namespace Semmle.Extraction
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="node">Symbol causing the error.</param>
|
||||
/// <param name="format">Format of message string.</param>
|
||||
/// <param name="args">Arguments to message string.</param>
|
||||
static public void ModelError(this Context cx, ISymbol symbol, string format, params object[] args)
|
||||
/// <param name="msg">The error message.</param>
|
||||
static public void ModelError(this Context cx, ISymbol symbol, string msg)
|
||||
{
|
||||
if (!cx.Extractor.Standalone)
|
||||
throw new InternalError(symbol, format, args);
|
||||
throw new InternalError(symbol, msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal an error in the program model.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="format">Format of message string.</param>
|
||||
/// <param name="args">Arguments to message string.</param>
|
||||
static public void ModelError(this Context cx, string format, params object[] args)
|
||||
/// <param name="msg">The error message.</param>
|
||||
static public void ModelError(this Context cx, string msg)
|
||||
{
|
||||
if (!cx.Extractor.Standalone)
|
||||
throw new InternalError(format, args);
|
||||
throw new InternalError(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -506,32 +551,21 @@ namespace Semmle.Extraction
|
||||
}
|
||||
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
|
||||
{
|
||||
var internalError = ex as InternalError;
|
||||
var message = internalError != null
|
||||
? internalError.ExtractionMessage
|
||||
: new Message { severity = Severity.Error, exception = ex, message = ex.ToString() };
|
||||
Message message;
|
||||
|
||||
if (node != null)
|
||||
message.node = node;
|
||||
message = Message.Create(context, ex.Message, node, ex.StackTrace);
|
||||
else if (symbol != null)
|
||||
message = Message.Create(context, ex.Message, symbol, ex.StackTrace);
|
||||
else if (ex is InternalError ie)
|
||||
message = new Message(ie.Text, ie.EntityText, Entities.Location.Create(context, ie.Location), ex.StackTrace);
|
||||
else
|
||||
message = new Message("Uncaught exception", ex.Message, GeneratedLocation.Create(context), ex.StackTrace);
|
||||
|
||||
if (symbol != null)
|
||||
message.symbol = symbol;
|
||||
|
||||
context.Extractor.Message(message);
|
||||
context.ExtractionError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the given string.
|
||||
/// </summary>
|
||||
/// <param name="cx">The extractor context.</param>
|
||||
/// <param name="format">The format string.</param>
|
||||
/// <param name="args">The inserts to the format string.</param>
|
||||
static public void Log(this Context cx, string format, params object[] args)
|
||||
{
|
||||
cx.Extractor.Message(new Message { severity = Severity.Info, message = string.Format(format, args) });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the given tuple to the trap file.
|
||||
/// </summary>
|
||||
|
||||
@@ -34,7 +34,8 @@ namespace Semmle.Extraction.Entities
|
||||
}
|
||||
}
|
||||
|
||||
public override bool NeedsPopulation => true;
|
||||
public override bool NeedsPopulation =>
|
||||
assembly != Context.Compilation.Assembly || !Context.IsGlobalContext;
|
||||
|
||||
public override int GetHashCode() =>
|
||||
symbol == null ? 91187354 : symbol.GetHashCode();
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Semmle.Extraction.Entities
|
||||
{
|
||||
class ExtractionMessage : FreshEntity
|
||||
{
|
||||
public ExtractionMessage(Context cx, Message msg) : base(cx)
|
||||
{
|
||||
cx.Emit(Tuples.extractor_messages(this, msg.Severity, "C# extractor", msg.Text, msg.EntityText, msg.Location, msg.StackTrace));
|
||||
}
|
||||
|
||||
public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.NoLabel;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ namespace Semmle.Extraction.Entities
|
||||
// On Windows: System.IO.DirectoryInfo.Name returns "L:\"
|
||||
string shortName = symbol.Parent == null ? "" : symbol.Name;
|
||||
|
||||
Context.Emit(Tuples.folders(this, Semmle.Extraction.Entities.File.PathAsDatabaseString(Path), shortName));
|
||||
Context.Emit(Tuples.folders(this, File.PathAsDatabaseString(Path), shortName));
|
||||
if (symbol.Parent != null)
|
||||
{
|
||||
Context.Emit(Tuples.containerparent(Create(Context, symbol.Parent), this));
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace Semmle.Extraction.Entities
|
||||
public Location(Context cx, Microsoft.CodeAnalysis.Location init)
|
||||
: base(cx, init) { }
|
||||
|
||||
internal static Location Create(Context cx, Microsoft.CodeAnalysis.Location loc) =>
|
||||
loc == null ? GeneratedLocation.Create(cx)
|
||||
: loc.IsInSource ? SourceLocation.Create(cx, loc)
|
||||
public static Location Create(Context cx, Microsoft.CodeAnalysis.Location loc) =>
|
||||
loc == null || loc.Kind == Microsoft.CodeAnalysis.LocationKind.None ?
|
||||
GeneratedLocation.Create(cx) : loc.IsInSource ? SourceLocation.Create(cx, loc)
|
||||
: Assembly.Create(cx, loc);
|
||||
|
||||
public override Microsoft.CodeAnalysis.Location ReportingLocation => symbol;
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace Semmle.Extraction
|
||||
/// </summary>
|
||||
/// <param name="path">The path to populate.</param>
|
||||
bool InFileScope(string path);
|
||||
|
||||
bool IsGlobalScope { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,15 +35,18 @@ namespace Semmle.Extraction
|
||||
readonly IAssemblySymbol assembly;
|
||||
readonly string filepath;
|
||||
|
||||
public AssemblyScope(IAssemblySymbol symbol, string path)
|
||||
public AssemblyScope(IAssemblySymbol symbol, string path, bool isOutput)
|
||||
{
|
||||
assembly = symbol;
|
||||
filepath = path;
|
||||
IsGlobalScope = isOutput;
|
||||
}
|
||||
|
||||
public bool IsGlobalScope { get; }
|
||||
|
||||
public bool InFileScope(string path) => path == filepath;
|
||||
|
||||
public bool InScope(ISymbol symbol) => Equals(symbol.ContainingAssembly, assembly);
|
||||
public bool InScope(ISymbol symbol) => Equals(symbol.ContainingAssembly, assembly) || Equals(symbol, assembly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,6 +61,8 @@ namespace Semmle.Extraction
|
||||
sourceTree = tree;
|
||||
}
|
||||
|
||||
public bool IsGlobalScope => false;
|
||||
|
||||
public bool InFileScope(string path) => path == sourceTree.FilePath;
|
||||
|
||||
public bool InScope(ISymbol symbol) => symbol.Locations.Any(loc => loc.SourceTree == sourceTree);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Semmle.Util;
|
||||
using Semmle.Util.Logging;
|
||||
|
||||
@@ -82,6 +83,15 @@ namespace Semmle.Extraction
|
||||
/// The extractor SHA, obtained from the git log.
|
||||
/// </summary>
|
||||
string Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new context.
|
||||
/// </summary>
|
||||
/// <param name="c">The C# compilation.</param>
|
||||
/// <param name="trapWriter">The trap writer.</param>
|
||||
/// <param name="scope">The extraction scope (what to include in this trap file).</param>
|
||||
/// <returns></returns>
|
||||
Context CreateContext(Compilation c, TrapWriter trapWriter, IExtractionScope scope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -122,7 +132,7 @@ namespace Semmle.Extraction
|
||||
lock (mutex)
|
||||
{
|
||||
|
||||
if (msg.severity == Severity.Error)
|
||||
if (msg.Severity == Severity.Error)
|
||||
{
|
||||
++Errors;
|
||||
if (Errors == maxErrors)
|
||||
@@ -136,24 +146,7 @@ namespace Semmle.Extraction
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log(msg.severity, " {0}", msg.message);
|
||||
|
||||
if (msg.node != null)
|
||||
{
|
||||
Logger.Log(msg.severity, " Syntax element '{0}' at {1}", msg.node, msg.node.GetLocation().GetLineSpan());
|
||||
}
|
||||
|
||||
if (msg.symbol != null)
|
||||
{
|
||||
Logger.Log(msg.severity, " Symbol '{0}'", msg.symbol);
|
||||
foreach (var l in msg.symbol.Locations)
|
||||
Logger.Log(msg.severity, " Location: {0}", l.IsInSource ? l.GetLineSpan().ToString() : l.MetadataModule.ToString());
|
||||
}
|
||||
|
||||
if (msg.exception != null)
|
||||
{
|
||||
Logger.Log(msg.severity, " Exception: {0}", msg.exception);
|
||||
}
|
||||
Logger.Log(msg.Severity, $" {msg.ToLogString()}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +184,11 @@ namespace Semmle.Extraction
|
||||
missingNamespaces.Add(fqdn);
|
||||
}
|
||||
|
||||
public Context CreateContext(Compilation c, TrapWriter trapWriter, IExtractionScope scope)
|
||||
{
|
||||
return new Context(this, c, trapWriter, scope);
|
||||
}
|
||||
|
||||
public IEnumerable<string> MissingTypes => missingTypes;
|
||||
|
||||
public IEnumerable<string> MissingNamespaces => missingNamespaces;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Semmle.Util.Logging;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Semmle.Extraction
|
||||
{
|
||||
@@ -9,26 +9,31 @@ namespace Semmle.Extraction
|
||||
/// </summary>
|
||||
public class InternalError : Exception
|
||||
{
|
||||
public InternalError(ISymbol symbol, string msg, params object[] args)
|
||||
public InternalError(ISymbol symbol, string msg)
|
||||
{
|
||||
ExtractionMessage = new Message { exception = this, symbol = symbol, severity = Severity.Error, message = string.Format(msg, args) };
|
||||
Text = msg;
|
||||
EntityText = symbol.ToString();
|
||||
Location = symbol.Locations.FirstOrDefault();
|
||||
}
|
||||
|
||||
public InternalError(SyntaxNode node, string msg, params object[] args)
|
||||
public InternalError(SyntaxNode node, string msg)
|
||||
{
|
||||
ExtractionMessage = new Message { exception = this, node = node, severity = Severity.Error, message = string.Format(msg, args) };
|
||||
Text = msg;
|
||||
EntityText = node.ToString();
|
||||
Location = node.GetLocation();
|
||||
}
|
||||
|
||||
public InternalError(string msg, params object[] args)
|
||||
public InternalError(string msg)
|
||||
{
|
||||
ExtractionMessage = new Message { exception = this, severity = Severity.Error, message = string.Format(msg, args) };
|
||||
Text = msg;
|
||||
EntityText = "";
|
||||
Location = null;
|
||||
}
|
||||
|
||||
public Message ExtractionMessage
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
public Location Location { get; }
|
||||
public string Text;
|
||||
public string EntityText;
|
||||
|
||||
public override string Message => ExtractionMessage.message;
|
||||
public override string Message => Text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,54 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Semmle.Util.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Semmle.Extraction
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates information for a log message.
|
||||
/// </summary>
|
||||
public struct Message
|
||||
public class Message
|
||||
{
|
||||
public Severity severity;
|
||||
public string message;
|
||||
public ISymbol symbol;
|
||||
public SyntaxNode node;
|
||||
public Exception exception;
|
||||
public readonly Severity Severity;
|
||||
public readonly string Text;
|
||||
public readonly string StackTrace;
|
||||
public readonly string EntityText;
|
||||
public readonly Entities.Location Location;
|
||||
|
||||
public override string ToString() => message;
|
||||
public Message(string text, string entityText, Entities.Location location, string stackTrace="", Severity severity = Severity.Error)
|
||||
{
|
||||
Severity = severity;
|
||||
Text = text;
|
||||
StackTrace = stackTrace;
|
||||
EntityText = entityText;
|
||||
Location = location;
|
||||
}
|
||||
|
||||
public static Message Create(Context cx, string text, ISymbol symbol, string stackTrace= "", Severity severity = Severity.Error)
|
||||
{
|
||||
return new Message(text, symbol.ToString(), Entities.Location.Create(cx, symbol.Locations.FirstOrDefault()), stackTrace, severity);
|
||||
}
|
||||
|
||||
public static Message Create(Context cx, string text, SyntaxNode node, string stackTrace = "", Severity severity = Severity.Error)
|
||||
{
|
||||
return new Message(text, node.ToString(), Entities.Location.Create(cx, node.GetLocation()), stackTrace, severity);
|
||||
}
|
||||
|
||||
public override string ToString() => Text;
|
||||
|
||||
public string ToLogString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(Text);
|
||||
if (!string.IsNullOrEmpty(EntityText))
|
||||
sb.Append(" in ").Append(EntityText);
|
||||
if (!(Location is null) && !(Location.UnderlyingObject is null))
|
||||
sb.Append(" at ").Append(Location.UnderlyingObject.GetLineSpan());
|
||||
if (!string.IsNullOrEmpty(StackTrace))
|
||||
sb.Append(" ").Append(StackTrace);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,4 @@
|
||||
<ProjectReference Include="..\Semmle.Util\Semmle.Util.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Entities\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace Semmle.Extraction
|
||||
|
||||
object ICachedEntity.UnderlyingObject => symbol;
|
||||
|
||||
public Initializer UnderlyingObject => symbol;
|
||||
|
||||
public abstract IId Id
|
||||
{
|
||||
get;
|
||||
|
||||
@@ -120,6 +120,9 @@ namespace Semmle.Extraction
|
||||
case int i:
|
||||
tb.Append(i);
|
||||
break;
|
||||
case float f:
|
||||
tb.Append(f.ToString("0.#####e0")); // Trap importer won't accept ints
|
||||
break;
|
||||
case string[] array:
|
||||
tb.Append("\"");
|
||||
if (NeedsTruncation(array))
|
||||
@@ -138,11 +141,9 @@ namespace Semmle.Extraction
|
||||
tb.Append("\"");
|
||||
break;
|
||||
case null:
|
||||
throw new InternalError("Attempt to write a null argument tuple {0} at column {1}",
|
||||
Name, column);
|
||||
throw new InternalError($"Attempt to write a null argument tuple {Name} at column {column}");
|
||||
default:
|
||||
throw new InternalError("Attempt to write an invalid argument type {0} in tuple {1} at column {2}",
|
||||
a.GetType(), Name, column);
|
||||
throw new InternalError($"Attempt to write an invalid argument type {a.GetType()} in tuple {Name} at column {column}");
|
||||
}
|
||||
|
||||
++column;
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace Semmle.Extraction
|
||||
internal static Tuple containerparent(Folder parent, IEntity child) =>
|
||||
new Tuple("containerparent", parent, child);
|
||||
|
||||
internal static Tuple extractor_messages(ExtractionMessage error, Semmle.Util.Logging.Severity severity, string origin, string errorMessage, string entityText, Location location, string stackTrace) =>
|
||||
new Tuple("extractor_messages", error, severity, origin, errorMessage, entityText, location, stackTrace);
|
||||
|
||||
internal static Tuple file_extraction_mode(File file, int mode) =>
|
||||
new Tuple("file_extraction_mode", file, mode);
|
||||
|
||||
|
||||
65
csharp/ql/src/semmle/code/csharp/commons/Compilation.qll
Normal file
65
csharp/ql/src/semmle/code/csharp/commons/Compilation.qll
Normal file
@@ -0,0 +1,65 @@
|
||||
import csharp
|
||||
import Diagnostics
|
||||
|
||||
/** An invocation of the C# compiler. */
|
||||
class Compilation extends @compilation {
|
||||
/** Gets a textual representation of this compilation. */
|
||||
string toString() { result = "compilation" }
|
||||
|
||||
/** Gets the directory in which this compilation was run, as a string. */
|
||||
string getDirectoryString() { compilations(this, result) }
|
||||
|
||||
/** Gets the folder in which this compilation was run. */
|
||||
Folder getFolder() { result.getAbsolutePath() = getDirectoryString() }
|
||||
|
||||
/** Gets the `i`th command line argument. */
|
||||
string getArgument(int i) { compilation_args(this, i, result) }
|
||||
|
||||
/** Gets the arguments as a concatenated string. */
|
||||
string getArguments() { result = concat(int i | exists(getArgument(i)) | getArgument(i), " ") }
|
||||
|
||||
/** Gets the 'i'th source file in this compilation. */
|
||||
File getFileCompiled(int i) { compilation_compiling_files(this, i, result) }
|
||||
|
||||
/** Gets a source file compiled in this compilation. */
|
||||
File getAFileCompiled() { result = getFileCompiled(_) }
|
||||
|
||||
/** Gets the `i`th reference in this compilation. */
|
||||
File getReference(int i) { compilation_referencing_files(this, i, result) }
|
||||
|
||||
/** Gets a reference in this compilation. */
|
||||
File getAReference() { result = getReference(_) }
|
||||
|
||||
/** Gets a diagnostic associated with this compilation. */
|
||||
Diagnostic getADiagnostic() { result.getCompilation() = this }
|
||||
|
||||
/** Gets a performance metric for this compilation. */
|
||||
float getMetric(int metric) { compilation_time(this, -1, metric, result) }
|
||||
|
||||
/** Gets the CPU time of the compilation. */
|
||||
float getFrontendCpuSeconds() { result = getMetric(0) }
|
||||
|
||||
/** Gets the elapsed time of the compilation. */
|
||||
float getFrontendElapsedSeconds() { result = getMetric(1) }
|
||||
|
||||
/** Gets the CPU time of the extraction. */
|
||||
float getExtractorCpuSeconds() { result = getMetric(2) }
|
||||
|
||||
/** Gets the elapsed time of the extraction. */
|
||||
float getExtractorElapsedSeconds() { result = getMetric(3) }
|
||||
|
||||
/** Gets the user CPU time of the compilation. */
|
||||
float getFrontendUserCpuSeconds() { result = getMetric(4) }
|
||||
|
||||
/** Gets the user CPU time of the extraction. */
|
||||
float getExtractorUserCpuSeconds() { result = getMetric(5) }
|
||||
|
||||
/** Gets the peak working set of the extractor process in MB. */
|
||||
float getPeakWorkingSetMB() { result = getMetric(6) }
|
||||
|
||||
/** Gets the CPU seconds for the entire extractor process. */
|
||||
float getCpuSeconds() { compilation_finished(this, result, _) }
|
||||
|
||||
/** Gets the elapsed seconds for the entire extractor process. */
|
||||
float getElapsedSeconds() { compilation_finished(this, _, result) }
|
||||
}
|
||||
115
csharp/ql/src/semmle/code/csharp/commons/Diagnostics.qll
Normal file
115
csharp/ql/src/semmle/code/csharp/commons/Diagnostics.qll
Normal file
@@ -0,0 +1,115 @@
|
||||
/** Provides classes relating to compilation and extraction diagnostics. */
|
||||
|
||||
import csharp
|
||||
import Compilation
|
||||
|
||||
/** A diagnostic emitted by a compilation, such as a compilation warning or an error. */
|
||||
class Diagnostic extends @diagnostic {
|
||||
/** Gets the compilation that generated this diagnostic. */
|
||||
Compilation getCompilation() { diagnostic_for(this, result, _, _) }
|
||||
|
||||
int severity;
|
||||
|
||||
string tag;
|
||||
|
||||
string message;
|
||||
|
||||
string fullMessage;
|
||||
|
||||
Location location;
|
||||
|
||||
Diagnostic() { diagnostics(this, severity, tag, message, fullMessage, location) }
|
||||
|
||||
/**
|
||||
* Gets the severity of this diagnostic.
|
||||
* 0 = Hidden
|
||||
* 1 = Info
|
||||
* 2 = Warning
|
||||
* 3 = Error
|
||||
*/
|
||||
int getSeverity() { result = severity }
|
||||
|
||||
/** Gets the identifier of this diagnostic, for example "CS8019". */
|
||||
string getTag() { result = tag }
|
||||
|
||||
/** Gets the short error message of this diagnostic. */
|
||||
string getMessage() { result = message }
|
||||
|
||||
/** Gets the full error message of this diagnostic. */
|
||||
string getFullMessage() { result = fullMessage }
|
||||
|
||||
/** Gets the location of this diagnostic. */
|
||||
Location getLocation() { result = location }
|
||||
|
||||
/** Gets a textual representation of this diagnostic. */
|
||||
string toString() { result = this.getTag() + ": " + this.getFullMessage() }
|
||||
|
||||
/** Gets the element associated with this diagnostic, if any. */
|
||||
Element getElement() { this.getLocation() = result.getLocation() }
|
||||
}
|
||||
|
||||
/** A diagnostic that is a compilation error. */
|
||||
class CompilerError extends Diagnostic {
|
||||
CompilerError() { this.getSeverity() >= 3 }
|
||||
}
|
||||
|
||||
/** A message from an extractor. */
|
||||
class ExtractorMessage extends @extractor_message {
|
||||
int severity;
|
||||
|
||||
string origin;
|
||||
|
||||
string text;
|
||||
|
||||
string element;
|
||||
|
||||
string stackTrace;
|
||||
|
||||
Location location;
|
||||
|
||||
ExtractorMessage() {
|
||||
extractor_messages(this, severity, origin, text, element, location, stackTrace)
|
||||
}
|
||||
|
||||
/** Gets the severity of this message. */
|
||||
int getSeverity() { result = severity }
|
||||
|
||||
/** Gets the name of the extractor that produced this message, for example, `C# extractor`. */
|
||||
string getOrigin() { result = origin }
|
||||
|
||||
/** Gets the text of this message. */
|
||||
string getText() { result = text }
|
||||
|
||||
/** Gets the textual representation of the entity that triggered this message. */
|
||||
string getElementText() { result = element }
|
||||
|
||||
/** Gets a string containing a stack trace of the extractor, or the empty string if none available. */
|
||||
string getStackTrace() { result = stackTrace }
|
||||
|
||||
/** Gets the location of the element. */
|
||||
Location getLocation() { result = location }
|
||||
|
||||
/** Gets a textual representation of this message. */
|
||||
string toString() { result = text }
|
||||
|
||||
/** Gets a string representation of the severity of this message. */
|
||||
string getSeverityText() {
|
||||
severity = 1 and result = "Trace"
|
||||
or
|
||||
severity = 2 and result = "Debug"
|
||||
or
|
||||
severity = 3 and result = "Info"
|
||||
or
|
||||
severity = 4 and result = "Warning"
|
||||
or
|
||||
severity = 5 and result = "Error"
|
||||
}
|
||||
|
||||
/** Gets the element associated with this message, if any. */
|
||||
Element getElement() { this.getLocation() = result.getLocation() }
|
||||
}
|
||||
|
||||
/** An error from an extractor. */
|
||||
class ExtractorError extends ExtractorMessage {
|
||||
ExtractorError() { this.getSeverity() >= 5 }
|
||||
}
|
||||
@@ -1,3 +1,156 @@
|
||||
|
||||
/**
|
||||
* An invocation of the compiler. Note that more than one file may be
|
||||
* compiled per invocation. For example, this command compiles three
|
||||
* source files:
|
||||
*
|
||||
* csc f1.cs f2.cs f3.cs
|
||||
*
|
||||
* The `id` simply identifies the invocation, while `cwd` is the working
|
||||
* directory from which the compiler was invoked.
|
||||
*/
|
||||
compilations(
|
||||
unique int id : @compilation,
|
||||
string cwd : string ref
|
||||
);
|
||||
|
||||
/**
|
||||
* The arguments that were passed to the extractor for a compiler
|
||||
* invocation. If `id` is for the compiler invocation
|
||||
*
|
||||
* csc f1.cs f2.cs f3.cs
|
||||
*
|
||||
* then typically there will be rows for
|
||||
*
|
||||
* num | arg
|
||||
* --- | ---
|
||||
* 0 | --compiler
|
||||
* 1 | *path to compiler*
|
||||
* 2 | --cil
|
||||
* 3 | f1.cs
|
||||
* 4 | f2.cs
|
||||
* 5 | f3.cs
|
||||
*/
|
||||
#keyset[id, num]
|
||||
compilation_args(
|
||||
int id : @compilation ref,
|
||||
int num : int ref,
|
||||
string arg : string ref
|
||||
);
|
||||
|
||||
/**
|
||||
* The source files that are compiled by a compiler invocation.
|
||||
* If `id` is for the compiler invocation
|
||||
*
|
||||
* csc f1.cs f2.cs f3.cs
|
||||
*
|
||||
* then there will be rows for
|
||||
*
|
||||
* num | arg
|
||||
* --- | ---
|
||||
* 0 | f1.cs
|
||||
* 1 | f2.cs
|
||||
* 2 | f3.cs
|
||||
*/
|
||||
#keyset[id, num]
|
||||
compilation_compiling_files(
|
||||
int id : @compilation ref,
|
||||
int num : int ref,
|
||||
int file : @file ref
|
||||
);
|
||||
|
||||
/**
|
||||
* The references used by a compiler invocation.
|
||||
* If `id` is for the compiler invocation
|
||||
*
|
||||
* csc f1.cs f2.cs f3.cs /r:ref1.dll /r:ref2.dll /r:ref3.dll
|
||||
*
|
||||
* then there will be rows for
|
||||
*
|
||||
* num | arg
|
||||
* --- | ---
|
||||
* 0 | ref1.dll
|
||||
* 1 | ref2.dll
|
||||
* 2 | ref3.dll
|
||||
*/
|
||||
#keyset[id, num]
|
||||
compilation_referencing_files(
|
||||
int id : @compilation ref,
|
||||
int num : int ref,
|
||||
int file : @file ref
|
||||
);
|
||||
|
||||
/**
|
||||
* The time taken by the extractor for a compiler invocation.
|
||||
*
|
||||
* For each file `num`, there will be rows for
|
||||
*
|
||||
* kind | seconds
|
||||
* ---- | ---
|
||||
* 1 | CPU seconds used by the extractor frontend
|
||||
* 2 | Elapsed seconds during the extractor frontend
|
||||
* 3 | CPU seconds used by the extractor backend
|
||||
* 4 | Elapsed seconds during the extractor backend
|
||||
*/
|
||||
#keyset[id, num, kind]
|
||||
compilation_time(
|
||||
int id : @compilation ref,
|
||||
int num : int ref,
|
||||
/* kind:
|
||||
1 = frontend_cpu_seconds
|
||||
2 = frontend_elapsed_seconds
|
||||
3 = extractor_cpu_seconds
|
||||
4 = extractor_elapsed_seconds
|
||||
*/
|
||||
int kind : int ref,
|
||||
float seconds : float ref
|
||||
);
|
||||
|
||||
/**
|
||||
* An error or warning generated by the extractor.
|
||||
* The diagnostic message `diagnostic` was generated during compiler
|
||||
* invocation `compilation`, and is the `file_number_diagnostic_number`th
|
||||
* message generated while extracting the `file_number`th file of that
|
||||
* invocation.
|
||||
*/
|
||||
#keyset[compilation, file_number, file_number_diagnostic_number]
|
||||
diagnostic_for(
|
||||
unique int diagnostic : @diagnostic ref,
|
||||
int compilation : @compilation ref,
|
||||
int file_number : int ref,
|
||||
int file_number_diagnostic_number : int ref
|
||||
);
|
||||
|
||||
diagnostics(
|
||||
unique int id: @diagnostic,
|
||||
int severity: int ref,
|
||||
string error_tag: string ref,
|
||||
string error_message: string ref,
|
||||
string full_error_message: string ref,
|
||||
int location: @location_default ref
|
||||
);
|
||||
|
||||
extractor_messages(
|
||||
unique int id: @extractor_message,
|
||||
int severity: int ref,
|
||||
string origin : string ref,
|
||||
string text : string ref,
|
||||
string entity : string ref,
|
||||
int location: @location_default ref,
|
||||
string stack_trace : string ref
|
||||
);
|
||||
|
||||
/**
|
||||
* If extraction was successful, then `cpu_seconds` and
|
||||
* `elapsed_seconds` are the CPU time and elapsed time (respectively)
|
||||
* that extraction took for compiler invocation `id`.
|
||||
*/
|
||||
compilation_finished(
|
||||
unique int id : @compilation ref,
|
||||
float cpu_seconds : float ref,
|
||||
float elapsed_seconds : float ref
|
||||
);
|
||||
|
||||
/*
|
||||
* External artifacts
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
diagnostics
|
||||
| Program.cs:7:13:7:13 | CS0219: The variable 'x' is assigned but its value is never used | CS0219 | 2 | Variable is assigned but its value is never used | The variable 'x' is assigned but its value is never used |
|
||||
| Program.cs:9:9:9:11 | CS0162: Unreachable code detected | CS0162 | 2 | Unreachable code detected | Unreachable code detected |
|
||||
| Program.cs:9:13:9:13 | CS0219: The variable 'y' is assigned but its value is never used | CS0219 | 2 | Variable is assigned but its value is never used | The variable 'y' is assigned but its value is never used |
|
||||
compilationErrors
|
||||
metricIsZero
|
||||
compilationArguments
|
||||
| compilation | 0 | -unsafe |
|
||||
| compilation | 1 | -target:library |
|
||||
| compilation | 2 | /noconfig |
|
||||
| compilation | 3 | /r:System.Private.CoreLib.dll |
|
||||
| compilation | 4 | /r:System.dll |
|
||||
| compilation | 5 | /r:System.Core.dll |
|
||||
| compilation | 6 | /r:System.Runtime.dll |
|
||||
| compilation | 7 | /r:System.Console.dll |
|
||||
| compilation | 8 | --console |
|
||||
| compilation | 9 | --verbosity |
|
||||
| compilation | 10 | 2 |
|
||||
| compilation | 11 | Program.cs |
|
||||
compilationFiles
|
||||
| compilation | 0 | Program.cs:0:0:0:0 | Program.cs |
|
||||
compilationFolder
|
||||
| compilation | compilations |
|
||||
diagnosticElements
|
||||
| Program.cs:7:13:7:13 | CS0219: The variable 'x' is assigned but its value is never used | Program.cs:7:13:7:13 | access to local variable x |
|
||||
| Program.cs:7:13:7:13 | CS0219: The variable 'x' is assigned but its value is never used | Program.cs:7:13:7:13 | x |
|
||||
| Program.cs:9:13:9:13 | CS0219: The variable 'y' is assigned but its value is never used | Program.cs:9:13:9:13 | access to local variable y |
|
||||
| Program.cs:9:13:9:13 | CS0219: The variable 'y' is assigned but its value is never used | Program.cs:9:13:9:13 | y |
|
||||
references
|
||||
| compilation | System.Console.dll |
|
||||
| compilation | System.Core.dll |
|
||||
| compilation | System.Private.CoreLib.dll |
|
||||
| compilation | System.Runtime.dll |
|
||||
| compilation | System.dll |
|
||||
| compilation | mscorlib.dll |
|
||||
timings
|
||||
| compilation |
|
||||
41
csharp/ql/test/library-tests/compilations/Compilations.ql
Normal file
41
csharp/ql/test/library-tests/compilations/Compilations.ql
Normal file
@@ -0,0 +1,41 @@
|
||||
import csharp
|
||||
import semmle.code.csharp.commons.Diagnostics
|
||||
|
||||
query predicate diagnostics(
|
||||
Diagnostic d, string tag, int severity, string message, string fullMessage
|
||||
) {
|
||||
tag = d.getTag() and
|
||||
severity = d.getSeverity() and
|
||||
message = d.getMessage() and
|
||||
fullMessage = d.getFullMessage()
|
||||
}
|
||||
|
||||
query predicate compilationErrors(CompilerError e) { any() }
|
||||
|
||||
query predicate metricIsZero(Compilation compilation, int metric) {
|
||||
compilation.getMetric(metric) = 0 and
|
||||
metric != 6 // Peak working set not implemented on Linux
|
||||
}
|
||||
|
||||
query predicate compilationArguments(Compilation compilation, int i, string arg) {
|
||||
arg = compilation.getArgument(i)
|
||||
}
|
||||
|
||||
query predicate compilationFiles(Compilation compilation, int i, File f) {
|
||||
f = compilation.getFileCompiled(i)
|
||||
}
|
||||
|
||||
query predicate compilationFolder(Compilation c, string folder) {
|
||||
folder = c.getFolder().getBaseName()
|
||||
}
|
||||
|
||||
query predicate diagnosticElements(Diagnostic d, Element e) { e = d.getElement() }
|
||||
|
||||
query predicate references(Compilation c, string reference) {
|
||||
reference = c.getAReference().getBaseName()
|
||||
}
|
||||
|
||||
query predicate timings(Compilation c) {
|
||||
c.getCpuSeconds() > 0 and
|
||||
c.getElapsedSeconds() > 0
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
extractorElements
|
||||
extractorErrors
|
||||
@@ -0,0 +1,8 @@
|
||||
import csharp
|
||||
import semmle.code.csharp.commons.Diagnostics
|
||||
|
||||
query predicate extractorElements(ExtractorMessage m, Element e) { e = m.getElement() }
|
||||
|
||||
query predicate extractorErrors(ExtractorError e, string origin, string stackTrace) {
|
||||
origin = e.getOrigin() and stackTrace = e.getStackTrace()
|
||||
}
|
||||
11
csharp/ql/test/library-tests/compilations/Program.cs
Normal file
11
csharp/ql/test/library-tests/compilations/Program.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System; // CS8019
|
||||
|
||||
class Class
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var x = 2; // CS0219
|
||||
return;
|
||||
var y = 3; // CS0219, CS0162
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
compilationMessages
|
||||
extractorMessages
|
||||
@@ -0,0 +1,6 @@
|
||||
import csharp
|
||||
import semmle.code.csharp.commons.Diagnostics
|
||||
|
||||
query predicate compilationMessages(Diagnostic diag) { any() }
|
||||
|
||||
query predicate extractorMessages(ExtractorMessage msg) { any() }
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
description: Adds information about compilations
|
||||
compatibility: backwards
|
||||
Reference in New Issue
Block a user