From 58459849597206decb9efab20e9f23d0afca69e1 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Thu, 1 Jan 2026 15:24:14 +0100 Subject: [PATCH] Fixed ConcurrentModificationException on compilerArguments --- .../compiler/csharp/CSharpCompiler.java | 220 ++++++------- .../csharp/DefaultCSharpCompilerParser.java | 9 +- .../CSharpCompilerGetArgumentsTest.java | 298 ++++++++++++++++++ 3 files changed, 392 insertions(+), 135 deletions(-) create mode 100644 plexus-compilers/plexus-compiler-csharp/src/test/java/org/codehaus/plexus/compiler/csharp/CSharpCompilerGetArgumentsTest.java diff --git a/plexus-compilers/plexus-compiler-csharp/src/main/java/org/codehaus/plexus/compiler/csharp/CSharpCompiler.java b/plexus-compilers/plexus-compiler-csharp/src/main/java/org/codehaus/plexus/compiler/csharp/CSharpCompiler.java index 2fe061776..ee2cb7946 100644 --- a/plexus-compilers/plexus-compiler-csharp/src/main/java/org/codehaus/plexus/compiler/csharp/CSharpCompiler.java +++ b/plexus-compilers/plexus-compiler-csharp/src/main/java/org/codehaus/plexus/compiler/csharp/CSharpCompiler.java @@ -28,9 +28,9 @@ import java.io.Writer; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -68,26 +68,16 @@ public class CSharpCompiler extends AbstractCompiler { private static final String[] DEFAULT_INCLUDES = {"**/**"}; - private Map compilerArguments; - - // ---------------------------------------------------------------------- - // - // ---------------------------------------------------------------------- - public CSharpCompiler() { super(CompilerOutputStyle.ONE_OUTPUT_FILE_FOR_ALL_INPUT_FILES, ".cs", null, null); } - // ---------------------------------------------------------------------- - // Compiler Implementation - // ---------------------------------------------------------------------- - @Override public String getCompilerId() { return "csharp"; } - public boolean canUpdateTarget(CompilerConfiguration configuration) throws CompilerException { + public boolean canUpdateTarget(CompilerConfiguration configuration) { return false; } @@ -130,37 +120,39 @@ public String[] createCommandLine(CompilerConfiguration config) throws CompilerE return buildCompilerArguments(config, CSharpCompiler.getSourceFiles(config)); } - // ---------------------------------------------------------------------- - // - // ---------------------------------------------------------------------- - - private Map getCompilerArguments(CompilerConfiguration config) { - if (compilerArguments != null) { - return compilerArguments; - } + /** + * Parse compiler arguments and normalize legacy colon-separated format. + * Converts arguments like "-main:MyClass" (stored as key with null value) + * into proper key-value pairs: "-main" -> "MyClass". + * + * @param config the compiler configuration + * @return normalized map of compiler arguments + */ + Map getCompilerArguments(CompilerConfiguration config) { + Map customArgs = config.getCustomCompilerArgumentsAsMap(); + Map normalizedArgs = new HashMap<>(); - compilerArguments = config.getCustomCompilerArgumentsAsMap(); + for (Map.Entry entry : customArgs.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); - Iterator i = compilerArguments.keySet().iterator(); + // Handle legacy format: "-main:MyClass" stored as key with null value + if (value == null && key.contains(":")) { + int colonIndex = key.indexOf(':'); + String actualKey = key.substring(0, colonIndex); + String actualValue = key.substring(colonIndex + 1); + normalizedArgs.put(actualKey, actualValue); - while (i.hasNext()) { - String orig = i.next(); - String v = compilerArguments.get(orig); - if (orig.contains(":") && v == null) { - String[] arr = orig.split(":"); - i.remove(); - String k = arr[0]; - v = arr[1]; - compilerArguments.put(k, v); if (config.isDebug()) { - System.out.println("transforming argument from " + orig + " to " + k + " = [" + v + "]"); + System.out.println("Normalized argument '" + key + "' to key='" + actualKey + "', value='" + + actualValue + "'"); } + } else { + normalizedArgs.put(key, value); } } - config.setCustomCompilerArgumentsAsMap(compilerArguments); - - return compilerArguments; + return normalizedArgs; } private String findExecutable(CompilerConfiguration config) { @@ -179,45 +171,53 @@ private String findExecutable(CompilerConfiguration config) { /* $ mcs --help - Mono C# compiler, (C) 2001 - 2003 Ximian, Inc. + Turbo C# compiler, Copyright 2001-2011 Novell, Inc., 2011-2016 Xamarin, Inc, 2016-2017 Microsoft Corp mcs [options] source-files - --about About the Mono C# compiler - -addmodule:MODULE Adds the module to the generated assembly - -checked[+|-] Set default context to checked - -codepage:ID Sets code page to the one in ID (number, utf8, reset) - -clscheck[+|-] Disables CLS Compliance verifications - -define:S1[;S2] Defines one or more symbols (short: /d:) - -debug[+|-], -g Generate debugging information - -delaysign[+|-] Only insert the public key into the assembly (no signing) - -doc:FILE XML Documentation file to generate - -keycontainer:NAME The key pair container used to strongname the assembly - -keyfile:FILE The strongname key file used to strongname the assembly - -langversion:TEXT Specifies language version modes: ISO-1 or Default - -lib:PATH1,PATH2 Adds the paths to the assembly link path - -main:class Specified the class that contains the entry point - -noconfig[+|-] Disables implicit references to assemblies - -nostdlib[+|-] Does not load core libraries - -nowarn:W1[,W2] Disables one or more warnings - -optimize[+|-] Enables code optimalizations - -out:FNAME Specifies output file - -pkg:P1[,Pn] References packages P1..Pn - -recurse:SPEC Recursively compiles the files in SPEC ([dir]/file) - -reference:ASS References the specified assembly (-r:ASS) - -target:KIND Specifies the target (KIND is one of: exe, winexe, - library, module), (short: /t:) - -unsafe[+|-] Allows unsafe code - -warnaserror[+|-] Treat warnings as errors - -warn:LEVEL Sets warning level (the highest is 4, the default is 2) - -help2 Show other help flags + --about About the Mono C# compiler + -addmodule:M1[,Mn] Adds the module to the generated assembly + -checked[+|-] Sets default aritmetic overflow context + -clscheck[+|-] Disables CLS Compliance verifications + -codepage:ID Sets code page to the one in ID (number, utf8, reset) + -define:S1[;S2] Defines one or more conditional symbols (short: -d) + -debug[+|-], -g Generate debugging information + -delaysign[+|-] Only insert the public key into the assembly (no signing) + -doc:FILE Process documentation comments to XML file + -fullpaths Any issued error or warning uses absolute file path + -help Lists all compiler options (short: -?) + -keycontainer:NAME The key pair container used to sign the output assembly + -keyfile:FILE The key file used to strongname the ouput assembly + -langversion:TEXT Specifies language version: ISO-1, ISO-2, 3, 4, 5, 6, Default or Experimental + -lib:PATH1[,PATHn] Specifies the location of referenced assemblies + -main:CLASS Specifies the class with the Main method (short: -m) + -noconfig Disables implicitly referenced assemblies + -nostdlib[+|-] Does not reference mscorlib.dll library + -nowarn:W1[,Wn] Suppress one or more compiler warnings + -optimize[+|-] Enables advanced compiler optimizations (short: -o) + -out:FILE Specifies output assembly name + -pathmap:K=V[,Kn=Vn] Sets a mapping for source path names used in generated output + -pkg:P1[,Pn] References packages P1..Pn + -platform:ARCH Specifies the target platform of the output assembly + ARCH can be one of: anycpu, anycpu32bitpreferred, arm, + x86, x64 or itanium. The default is anycpu. + -recurse:SPEC Recursively compiles files according to SPEC pattern + -reference:A1[,An] Imports metadata from the specified assembly (short: -r) + -reference:ALIAS=A Imports metadata using specified extern alias (short: -r) + -sdk:VERSION Specifies SDK version of referenced assemblies + VERSION can be one of: 2, 4, 4.5 (default) or a custom value + -target:KIND Specifies the format of the output assembly (short: -t) + KIND can be one of: exe, winexe, library, module + -unsafe[+|-] Allows to compile code which uses unsafe keyword + -warnaserror[+|-] Treats all warnings as errors + -warnaserror[+|-]:W1[,Wn] Treats one or more compiler warnings as errors + -warn:0-4 Sets warning level, the default is 4 (short -w:) + -helpinternal Shows internal and advanced compiler options Resources: - -linkresource:FILE[,ID] Links FILE as a resource - -resource:FILE[,ID] Embed FILE as a resource + -linkresource:FILE[,ID] Links FILE as a resource (short: -linkres) + -resource:FILE[,ID] Embed FILE as a resource (short: -res) -win32res:FILE Specifies Win32 resource file (.res) -win32icon:FILE Use this icon for the output @file Read response file for more options - - Options can be of the form -option or /option */ /* @@ -385,25 +385,14 @@ SHA1 or SHA256 (default). accessibility errors with what assembly they came from. */ - private String[] buildCompilerArguments(CompilerConfiguration config, String[] sourceFiles) - throws CompilerException { + String[] buildCompilerArguments(CompilerConfiguration config, String[] sourceFiles) throws CompilerException { List args = new ArrayList<>(); - if (config.isDebug()) { - args.add("/debug+"); - } else { - args.add("/debug-"); - } - // config.isShowWarnings() // config.getSourceVersion() // config.getTargetVersion() // config.getSourceEncoding() - // ---------------------------------------------------------------------- - // - // ---------------------------------------------------------------------- - for (String element : config.getClasspathEntries()) { File f = new File(element); @@ -433,122 +422,91 @@ private String[] buildCompilerArguments(CompilerConfiguration config, String[] s } } - // ---------------------------------------------------------------------- - // Main class - // ---------------------------------------------------------------------- - + // TODO: include all user compiler arguments and not only some! Map compilerArguments = getCompilerArguments(config); String mainClass = compilerArguments.get("-main"); - if (!StringUtils.isEmpty(mainClass)) { args.add("/main:" + mainClass); } - // ---------------------------------------------------------------------- // Xml Doc output - // ---------------------------------------------------------------------- - String doc = compilerArguments.get("-doc"); - if (!StringUtils.isEmpty(doc)) { args.add("/doc:" + new File(config.getOutputLocation(), config.getOutputFileName() + ".xml").getAbsolutePath()); } - // ---------------------------------------------------------------------- - // Nowarn option - // ---------------------------------------------------------------------- + // Debug option (full, pdbonly...) + String debug = compilerArguments.get("-debug"); + if (!StringUtils.isEmpty(debug)) { + args.add("/debug:" + debug); + } + // Nowarn option (w#1,w#2...) String nowarn = compilerArguments.get("-nowarn"); - if (!StringUtils.isEmpty(nowarn)) { args.add("/nowarn:" + nowarn); } - // ---------------------------------------------------------------------- // Out - Override output name, this is required for generating the unit test dll - // ---------------------------------------------------------------------- - String out = compilerArguments.get("-out"); - if (!StringUtils.isEmpty(out)) { args.add("/out:" + new File(config.getOutputLocation(), out).getAbsolutePath()); } else { args.add("/out:" + new File(config.getOutputLocation(), getOutputFile(config)).getAbsolutePath()); } - // ---------------------------------------------------------------------- // Resource File - compile in a resource file into the assembly being created - // ---------------------------------------------------------------------- String resourcefile = compilerArguments.get("-resourcefile"); - if (!StringUtils.isEmpty(resourcefile)) { String resourceTarget = compilerArguments.get("-resourcetarget"); args.add("/res:" + new File(resourcefile).getAbsolutePath() + "," + resourceTarget); } - // ---------------------------------------------------------------------- - // Target - type of assembly to produce, lib,exe,winexe etc... - // ---------------------------------------------------------------------- - + // Target - type of assembly to produce: library,exe,winexe... String target = compilerArguments.get("-target"); - if (StringUtils.isEmpty(target)) { args.add("/target:library"); } else { args.add("/target:" + target); } - // ---------------------------------------------------------------------- // remove MS logo from output (not applicable for mono) - // ---------------------------------------------------------------------- String nologo = compilerArguments.get("-nologo"); - - if (!StringUtils.isEmpty(nologo)) { + if (!StringUtils.isEmpty(nologo) && !"false".equalsIgnoreCase(nologo)) { args.add("/nologo"); } - // ---------------------------------------------------------------------- // Unsafe option - // ---------------------------------------------------------------------- String unsafe = compilerArguments.get("-unsafe"); - - if (!StringUtils.isEmpty(unsafe) && unsafe.equals("true")) { + if (!StringUtils.isEmpty(unsafe) && "true".equalsIgnoreCase(unsafe)) { args.add("/unsafe"); } - // ---------------------------------------------------------------------- // PreferredUILang option - // ---------------------------------------------------------------------- String preferreduilang = compilerArguments.get("-preferreduilang"); - if (!StringUtils.isEmpty(preferreduilang)) { args.add("/preferreduilang:" + preferreduilang); } - // ---------------------------------------------------------------------- // Utf8Output option - // ---------------------------------------------------------------------- String utf8output = compilerArguments.get("-utf8output"); - - if (!StringUtils.isEmpty(utf8output)) { - args.add("/utf8output:"); + if (!StringUtils.isEmpty(utf8output) && !"false".equals(utf8output)) { + args.add("/utf8output"); } - // ---------------------------------------------------------------------- // add any resource files - // ---------------------------------------------------------------------- this.addResourceArgs(config, args); - // ---------------------------------------------------------------------- // add source files - // ---------------------------------------------------------------------- - for (String sourceFile : sourceFiles) { - args.add(sourceFile); + Collections.addAll(args, sourceFiles); + + if (config.isDebug()) { + System.out.println("built compiler arguments:" + args); } - return args.toArray(new String[args.size()]); + return args.toArray(new String[0]); } private void addResourceArgs(CompilerConfiguration config, List args) { @@ -560,7 +518,7 @@ private void addResourceArgs(CompilerConfiguration config, List args) { scanner.addDefaultExcludes(); scanner.scan(); - List includedFiles = Arrays.asList(scanner.getIncludedFiles()); + String[] includedFiles = scanner.getIncludedFiles(); for (String name : includedFiles) { File filteredResource = new File(filteredResourceDir, name); String assemblyResourceName = this.convertNameToAssemblyResourceName(name); @@ -585,7 +543,7 @@ private File findResourceDir(CompilerConfiguration config) { if (tempResourcesDirAsString != null) { filteredResourceDir = new File(tempResourcesDirAsString); if (config.isDebug()) { - System.out.println("Found resourceDir at: " + filteredResourceDir.toString()); + System.out.println("Found resourceDir at: " + filteredResourceDir); } } else { if (config.isDebug()) { diff --git a/plexus-compilers/plexus-compiler-csharp/src/main/java/org/codehaus/plexus/compiler/csharp/DefaultCSharpCompilerParser.java b/plexus-compilers/plexus-compiler-csharp/src/main/java/org/codehaus/plexus/compiler/csharp/DefaultCSharpCompilerParser.java index 6ef8188a4..8769ee821 100644 --- a/plexus-compilers/plexus-compiler-csharp/src/main/java/org/codehaus/plexus/compiler/csharp/DefaultCSharpCompilerParser.java +++ b/plexus-compilers/plexus-compiler-csharp/src/main/java/org/codehaus/plexus/compiler/csharp/DefaultCSharpCompilerParser.java @@ -1,6 +1,7 @@ package org.codehaus.plexus.compiler.csharp; import org.codehaus.plexus.compiler.CompilerMessage; +import org.codehaus.plexus.compiler.CompilerMessage.Kind; import org.codehaus.plexus.util.StringUtils; /** @@ -60,7 +61,7 @@ private static boolean isOutputWithNoColumnNumber(String line) { private static CompilerMessage parseLineWithNoColumnNumber(String line) { String file = null; - boolean error = true; + Kind error = Kind.ERROR; int startline = -1; int startcolumn = -1; int endline = -1; @@ -88,7 +89,7 @@ private static CompilerMessage parseLineWithNoColumnNumber(String line) { message = line.substring(j + 1 + ERROR_PREFIX.length()); - error = line.contains(") error"); + error = line.contains(") error") ? Kind.ERROR : Kind.WARNING; } else { System.err.println("Unknown output: " + line); @@ -101,7 +102,7 @@ private static CompilerMessage parseLineWithNoColumnNumber(String line) { private static CompilerMessage parseLineWithColumnNumberAndLineNumber(String line) { String file = null; - boolean error = true; + Kind error = Kind.ERROR; int startline = -1; int startcolumn = -1; int endline = -1; @@ -145,7 +146,7 @@ private static CompilerMessage parseLineWithColumnNumberAndLineNumber(String lin message = line.substring(j + 1 + ERROR_PREFIX.length()); - error = line.contains("): error"); + error = line.contains("): error") ? Kind.ERROR : Kind.WARNING; } else { System.err.println("Unknown output: " + line); diff --git a/plexus-compilers/plexus-compiler-csharp/src/test/java/org/codehaus/plexus/compiler/csharp/CSharpCompilerGetArgumentsTest.java b/plexus-compilers/plexus-compiler-csharp/src/test/java/org/codehaus/plexus/compiler/csharp/CSharpCompilerGetArgumentsTest.java new file mode 100644 index 000000000..9d11880a0 --- /dev/null +++ b/plexus-compilers/plexus-compiler-csharp/src/test/java/org/codehaus/plexus/compiler/csharp/CSharpCompilerGetArgumentsTest.java @@ -0,0 +1,298 @@ +package org.codehaus.plexus.compiler.csharp; + +/* + * Copyright 2026 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.util.HashMap; +import java.util.Map; + +import org.codehaus.plexus.compiler.CompilerConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for {@link CSharpCompiler#getCompilerArguments(CompilerConfiguration)} method. + * Tests the normalization of legacy colon-separated argument formats to key-value pairs. + */ +public class CSharpCompilerGetArgumentsTest { + + private CSharpCompiler compiler; + private CompilerConfiguration config; + + @BeforeEach + public void setUp() { + compiler = new CSharpCompiler(); + config = new CompilerConfiguration(); + } + + @Test + public void testEmptyArguments() { + // Test with no custom arguments + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result, "Result should not be null"); + assertTrue(result.isEmpty(), "Result should be empty when no custom arguments"); + } + + @Test + public void testNormalArgumentsPassThrough() { + // Test that normal key-value arguments pass through unchanged + Map customArgs = new HashMap<>(); + customArgs.put("-target", "library"); + customArgs.put("-debug", "full"); + customArgs.put("-nologo", "true"); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("library", result.get("-target")); + assertEquals("full", result.get("-debug")); + assertEquals("true", result.get("-nologo")); + } + + @Test + public void testLegacyColonSeparatedFormat() { + // Test that legacy format "-main:MyClass" is normalized to "-main" -> "MyClass" + Map customArgs = new HashMap<>(); + customArgs.put("-main:MyClass", null); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("MyClass", result.get("-main")); + assertFalse(result.containsKey("-main:MyClass"), "Original format key should not exist"); + } + + @Test + public void testMultipleLegacyArguments() { + // Test multiple legacy format arguments + Map customArgs = new HashMap<>(); + customArgs.put("-main:Program", null); + customArgs.put("-doc:MyDoc.xml", null); + customArgs.put("-out:MyApp.dll", null); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("Program", result.get("-main")); + assertEquals("MyDoc.xml", result.get("-doc")); + assertEquals("MyApp.dll", result.get("-out")); + } + + @Test + public void testMixedNormalAndLegacyArguments() { + // Test combination of normal and legacy format arguments + Map customArgs = new HashMap<>(); + customArgs.put("-target", "library"); + customArgs.put("-main:MyApp", null); + customArgs.put("-debug", "pdbonly"); + customArgs.put("-nowarn:CS0168,CS0219", null); + customArgs.put("-unsafe", "true"); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(5, result.size()); + assertEquals("library", result.get("-target")); + assertEquals("MyApp", result.get("-main")); + assertEquals("pdbonly", result.get("-debug")); + assertEquals("CS0168,CS0219", result.get("-nowarn")); + assertEquals("true", result.get("-unsafe")); + } + + @Test + public void testColonSeparatedWithValue() { + // Test that colon-separated arguments with actual values are NOT treated as legacy + Map customArgs = new HashMap<>(); + customArgs.put("-pathmap:old=new", "somevalue"); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("somevalue", result.get("-pathmap:old=new")); + assertNull(result.get("-pathmap"), "Should not split when value is present"); + } + + @Test + public void testMultipleColonsInLegacyFormat() { + // Test arguments with multiple colons (e.g., paths) + Map customArgs = new HashMap<>(); + customArgs.put("-keyfile:C:\\path\\to\\key.snk", null); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(1, result.size()); + // Should split at first colon only + assertEquals("C:\\path\\to\\key.snk", result.get("-keyfile")); + } + + @Test + public void testEmptyValueAfterColon() { + // Test legacy format with empty value after colon + Map customArgs = new HashMap<>(); + customArgs.put("-somearg:", null); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("", result.get("-somearg")); + } + + @Test + public void testStatelessBehavior() { + // Test that method is stateless - multiple calls with different configs + // should not affect each other + Map customArgs1 = new HashMap<>(); + customArgs1.put("-main:App1", null); + customArgs1.put("-target", "exe"); + + CompilerConfiguration config1 = new CompilerConfiguration(); + config1.setCustomCompilerArgumentsAsMap(customArgs1); + + Map result1 = compiler.getCompilerArguments(config1); + + // Second call with different arguments + Map customArgs2 = new HashMap<>(); + customArgs2.put("-main:App2", null); + customArgs2.put("-target", "library"); + + CompilerConfiguration config2 = new CompilerConfiguration(); + config2.setCustomCompilerArgumentsAsMap(customArgs2); + + Map result2 = compiler.getCompilerArguments(config2); + + // Verify first result wasn't affected by second call + assertEquals("App1", result1.get("-main")); + assertEquals("exe", result1.get("-target")); + + // Verify second result is correct + assertEquals("App2", result2.get("-main")); + assertEquals("library", result2.get("-target")); + } + + @Test + public void testDoesNotModifyOriginalConfig() { + // Test that original config map is not modified + Map customArgs = new HashMap<>(); + customArgs.put("-main:MyClass", null); + customArgs.put("-target", "library"); + + config.setCustomCompilerArgumentsAsMap(customArgs); + + // Store original state + Map originalArgs = new HashMap<>(config.getCustomCompilerArgumentsAsMap()); + + // Call method + compiler.getCompilerArguments(config); + + // Verify config wasn't modified + Map currentArgs = config.getCustomCompilerArgumentsAsMap(); + assertEquals(originalArgs.size(), currentArgs.size()); + assertTrue(currentArgs.containsKey("-main:MyClass")); + assertEquals(originalArgs.get("-target"), currentArgs.get("-target")); + } + + @Test + public void testNullValueInNormalArgument() { + // Test that null values in non-colon arguments are preserved + Map customArgs = new HashMap<>(); + customArgs.put("-someFlag", null); + customArgs.put("-normalArg", "value"); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(2, result.size()); + assertNull(result.get("-someFlag"), "Null value should be preserved for non-colon args"); + assertEquals("value", result.get("-normalArg")); + } + + @Test + public void testRealWorldScenario() { + // Test with realistic combination of arguments + Map customArgs = new HashMap<>(); + customArgs.put("-target", "library"); + customArgs.put("-out:MyLibrary.dll", null); + customArgs.put("-debug:full", null); + customArgs.put("-nowarn:CS0168", null); + customArgs.put("-nologo", "true"); + customArgs.put("-unsafe", "false"); + customArgs.put("-doc:Documentation.xml", null); + customArgs.put("-main:Program", null); + + config.setCustomCompilerArgumentsAsMap(customArgs); + Map result = compiler.getCompilerArguments(config); + + assertNotNull(result); + assertEquals(8, result.size()); + + // Verify normal arguments + assertEquals("library", result.get("-target")); + assertEquals("true", result.get("-nologo")); + assertEquals("false", result.get("-unsafe")); + + // Verify normalized legacy arguments + assertEquals("MyLibrary.dll", result.get("-out")); + assertEquals("full", result.get("-debug")); + assertEquals("CS0168", result.get("-nowarn")); + assertEquals("Documentation.xml", result.get("-doc")); + assertEquals("Program", result.get("-main")); + } + + @Test + public void testScenarioFromPR() throws Exception { + Map customArgs = new HashMap<>(); + customArgs.put("-nologo:true", null); + customArgs.put("-nowarn:0414", null); + customArgs.put("-utf8output:true", null); + customArgs.put("-target:exe", null); + customArgs.put("-out:${project.artifactId}.exe", null); + + config.setCustomCompilerArgumentsAsMap(customArgs); + String[] sourceFiles = {"src/main/csharp/App.cs"}; + String[] result = compiler.buildCompilerArguments(config, sourceFiles); + + assertNotNull(result); + assertEquals(6, result.length); + + // Verify + assertEquals("/nowarn:0414", result[0]); + assertTrue(result[1].startsWith("/out:")); + assertEquals("/target:exe", result[2]); + assertEquals("/nologo", result[3]); + assertEquals("/utf8output", result[4]); + assertTrue(result[5].endsWith("App.cs")); + } +}