diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md
index a1ac5bfbd8..79e4f4cabe 100644
--- a/DEVELOPER_GUIDE.md
+++ b/DEVELOPER_GUIDE.md
@@ -1205,11 +1205,11 @@ The shared GitHub HTTP client uses an explicit 10-second submission timeout by d
### Heuristic source code guard (not a security boundary)
-The description, context, and optional tool invocation context fields pass through `SourceCodeDetector` before storage and optional GitHub submission. This heuristic rejects common pasted code patterns (multi-line blocks, fenced code, import runs, function definitions) but intentionally allows short inline code examples so gap descriptions remain useful. It is **not a security boundary** — a determined agent could bypass it. The guard is a best-effort filter to catch accidental code inclusion, not a guarantee that no code-like text will ever be transmitted.
+The description, context, and optional tool invocation context fields pass through `SourceCodeDetector` before storage and optional GitHub submission. This heuristic rejects common pasted code patterns (multi-line blocks, backtick or tilde fenced code, import runs, function definitions) but intentionally allows short inline code examples so gap descriptions remain useful. Rejections return a bounded `source_code_rejection` object with the rejected field name and stable `reason_code`; they do not echo the rejected text. It is **not a security boundary** — a determined agent could bypass it. The guard is a best-effort filter to catch accidental code inclusion, not a guarantee that no code-like text will ever be transmitted.
### SourceCodeDetector design
-`SourceCodeDetector` uses five independent heuristics to reject text that looks like pasted source code. Each heuristic is implemented as a clearly named private method with detailed comments explaining what it detects and why. The class is designed for readability: anyone reviewing the open-source code can verify the detection logic and confirm that no source code passes through.
+`SourceCodeDetector` uses six independent heuristics to reject text that looks like pasted source code. Each heuristic is implemented as a clearly named private method with detailed comments explaining what it detects and why, and maps to a stable reason code such as `statement-ending`, `indented-code-lines`, `block-structure`, `repeated-imports`, `function-definition`, or `fenced-code-block`. The class is designed for readability: anyone reviewing the open-source code can verify the detection logic and confirm that no source code passes through.
The detector intentionally allows short inline code examples (e.g. `` `const foo = () => {}` ``) and only rejects multi-line code blocks. False negatives (missing some code) are acceptable; false positives (rejecting valid descriptions) are not.
@@ -3435,11 +3435,11 @@ upstream Issue を作成する前に、`GitHubIssueReporter` は同じ SHA256
### ヒューリスティックなソースコードガード(セキュリティ境界ではない)
-description、context、および任意の tool invocation context フィールドは、保存およびオプションの GitHub 送信前に `SourceCodeDetector` を通過する。このヒューリスティックは一般的なコードコピペパターン(複数行ブロック、フェンスドコード、import の連打、関数定義)を拒否するが、ギャップの説明として有用な短いインラインコード例は意図的に許容する。これは**セキュリティ境界ではない** — 意図的に回避しようとするエージェントは回避できる。このガードはコードの誤混入を防ぐベストエフォートのフィルタであり、コード的テキストが一切送信されないことの保証ではない。
+description、context、および任意の tool invocation context フィールドは、保存およびオプションの GitHub 送信前に `SourceCodeDetector` を通過する。このヒューリスティックは一般的なコードコピペパターン(複数行ブロック、バッククォートまたはチルダのフェンスドコード、import の連打、関数定義)を拒否するが、ギャップの説明として有用な短いインラインコード例は意図的に許容する。拒否時は、拒否対象フィールド名と安定した `reason_code` を持つ上限付きの `source_code_rejection` object を返し、拒否された本文は反映しない。これは**セキュリティ境界ではない** — 意図的に回避しようとするエージェントは回避できる。このガードはコードの誤混入を防ぐベストエフォートのフィルタであり、コード的テキストが一切送信されないことの保証ではない。
### SourceCodeDetector の設計
-`SourceCodeDetector` は5つの独立したヒューリスティックを使って、コピペされたソースコードに見えるテキストを拒否する。各ヒューリスティックは明確な名前の private メソッドとして実装され、何を検出し、なぜそれがソースコードの兆候なのかを詳細なコメントで説明している。可読性を重視して設計されており、オープンソースのコードをレビューする誰もが検出ロジックを検証し、ソースコードが通過しないことを確認できる。
+`SourceCodeDetector` は6つの独立したヒューリスティックを使って、コピペされたソースコードに見えるテキストを拒否する。各ヒューリスティックは明確な名前の private メソッドとして実装され、何を検出し、なぜそれがソースコードの兆候なのかを詳細なコメントで説明している。また、`statement-ending`、`indented-code-lines`、`block-structure`、`repeated-imports`、`function-definition`、`fenced-code-block` のような安定した理由コードに対応する。可読性を重視して設計されており、オープンソースのコードをレビューする誰もが検出ロジックを検証し、ソースコードが通過しないことを確認できる。
短いインラインコード例(例: `` `const foo = () => {}` ``)は意図的に許容し、複数行のコードブロックのみを拒否する。偽陰性(一部のコードの見逃し)は許容する。偽陽性(有効な説明の拒否)は許容しない。
diff --git a/USER_GUIDE.md b/USER_GUIDE.md
index 361bd8540a..dc82f45a1e 100644
--- a/USER_GUIDE.md
+++ b/USER_GUIDE.md
@@ -2343,7 +2343,7 @@ The MCP `tools/list` response includes an `examples` array for every registered
| `backfill_fold` | Upgrade folded-name keys in an existing DB without reparsing source files |
| `suggest_improvement` | Submit structured improvement suggestions or error reports |
-`suggest_improvement` always stores accepted suggestions locally. Its response includes `submitted_to_github` and `github_submission_reason` so clients can distinguish `submitted`, `token_not_configured`, `repo_not_configured`, `network_error`, and `api_error`; failed GitHub attempts also include `github_submission_error`.
+`suggest_improvement` always stores accepted suggestions locally. Its response includes `submitted_to_github` and `github_submission_reason` so clients can distinguish `submitted`, `token_not_configured`, `repo_not_configured`, `network_error`, and `api_error`; failed GitHub attempts also include `github_submission_error`. If the source-code guard rejects `description`, `context`, or `toolInvocationContext`, the error `structuredContent` includes `source_code_rejection.field` and `source_code_rejection.reason_code` without echoing the rejected text.
The MCP `index` tool returns a `diagnostics` object when non-fatal indexing problems occur. It includes category counts and up to 50 bounded items for recoverable indexing errors and skipped file-size measurements; item paths are project-relative when possible, and messages are redacted and bounded so permission or path failures can be acted on without leaking local absolute paths or token-shaped values.
@@ -4876,7 +4876,7 @@ OpenAI Codex CLI (`codex.json` または `~/.codex/config.json`):
| `backfill_fold` | 既存 DB の folded-name key をソース再解析なしで更新 |
| `suggest_improvement` | 構造化された改善提案またはエラー報告を送信 |
-`suggest_improvement` は受理した提案を常にローカル保存します。応答には `submitted_to_github` と `github_submission_reason` が含まれ、クライアントは `submitted`、`token_not_configured`、`repo_not_configured`、`network_error`、`api_error` を区別できます。GitHub 送信に失敗した場合は `github_submission_error` も含まれます。
+`suggest_improvement` は受理した提案を常にローカル保存します。応答には `submitted_to_github` と `github_submission_reason` が含まれ、クライアントは `submitted`、`token_not_configured`、`repo_not_configured`、`network_error`、`api_error` を区別できます。GitHub 送信に失敗した場合は `github_submission_error` も含まれます。ソースコードガードが `description`、`context`、または `toolInvocationContext` を拒否した場合、エラーの `structuredContent` には拒否された本文を反映せずに `source_code_rejection.field` と `source_code_rejection.reason_code` が含まれます。
MCP の `index` tool は、致命的ではない indexing 問題が発生した場合に `diagnostics` object を返します。recoverable な indexing error と file-size 測定 skip について category count と最大 50 件の bounded item を含み、path は可能な限り project-relative、message は redaction と上限適用済みなので、local absolute path や token 風の値を漏らさず permission / path 問題を判断できます。
diff --git a/changelog.d/unreleased/3830.fixed.md b/changelog.d/unreleased/3830.fixed.md
new file mode 100644
index 0000000000..72fec00554
--- /dev/null
+++ b/changelog.d/unreleased/3830.fixed.md
@@ -0,0 +1,18 @@
+---
+category: fixed
+issues:
+ - 3830
+affected:
+ - src/CodeIndex/Cli/SourceCodeDetector.cs
+ - src/CodeIndex/Mcp/McpToolHandlers.cs
+ - USER_GUIDE.md
+ - DEVELOPER_GUIDE.md
+---
+
+## English
+
+- **Source-code guard rejections now report stable reasons without echoing source text (#3830)** — `suggest_improvement` source-code guard errors now include a bounded `source_code_rejection.reason_code`, and the detector recognizes tilde Markdown fences in addition to backtick fences.
+
+## 日本語
+
+- **ソースコードガードの拒否がソース本文を反映せず安定した理由を返すようになりました (#3830)** — `suggest_improvement` のソースコードガードエラーは上限付きの `source_code_rejection.reason_code` を含むようになり、detector はバッククォート fence に加えてチルダの Markdown fence も認識します。
diff --git a/src/CodeIndex/Cli/SourceCodeDetector.cs b/src/CodeIndex/Cli/SourceCodeDetector.cs
index 1c28369943..eb9ea2cb9c 100644
--- a/src/CodeIndex/Cli/SourceCodeDetector.cs
+++ b/src/CodeIndex/Cli/SourceCodeDetector.cs
@@ -3,6 +3,17 @@
namespace CodeIndex.Cli;
+///
+/// Source-code detection result with a stable reason code for diagnostics.
+/// 診断用の安定した理由コードを持つソースコード検出結果。
+///
+public readonly record struct SourceCodeDetectionResult(bool ContainsSourceCode, string? ReasonCode)
+{
+ public static SourceCodeDetectionResult NoSourceCode { get; } = new(false, null);
+
+ public static SourceCodeDetectionResult Detected(string reasonCode) => new(true, reasonCode);
+}
+
///
/// Detects whether a text string likely contains source code.
/// テキスト文字列にソースコードが含まれている可能性があるかを検出する。
@@ -87,6 +98,13 @@ public static class SourceCodeDetector
///
private const int ConsecutiveImportThreshold = 3;
+ public const string ReasonStatementEnding = "statement-ending";
+ public const string ReasonIndentedCodeLines = "indented-code-lines";
+ public const string ReasonBlockStructure = "block-structure";
+ public const string ReasonRepeatedImports = "repeated-imports";
+ public const string ReasonFunctionDefinition = "function-definition";
+ public const string ReasonFencedCodeBlock = "fenced-code-block";
+
// ---------------------------------------------------------------
// Public API / 公開API
// ---------------------------------------------------------------
@@ -101,10 +119,18 @@ public static class SourceCodeDetector
///
/// The text to inspect / 検査するテキスト
/// true if source code is likely present / ソースコードが含まれる可能性がある場合 true
- public static bool ContainsSourceCode(string text)
+ public static bool ContainsSourceCode(string? text) => Detect(text).ContainsSourceCode;
+
+ ///
+ /// Returns a detection result with a stable reason code when source code is likely present.
+ /// ソースコードが含まれる可能性がある場合、安定した理由コード付きの検出結果を返す。
+ ///
+ /// The text to inspect / 検査するテキスト
+ /// Detection result / 検出結果
+ public static SourceCodeDetectionResult Detect(string? text)
{
if (string.IsNullOrWhiteSpace(text))
- return false;
+ return SourceCodeDetectionResult.NoSourceCode;
// Run each heuristic independently.
// Any single match is sufficient to flag the text.
@@ -112,24 +138,24 @@ public static bool ContainsSourceCode(string text)
// 1つでもマッチすればテキストをフラグする。
if (HasCodeStatementPattern(text))
- return true;
+ return SourceCodeDetectionResult.Detected(ReasonStatementEnding);
if (HasConsecutiveCodeLines(text))
- return true;
+ return SourceCodeDetectionResult.Detected(ReasonIndentedCodeLines);
if (HasBlockStructure(text))
- return true;
+ return SourceCodeDetectionResult.Detected(ReasonBlockStructure);
if (HasRepeatedImports(text))
- return true;
+ return SourceCodeDetectionResult.Detected(ReasonRepeatedImports);
if (HasMultiLineFunctionDefinition(text))
- return true;
+ return SourceCodeDetectionResult.Detected(ReasonFunctionDefinition);
if (HasFencedCodeBlock(text))
- return true;
+ return SourceCodeDetectionResult.Detected(ReasonFencedCodeBlock);
- return false;
+ return SourceCodeDetectionResult.NoSourceCode;
}
// ---------------------------------------------------------------
@@ -522,24 +548,24 @@ private static bool IsFunctionDefinitionLine(string trimmedLine)
///
private static bool HasFencedCodeBlock(string text)
{
- bool inFence = false;
+ char activeMarker = '\0';
+ int activeMarkerLength = 0;
int contentLines = 0;
foreach (var rawLine in EnumerateLines(text))
{
- var trimmed = rawLine.Trim();
-
- // Check for fence delimiter (``` with optional language tag)
- // フェンス区切り(```+任意の言語タグ)を検査
- if (trimmed.StartsWith("```", StringComparison.Ordinal))
+ // Check for fence delimiter (``` or ~~~ with optional language tag)
+ // フェンス区切り(``` または ~~~+任意の言語タグ)を検査
+ if (TryReadMarkdownFence(rawLine, out var marker, out var markerLength))
{
- if (!inFence)
+ if (activeMarker == '\0')
{
// Opening fence / 開始フェンス
- inFence = true;
+ activeMarker = marker;
+ activeMarkerLength = markerLength;
contentLines = 0;
}
- else
+ else if (marker == activeMarker && markerLength >= activeMarkerLength)
{
// Closing fence — if there was at least 1 content line,
// this is a fenced code block.
@@ -547,18 +573,53 @@ private static bool HasFencedCodeBlock(string text)
// フェンスドコードブロックである。
if (contentLines >= 1)
return true;
- inFence = false;
+ activeMarker = '\0';
+ activeMarkerLength = 0;
+ }
+ else
+ {
+ contentLines++;
}
continue;
}
- if (inFence && trimmed.Length > 0)
+ if (activeMarker != '\0' && rawLine.Trim().Length > 0)
contentLines++;
}
return false;
}
+ private static bool TryReadMarkdownFence(string line, out char marker, out int markerLength)
+ {
+ marker = '\0';
+ markerLength = 0;
+ var index = 0;
+ while (index < line.Length && line[index] is ' ' or '\t')
+ index++;
+ if (index >= line.Length)
+ return false;
+
+ var candidate = line[index];
+ if (candidate is not ('`' or '~'))
+ return false;
+
+ while (index < line.Length && line[index] == candidate)
+ {
+ markerLength++;
+ index++;
+ }
+
+ if (markerLength < 3)
+ {
+ markerLength = 0;
+ return false;
+ }
+
+ marker = candidate;
+ return true;
+ }
+
private static IEnumerable EnumerateLines(string text)
{
var start = 0;
diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs
index 8a59967b4a..4418dd0291 100644
--- a/src/CodeIndex/Mcp/McpToolHandlers.cs
+++ b/src/CodeIndex/Mcp/McpToolHandlers.cs
@@ -6803,13 +6803,35 @@ private async Task ExecuteSuggestImprovementAsync(JsonNode? id, JsonNo
// 3. Source code leak detection — reject if code is detected
// ソースコード漏洩検出 — コードが検出されたら拒否
- if (SourceCodeDetector.ContainsSourceCode(description))
- return CreateToolErrorResponse(id, "Description appears to contain source code. Please describe the gap in natural language without including code.");
+ var descriptionDetection = SourceCodeDetector.Detect(description);
+ if (descriptionDetection.ContainsSourceCode)
+ return CreateSourceCodeDetectedErrorResponse(
+ id,
+ "description",
+ descriptionDetection,
+ "Description appears to contain source code. Please describe the gap in natural language without including code.");
+
+ if (context != null)
+ {
+ var contextDetection = SourceCodeDetector.Detect(context);
+ if (contextDetection.ContainsSourceCode)
+ return CreateSourceCodeDetectedErrorResponse(
+ id,
+ "context",
+ contextDetection,
+ "Context appears to contain source code. Please describe what you were trying to do without including code.");
+ }
- if (context != null && SourceCodeDetector.ContainsSourceCode(context))
- return CreateToolErrorResponse(id, "Context appears to contain source code. Please describe what you were trying to do without including code.");
- if (toolInvocationContext != null && SourceCodeDetector.ContainsSourceCode(toolInvocationContext))
- return CreateToolErrorResponse(id, "Tool invocation context appears to contain source code. Please describe the invocation without including code.");
+ if (toolInvocationContext != null)
+ {
+ var invocationDetection = SourceCodeDetector.Detect(toolInvocationContext);
+ if (invocationDetection.ContainsSourceCode)
+ return CreateSourceCodeDetectedErrorResponse(
+ id,
+ "toolInvocationContext",
+ invocationDetection,
+ "Tool invocation context appears to contain source code. Please describe the invocation without including code.");
+ }
var samplingDecision = ResolveSuggestionSamplingDecision();
var samplingAttempt = await TrySampleSuggestionMetadataAsync(
@@ -6939,6 +6961,29 @@ private async Task ExecuteSuggestImprovementAsync(JsonNode? id, JsonNo
return CreateToolResult(id, "Suggestion recorded. Thank you for the feedback.", payload);
}
+ private JsonObject CreateSourceCodeDetectedErrorResponse(
+ JsonNode? id,
+ string field,
+ SourceCodeDetectionResult detection,
+ string message)
+ {
+ var extraData = new JsonObject
+ {
+ ["source_code_rejection"] = new JsonObject
+ {
+ ["field"] = field,
+ ["reason_code"] = detection.ReasonCode ?? "unknown",
+ },
+ };
+ return CreateToolErrorResponse(
+ id,
+ message,
+ category: McpErrorEnvelope.CategoryInvalidArgument,
+ suggestion: "Describe the gap in natural language without including code.",
+ retrySafe: false,
+ extraData: extraData);
+ }
+
private static string[]? ReadEvidencePaths(JsonNode? node, out string? error)
{
error = null;
diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs
index 3e33748812..b674dbe504 100644
--- a/tests/CodeIndex.Tests/McpServerTests.cs
+++ b/tests/CodeIndex.Tests/McpServerTests.cs
@@ -14840,6 +14840,37 @@ public void SuggestImprovement_SourceCodeInDescription_ReturnsError()
Assert.True(response["result"]!["isError"]!.GetValue());
Assert.Contains("source code", response["result"]!["content"]![0]!["text"]!.GetValue());
+ var rejection = response["result"]!["structuredContent"]!["source_code_rejection"]!;
+ Assert.Equal("description", rejection["field"]!.GetValue());
+ Assert.Equal(SourceCodeDetector.ReasonStatementEnding, rejection["reason_code"]!.GetValue());
+ }
+
+ [Fact]
+ public void SuggestImprovement_SourceCodeFenceRejectionIncludesBoundedReason_Issue3830()
+ {
+ const string leakedToken = "SHOULD_NOT_APPEAR_3830";
+ var desc = "The tool should explain this failure:\n"
+ + "~~~csharp\n"
+ + leakedToken + "\n"
+ + "~~~";
+ var json = new JsonObject
+ {
+ ["jsonrpc"] = "2.0",
+ ["id"] = 1,
+ ["method"] = "tools/call",
+ ["params"] = new JsonObject
+ {
+ ["name"] = "suggest_improvement",
+ ["arguments"] = new JsonObject { ["category"] = "other", ["description"] = desc }
+ }
+ };
+ var response = _server.HandleMessage(json)!;
+
+ Assert.True(response["result"]!["isError"]!.GetValue());
+ var rejection = response["result"]!["structuredContent"]!["source_code_rejection"]!;
+ Assert.Equal("description", rejection["field"]!.GetValue());
+ Assert.Equal(SourceCodeDetector.ReasonFencedCodeBlock, rejection["reason_code"]!.GetValue());
+ Assert.DoesNotContain(leakedToken, response.ToJsonString());
}
[Fact]
diff --git a/tests/CodeIndex.Tests/SourceCodeDetectorTests.cs b/tests/CodeIndex.Tests/SourceCodeDetectorTests.cs
index 86c09427db..780df1220f 100644
--- a/tests/CodeIndex.Tests/SourceCodeDetectorTests.cs
+++ b/tests/CodeIndex.Tests/SourceCodeDetectorTests.cs
@@ -89,6 +89,7 @@ public void AllowsEmptyOrWhitespace()
Assert.False(SourceCodeDetector.ContainsSourceCode(""));
Assert.False(SourceCodeDetector.ContainsSourceCode(" "));
Assert.False(SourceCodeDetector.ContainsSourceCode(null!));
+ Assert.Null(SourceCodeDetector.Detect(null).ReasonCode);
}
[Fact]
@@ -102,6 +103,18 @@ public void AllowsTwoLineCodeSnippet()
Assert.False(SourceCodeDetector.ContainsSourceCode(text));
}
+ [Fact]
+ public void AllowsProseWithBackticksAndTildes_Issue3830()
+ {
+ var text = "The docs mention `inline code`, ``` fences, and ~~~ fences in prose, "
+ + "but no fenced block is present.";
+
+ var result = SourceCodeDetector.Detect(text);
+
+ Assert.False(result.ContainsSourceCode);
+ Assert.Null(result.ReasonCode);
+ }
+
// ================================================================
// REJECTED inputs — these SHOULD be flagged as source code.
// 拒否される入力 — ソースコードとしてフラグされるべき。
@@ -124,6 +137,34 @@ public void RejectsMultiLineCodeBlock()
Assert.True(SourceCodeDetector.ContainsSourceCode(text));
}
+ [Theory]
+ [InlineData(
+ "alpha;\nbeta;\ngamma;\ndelta;\nepsilon;",
+ SourceCodeDetector.ReasonStatementEnding)]
+ [InlineData(
+ " var current = 1\n return current\n result.ToString()",
+ SourceCodeDetector.ReasonIndentedCodeLines)]
+ [InlineData(
+ "section {\nalpha\nbeta\ngamma\n}",
+ SourceCodeDetector.ReasonBlockStructure)]
+ [InlineData(
+ "import alpha\nimport beta\nimport gamma",
+ SourceCodeDetector.ReasonRepeatedImports)]
+ [InlineData(
+ "def process():\n return 1",
+ SourceCodeDetector.ReasonFunctionDefinition)]
+ [InlineData(
+ "Here is the snippet:\n~~~csharp\nreturn token;\n~~~",
+ SourceCodeDetector.ReasonFencedCodeBlock)]
+ public void Detect_ReturnsStableReasonCode_Issue3830(string text, string expectedReason)
+ {
+ var result = SourceCodeDetector.Detect(text);
+
+ Assert.True(result.ContainsSourceCode);
+ Assert.Equal(expectedReason, result.ReasonCode);
+ Assert.True(SourceCodeDetector.ContainsSourceCode(text));
+ }
+
[Fact]
public void RejectsCrLfMultiLineCodeBlock_Issue3068()
{
@@ -370,6 +411,47 @@ public void RejectsFencedCodeBlockMultipleLines()
Assert.True(SourceCodeDetector.ContainsSourceCode(text));
}
+ [Fact]
+ public void RejectsTildeFencedCodeBlock_Issue3830()
+ {
+ var text = "The problem:\n"
+ + "~~~python\n"
+ + "token\n"
+ + "~~~";
+
+ var result = SourceCodeDetector.Detect(text);
+
+ Assert.True(result.ContainsSourceCode);
+ Assert.Equal(SourceCodeDetector.ReasonFencedCodeBlock, result.ReasonCode);
+ }
+
+ [Fact]
+ public void RejectsIndentedTildeFencedCodeBlock_Issue3830()
+ {
+ var text = "The problem:\n"
+ + " ~~~python\n"
+ + "token\n"
+ + " ~~~";
+
+ var result = SourceCodeDetector.Detect(text);
+
+ Assert.True(result.ContainsSourceCode);
+ Assert.Equal(SourceCodeDetector.ReasonFencedCodeBlock, result.ReasonCode);
+ }
+
+ [Theory]
+ [InlineData(" ```csharp\nreturn token;\n ```")]
+ [InlineData("\t```csharp\nreturn token;\n\t```")]
+ [InlineData(" ~~~csharp\nreturn token;\n ~~~")]
+ [InlineData("\t~~~csharp\nreturn token;\n\t~~~")]
+ public void RejectsListIndentedFencedCodeBlocks_Issue3830(string text)
+ {
+ var result = SourceCodeDetector.Detect(text);
+
+ Assert.True(result.ContainsSourceCode);
+ Assert.Equal(SourceCodeDetector.ReasonFencedCodeBlock, result.ReasonCode);
+ }
+
[Fact]
public void AllowsEmptyFencedBlock()
{
@@ -382,6 +464,20 @@ public void AllowsEmptyFencedBlock()
Assert.False(SourceCodeDetector.ContainsSourceCode(text));
}
+ [Fact]
+ public void AllowsEmptyTildeFencedBlock_Issue3830()
+ {
+ var text = "See:\n"
+ + "~~~\n"
+ + "~~~\n"
+ + "Nothing there.";
+
+ var result = SourceCodeDetector.Detect(text);
+
+ Assert.False(result.ContainsSourceCode);
+ Assert.Null(result.ReasonCode);
+ }
+
[Fact]
public void RejectsShortUnindentedCodeInFence()
{