diff --git a/csharp/change-notes/2021-04-23-model-error-extraction.md b/csharp/change-notes/2021-04-23-model-error-extraction.md
new file mode 100644
index 000000000000..7ac878442984
--- /dev/null
+++ b/csharp/change-notes/2021-04-23-model-error-extraction.md
@@ -0,0 +1,2 @@
+lgtm,codescanning
+* Program model errors are now extracted in standalone extraction.
\ No newline at end of file
diff --git a/csharp/extractor/Semmle.Extraction/Context.cs b/csharp/extractor/Semmle.Extraction/Context.cs
index c0580db96a7d..a60d82c6952a 100644
--- a/csharp/extractor/Semmle.Extraction/Context.cs
+++ b/csharp/extractor/Semmle.Extraction/Context.cs
@@ -376,6 +376,19 @@ private void ExtractionError(Message msg)
Extractor.Message(msg);
}
+ private void ExtractionError(InternalError error)
+ {
+ ExtractionError(new Message(error.Message, error.EntityText, CreateLocation(error.Location), error.StackTrace, Severity.Error));
+ }
+
+ private void ReportError(InternalError error)
+ {
+ if (!Extractor.Standalone)
+ throw error;
+
+ ExtractionError(error);
+ }
+
///
/// Signal an error in the program model.
///
@@ -383,8 +396,7 @@ private void ExtractionError(Message msg)
/// The error message.
public void ModelError(SyntaxNode node, string msg)
{
- if (!Extractor.Standalone)
- throw new InternalError(node, msg);
+ ReportError(new InternalError(node, msg));
}
///
@@ -394,8 +406,7 @@ public void ModelError(SyntaxNode node, string msg)
/// The error message.
public void ModelError(ISymbol symbol, string msg)
{
- if (!Extractor.Standalone)
- throw new InternalError(symbol, msg);
+ ReportError(new InternalError(symbol, msg));
}
///
@@ -404,8 +415,7 @@ public void ModelError(ISymbol symbol, string msg)
/// The error message.
public void ModelError(string msg)
{
- if (!Extractor.Standalone)
- throw new InternalError(msg);
+ ReportError(new InternalError(msg));
}
///
diff --git a/csharp/ql/src/Diagnostics/DiagnosticExtractionErrors.ql b/csharp/ql/src/Diagnostics/DiagnosticExtractionErrors.ql
new file mode 100644
index 000000000000..23943d8491fb
--- /dev/null
+++ b/csharp/ql/src/Diagnostics/DiagnosticExtractionErrors.ql
@@ -0,0 +1,63 @@
+/**
+ * @name Extraction errors
+ * @description List all errors reported by the extractor or the compiler. Extractor errors are
+ * limited to those files where there are no compilation errors.
+ * @kind diagnostic
+ * @id cs/diagnostics/extraction-errors
+ */
+
+import csharp
+import semmle.code.csharp.commons.Diagnostics
+
+private newtype TDiagnosticError =
+ TCompilerError(CompilerError c) or
+ TExtractorError(ExtractorError e)
+
+abstract private class DiagnosticError extends TDiagnosticError {
+ abstract string getMessage();
+
+ abstract string toString();
+
+ abstract Location getLocation();
+
+ string getLocationMessage() {
+ if getLocation().getFile().fromSource()
+ then result = " in " + getLocation().getFile()
+ else result = ""
+ }
+}
+
+private class DiagnosticCompilerError extends DiagnosticError {
+ CompilerError c;
+
+ DiagnosticCompilerError() { this = TCompilerError(c) }
+
+ override string getMessage() {
+ result = "Compiler error" + this.getLocationMessage() + ": " + c.getMessage()
+ }
+
+ override string toString() { result = c.toString() }
+
+ override Location getLocation() { result = c.getLocation() }
+}
+
+private class DiagnosticExtractorError extends DiagnosticError {
+ ExtractorError e;
+
+ DiagnosticExtractorError() {
+ this = TExtractorError(e) and
+ not exists(CompilerError ce | ce.getLocation().getFile() = e.getLocation().getFile())
+ }
+
+ override string getMessage() {
+ result =
+ "Unexpected " + e.getOrigin() + " error" + this.getLocationMessage() + ": " + e.getText()
+ }
+
+ override string toString() { result = e.toString() }
+
+ override Location getLocation() { result = e.getLocation() }
+}
+
+from DiagnosticError error
+select error.getMessage(), 2
diff --git a/csharp/ql/src/Diagnostics/DiagnosticNoExtractionErrors.ql b/csharp/ql/src/Diagnostics/DiagnosticNoExtractionErrors.ql
new file mode 100644
index 000000000000..cb86e293efcb
--- /dev/null
+++ b/csharp/ql/src/Diagnostics/DiagnosticNoExtractionErrors.ql
@@ -0,0 +1,17 @@
+/**
+ * @name Successfully extracted files
+ * @description A list of all files in the source code directory that were extracted
+ * without encountering an extraction or compiler error in the file.
+ * @kind diagnostic
+ * @id cs/diagnostics/successfully-extracted-files
+ */
+
+import csharp
+import semmle.code.csharp.commons.Diagnostics
+
+from File file
+where
+ file.fromSource() and
+ not exists(ExtractorError e | e.getLocation().getFile() = file) and
+ not exists(CompilerError e | e.getLocation().getFile() = file)
+select file, ""
diff --git a/csharp/ql/test/library-tests/diagnostics/A.cs b/csharp/ql/test/library-tests/diagnostics/A.cs
new file mode 100644
index 000000000000..21a5b7f52616
--- /dev/null
+++ b/csharp/ql/test/library-tests/diagnostics/A.cs
@@ -0,0 +1,4 @@
+public class A
+{
+ public void M() { }
+}
\ No newline at end of file
diff --git a/csharp/ql/test/library-tests/diagnostics/DiagnosticExtractorErrors.expected b/csharp/ql/test/library-tests/diagnostics/DiagnosticExtractorErrors.expected
new file mode 100644
index 000000000000..e4ae5ec1ef29
--- /dev/null
+++ b/csharp/ql/test/library-tests/diagnostics/DiagnosticExtractorErrors.expected
@@ -0,0 +1 @@
+| Unexpected C# extractor error in Program.cs: Unable to resolve target for call. (Compilation error?) | 2 |
diff --git a/csharp/ql/test/library-tests/diagnostics/DiagnosticExtractorErrors.qlref b/csharp/ql/test/library-tests/diagnostics/DiagnosticExtractorErrors.qlref
new file mode 100644
index 000000000000..7068705cc1be
--- /dev/null
+++ b/csharp/ql/test/library-tests/diagnostics/DiagnosticExtractorErrors.qlref
@@ -0,0 +1 @@
+Diagnostics/DiagnosticExtractionErrors.ql
diff --git a/csharp/ql/test/library-tests/diagnostics/DiagnosticNoExtractorErrors.expected b/csharp/ql/test/library-tests/diagnostics/DiagnosticNoExtractorErrors.expected
new file mode 100644
index 000000000000..17e4dc54b7dd
--- /dev/null
+++ b/csharp/ql/test/library-tests/diagnostics/DiagnosticNoExtractorErrors.expected
@@ -0,0 +1 @@
+| A.cs:0:0:0:0 | A.cs | |
diff --git a/csharp/ql/test/library-tests/diagnostics/DiagnosticNoExtractorErrors.qlref b/csharp/ql/test/library-tests/diagnostics/DiagnosticNoExtractorErrors.qlref
new file mode 100644
index 000000000000..7994e050699f
--- /dev/null
+++ b/csharp/ql/test/library-tests/diagnostics/DiagnosticNoExtractorErrors.qlref
@@ -0,0 +1 @@
+Diagnostics/DiagnosticNoExtractionErrors.ql
diff --git a/csharp/ql/test/library-tests/diagnostics/Program.cs b/csharp/ql/test/library-tests/diagnostics/Program.cs
new file mode 100644
index 000000000000..7f0a7e57d1cd
--- /dev/null
+++ b/csharp/ql/test/library-tests/diagnostics/Program.cs
@@ -0,0 +1,17 @@
+// semmle-extractor-options: --standalone
+
+using System;
+
+class Class
+{
+ static void Main(string[] args)
+ {
+ int z = GetParamLength(__arglist(1, 2));
+ }
+
+ public static int GetParamLength(__arglist)
+ {
+ ArgIterator iterator = new ArgIterator(__arglist);
+ return iterator.GetRemainingCount();
+ }
+}
diff --git a/csharp/ql/test/library-tests/standalone/errorrecovery/DiagnosticsAndErrors.expected b/csharp/ql/test/library-tests/standalone/errorrecovery/DiagnosticsAndErrors.expected
index 13bdd30b1c40..917d625c10c3 100644
--- a/csharp/ql/test/library-tests/standalone/errorrecovery/DiagnosticsAndErrors.expected
+++ b/csharp/ql/test/library-tests/standalone/errorrecovery/DiagnosticsAndErrors.expected
@@ -1,3 +1,13 @@
compilationMessages
extractorMessages
+| errors.cs:8:1:8:22 | Namespace not found |
+| errors.cs:24:31:24:32 | Failed to determine type |
+| errors.cs:24:31:24:40 | Failed to determine type |
+| errors.cs:24:31:24:40 | Unable to resolve target for call. (Compilation error?) |
+| errors.cs:24:38:24:39 | Failed to determine type |
+| errors.cs:57:20:57:20 | Failed to determine type |
+| errors.cs:93:45:93:45 | Failed to determine type |
+| errors.cs:93:45:93:45 | Failed to resolve name |
+| errors.cs:94:45:94:45 | Failed to determine type |
+| errors.cs:94:45:94:45 | Failed to resolve name |
| file://:0:0:0:0 | Extracting default argument value 'object RecordNumber = default' instead of 'object RecordNumber = -1'. The latter is not supported in C#. |