From f12319d5a8735127e1b7124c5d6b5e46fb6e822b Mon Sep 17 00:00:00 2001
From: WebFreak001 <gh@webfreak.org>
Date: Sat, 8 Jul 2023 22:38:48 +0200
Subject: [PATCH] add autofix whitespace collapsing API

---
 src/dscanner/analysis/run.d | 143 ++++++++++++++++++++++++++++++++++++
 1 file changed, 143 insertions(+)

diff --git a/src/dscanner/analysis/run.d b/src/dscanner/analysis/run.d
index 81ea1d5..3bc0eea 100644
--- a/src/dscanner/analysis/run.d
+++ b/src/dscanner/analysis/run.d
@@ -765,6 +765,149 @@ MessageSet analyze(string fileName, const Module m, const StaticAnalysisConfig a
 	return set;
 }
 
+void improveAutoFixWhitespace(scope const(char)[] code, AutoFix.CodeReplacement[] replacements)
+{
+	import std.ascii : isWhite;
+	import std.string : strip;
+	import std.utf : stride, strideBack;
+
+	enum WS
+	{
+		none, tab, space, newline
+	}
+
+	WS getWS(size_t i)
+	{
+		if (cast(ptrdiff_t) i < 0 || i >= code.length)
+			return WS.newline;
+		switch (code[i])
+		{
+		case '\n':
+		case '\r':
+			return WS.newline;
+		case '\t':
+			return WS.tab;
+		case ' ':
+			return WS.space;
+		default:
+			return WS.none;
+		}
+	}
+
+	foreach (ref replacement; replacements)
+	{
+		assert(replacement.range[0] >= 0 && replacement.range[0] < code.length
+			&& replacement.range[1] >= 0 && replacement.range[1] < code.length
+			&& replacement.range[0] <= replacement.range[1], "trying to autofix whitespace on code that doesn't match with what the replacements were generated for");
+
+		void growRight()
+		{
+			// this is basically: replacement.range[1]++;
+			if (code[replacement.range[1] .. $].startsWith("\r\n"))
+				replacement.range[1] += 2;
+			else if (replacement.range[1] < code.length)
+				replacement.range[1] += code.stride(replacement.range[1]);
+		}
+
+		void growLeft()
+		{
+			// this is basically: replacement.range[0]--;
+			if (code[0 .. replacement.range[0]].endsWith("\r\n"))
+				replacement.range[0] -= 2;
+			else if (replacement.range[0] > 0)
+				replacement.range[0] -= code.strideBack(replacement.range[0]);
+		}
+
+		if (replacement.newText.strip.length)
+		{
+			if (replacement.newText.startsWith(" "))
+			{
+				// we insert with leading space, but there is a space/NL/SOF before
+				// remove to-be-inserted space
+				if (getWS(replacement.range[0] - 1))
+					replacement.newText = replacement.newText[1 .. $];
+			}
+			if (replacement.newText.startsWith("]", ")"))
+			{
+				// when inserting `)`, consume regular space before
+				if (getWS(replacement.range[0] - 1) == WS.space)
+					growLeft();
+			}
+			if (replacement.newText.endsWith(" "))
+			{
+				// we insert with trailing space, but there is a space/NL/EOF after, chomp off
+				if (getWS(replacement.range[1]))
+					replacement.newText = replacement.newText[0 .. $ - 1];
+			}
+			if (replacement.newText.endsWith("[", "("))
+			{
+				if (getWS(replacement.range[1]))
+					growRight();
+			}
+		}
+		else if (!replacement.newText.length)
+		{
+			// after removing code and ending up with whitespace on both sides,
+			// collapse 2 whitespace into one
+			switch (getWS(replacement.range[1]))
+			{
+			case WS.newline:
+				switch (getWS(replacement.range[0] - 1))
+				{
+				case WS.newline:
+					// after removal we have NL ~ NL or SOF ~ NL,
+					// remove right NL
+					growRight();
+					break;
+				case WS.space:
+				case WS.tab:
+					// after removal we have space ~ NL,
+					// remove the space
+					growLeft();
+					break;
+				default:
+					break;
+				}
+				break;
+			case WS.space:
+			case WS.tab:
+				// for NL ~ space, SOF ~ space, space ~ space, tab ~ space,
+				// for NL ~ tab, SOF ~ tab, space ~ tab, tab ~ tab
+				// remove right space/tab
+				if (getWS(replacement.range[0] - 1))
+					growRight();
+				break;
+			default:
+				break;
+			}
+		}
+	}
+}
+
+unittest
+{
+	AutoFix.CodeReplacement r(int start, int end, string s)
+	{
+		return AutoFix.CodeReplacement([start, end], s);
+	}
+
+	string test(string code, AutoFix.CodeReplacement[] replacements...)
+	{
+		replacements.sort!"a.range[0] < b.range[0]";
+		improveAutoFixWhitespace(code, replacements);
+		foreach_reverse (r; replacements)
+			code = code[0 .. r.range[0]] ~ r.newText ~ code[r.range[1] .. $];
+		return code;
+	}
+
+	assert(test("import a;\nimport b;", r(0, 9, "")) == "import b;");
+	assert(test("import a;\r\nimport b;", r(0, 9, "")) == "import b;");
+	assert(test("import a;\nimport b;", r(8, 9, "")) == "import a\nimport b;");
+	assert(test("import a;\nimport b;", r(7, 8, "")) == "import ;\nimport b;");
+	assert(test("import a;\r\nimport b;", r(7, 8, "")) == "import ;\r\nimport b;");
+	assert(test("a b c", r(2, 3, "")) == "a c");
+}
+
 version (unittest)
 {
 	shared static this()