Dynamically creating a build plan for analyzing .NET project files using FxCop


It so happened that a year ago, I had to write a build plan using ant. It was intended for our small web project, executed on Hudson and had to produce: compilation, running NUnit tests, counting% coverage of the code for tests, finding duplicate code and identifying basic stylistic inconsistencies in the code. But this is an introduction, and then we’ll talk about writing a build plan for analyzing project files using FxCop.

So! Go!



Introductory



As usual, I broke the build plan into several components:

  1. dbdeploy.build.xml - responsible for creating a test database and rolling up scripts that appear
  2. fxcop.build.xml - is responsible for running FxCop analysis and processing of project files and building a report on problems found
  3. main.build.xml - here the main actions are performed to fill out the configs, automatically search for sln files for their assembly
  4. ncover.build.xml - in this part, a report is built on the coverage of code with tests
  5. simian.build.xml - and here is the report on duplication in the code.
  6. tests.build.xml - well, here we search for all NUnit tests in the project folder and run them


Such a modular construction makes it easy to exclude individual parts, separated by specific responsibilities. For you and me, it is up to the device of the fxcop.build.xml file to be considered.

Let's get started



At first I tried to transfer, a list of prepared paths to the analyzed files, using the command line, but as practice has shown, this is a dreary and long exercise. And not very reliable, since when expanding the project, it will be necessary to update the list of files for analysis. Then I began to look for ways to dynamically create a list of files and transfer FxCop through Ant. Since there were a lot of analyzed files, we needed exactly an automatic system for finding the necessary files and transferring them to FxCop. Rummaging on the Internet and reading the manual on the Ant-Contrib and Ant commands, I found what I needed. It was the subant team that made it possible to achieve the goal. But more about that below!

Implementation


Consider the device file. It contains several tasks:

  • clean-fxcop-result-folder - cleans the FxCop report folder and deletes the dynamically generated parameter file for FxCop
  • run-fxcop - the main task that starts the analysis of files FxCop'om
  • create-arguments - a task that processes paths to files suitable for analysis and writes line by line to a dynamically generated file of a sub build plan
  • write-head-part - writes a header to a dynamically generated sub-build file
  • write-footer-part - writes FxCop commands that complete the list of paths to the analyzed files


Next, consider the basic Apache Ant commands for solving the problem.

basename - allows you to get the file name with the extension from the full path.
loadfile - allows you to load certain data from a file. In this case, task is used to parse the proj of the .NET project file.
subant - allows you to carry out task from another build file, in this case, from a
propertyregex dynamically generated for FxCop
- allows you to select data using a given regular expression on the input line.
if - allows adding logic of execution depending on logical expressions to the build file.

Now you can proceed to consider each of the tasks separately.

write-head-part


	<target name="write-head-part">	  
		<echo file="${dynafile.path}\${dynafilename}">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;project name="dyna-fxcop-build" default="run-fx-cop-report-creation" basedir="."&gt;
	&lt;target name="run-fx-cop-report-creation"&gt;
		&lt;exec executable="${fxcop.path}\FxCopCmd.exe" failonerror="false"&gt;</echo>
	</target>


In this task, the standard header of the build plan is written to a file located along the path ${dynafile.path}\${dynafilename} , using &lt; &gt; , to escape characters < > . And also a task is recorded exec to pass the necessary parameters to the FxCop application. In this way, passing parameters through arg , it is possible to solve the problem of a long list of paths to the analyzed files.

create-arguments


	<target name="create-arguments">
        //Вывод пути до файла на консоль для информирования
		<echo message="${item.file}"/>
        //Получение имени файла с расширением, которое записывается в свойство filename
		<basename property="filename" file="${item.file}"/>
		
        //Загрузка в свойство output.path строк содержащих <OutputPath> из файла csproj
		<loadfile srcfile="${item.file}" property="output.path">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;OutputPath&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
        //Загрузка строк содержащих <OutputType> из файла csproj в свойство output.type
		<loadfile srcfile="${item.file}" property="output.type">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;OutputType&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
        //Загрузка строк содержащих <AssemblyName> из файла csproj в свойство assembly.name 
		<loadfile srcfile="${item.file}" property="assembly.name">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;AssemblyName&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
        //Выделение значения между открывающим и закрывающим тегом OutputPath и запись в output.path.info
		<propertyregex property="output.path.info" input="${output.path}"
					   regexp="&lt;OutputPath&gt;(.*?)&lt;/OutputPath&gt;" select="\1" />
		
        //Выделение значения между открывающим и закрывающим тегом OutputType и запись в output.type.info        
		<propertyregex property="output.type.info" input="${output.type}"
					   regexp="&lt;OutputType&gt;(.*?)&lt;/OutputType&gt;" select="\1" />
                       
		//Выделение значения между открывающим и закрывающим тегом AssemblyName и запись в assembly.name.info 			   
		<propertyregex property="assembly.name.info" input="${assembly.name}"
					   regexp="&lt;AssemblyName&gt;(.*?)&lt;/AssemblyName&gt;" select="\1" />
		
        //Получение пути до файла без имени файла
		<propertyregex property="item.path" input="${item.file}"
                       regexp="(.*)\\" select="\1" />

		<echo message="output.type.info = ${output.type.info}"/>
		<echo message="output.path = ${output.path}"/>
		
        //Формирование расширения файла в зависимости от значения в свойстве output.type.info
		<if>
			<contains string="WinExe" substring="${output.type.info}"/>
			<then>
				<property name="file.name.ext" value="${assembly.name.info}.exe"/>
			</then>
			<elseif>
				<contains string="Exe" substring="${output.type.info}"/>
				<then>
					<property name="file.name.ext" value="${assembly.name.info}.exe"/>
				</then>
			</elseif>
			<else>
				<property name="file.name.ext" value="${assembly.name.info}.dll"/>
			</else>
		</if>
		//Запись параметра <arg value=""/> с заполненным параметром value и записью данного значения в файл.
		<echo file="${dynafile.path}\${dynafilename}" append="true">				&lt;arg value="/f:${item.path}\${output.path.info}${file.name.ext}"/&gt;
		</echo>
	</target>


In this task, the processing of project files is carried out, with the extension csproj. From the file, the tag data is highlighted: OutputPath, OutputType and AssemblyName. This is necessary so as not to be guided by the name of the project file (since such project files in which the assembly name was changed) came across. And in the dynamically created file of the build plan, the arg lines for the exec task are written , with the / f: flag specified .

write-footer-part


<target name="write-footer-part">
		<echo file="${dynafile.path}\${dynafilename}" append="true">				&lt;arg value="/r:${fxcop.path}\Rules"/&gt;
						&lt;arg value="/o:${fxcop.report.full.path}"/&gt;
		&lt;/exec&gt;
	&lt;/target&gt;
&lt;/project&gt;</echo>
	</target>


This task records the final part, the dynamically generated build plan, appending the FxCop directives intended for setting the path to the rule /r:${fxcop.path}\Rules folder and the report output folder /o:${fxcop.report.full.path} . The closing tags for exec , traget and are also recorded project .

run-fxcop


<target name="run-fxcop">	
        //Запись заголовка динамического билд-плана
		<antcall target="write-head-part"/>
        //Перевод файла в режим добавления данных в конец
		<echo file="${dynafile.path}\${dynafilename}" append="true">
		</echo>
		<var name="dll.names" value=""/>
        //Перебор всех csproj файлов в папке проекта, с передачей пути до файла, записанного в переменной item.file,
        //в задачу create-arguments
		<foreach  target="create-arguments" param="item.file" inheritall="true">
			<fileset dir="${basedir}" casesensitive="no">
			  <include name="**/*.csproj"/>
              //Исключаем все что находится в папке /obj/Debug/
			  <exclude name="**/obj/Debug/**.*"/>
			</fileset>
		</foreach>
        //Запись заключительной части динамического билд-плана
		<antcall target="write-footer-part"/>
        //Выполнение задачи из динамически созданного билд-плана.
		<subant target="run-fx-cop-report-creation">
			<fileset dir="${dynafile.path}" includes="${dynafilename}"/>
		</subant>
	</target>


Well, the most important task of the FxCop build plan is to dynamically create, so to speak, a sub build plan. Using just such an approach, the analysis of files of the .NET project will be performed. In this task, through write-head-part , the header is written to a file that is created along the path ${dynafile.path}\${dynafilename} . Next, the file is transferred to the mode of adding data to the end, using the echo command with the parameter append="true" .

After these actions, iterates through the files with the extension csproj using foreach . In this case, the path to the file is written to the variable item.file, which is defined by param="item.file" . Well, so that ant does not view the contents of obj / Debug using the instruction /> , we put it in ignore.

Further, with the help write-footer-part , the final part of the dynamically generated build file is recorded.

And now the fun part! Since we, in a dynamic build plan, created a task with a name run-fx-cop-report-creation , now we can execute it using the subant task . In the parameters to subant, indicating the path to the dynamically generated file of the build plan, from which the run-fx-cop-report-creation task will be performed run-fx-cop-report-creation .

Conclusion



I hope that this material was interesting :) Thank you for your attention!

Full xml build file code for FxCop
<?xml version="1.0" encoding="UTF-8"?>

<project name="fxcop-xxx-project" default="run-fxcop" basedir=".">
	<property name="dynafile.path" value="${basedir}"/>
	<property name="dynafilename" value="dynabuild.xml"/>
	<property name="fxcop.report.dir" value="${basedir}\FxCopReports"/>
	<property name="fxcop.report.full.path" value="${fxcop.report.dir}\fxcop.report.xml"/>
	
	<target name="clean-fxcop-result-folder">
		<echo message="Cleaning FxCop result report dir, and dynamic xml"/>
		<delete>
			<fileset dir="${fxcop.report.dir}" includes="**/*.*"/>
		</delete>
		<delete file="${dynafile.path}\${dynafilename}" failonerror="false"/>
	</target>
	
	<target name="run-fxcop">	
		<antcall target="write-head-part"/>
		<echo file="${dynafile.path}\${dynafilename}" append="true">
		</echo>
		<var name="dll.names" value=""/>
		<foreach  target="create-arguments" param="item.file" inheritall="true">
			<fileset dir="${basedir}" casesensitive="no">
			  <include name="**/*.csproj"/>
			  <exclude name="**/obj/Debug/**.*"/>
			</fileset>
		</foreach>
		<antcall target="write-footer-part"/>
		<subant target="run-fx-cop-report-creation">
			<fileset dir="${dynafile.path}" includes="${dynafilename}"/>
		</subant>
	</target>
	
	<target name="create-arguments">
		<echo message="${item.file}"/>
		<basename property="filename" file="${item.file}"/>
					   
		<loadfile srcfile="${item.file}" property="output.path">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;OutputPath&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
		<loadfile srcfile="${item.file}" property="output.type">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;OutputType&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
		<loadfile srcfile="${item.file}" property="assembly.name">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;AssemblyName&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
		<propertyregex property="output.path.info" input="${output.path}"
					   regexp="&lt;OutputPath&gt;(.*?)&lt;/OutputPath&gt;" select="\1" />
					   
		<propertyregex property="output.type.info" input="${output.type}"
					   regexp="&lt;OutputType&gt;(.*?)&lt;/OutputType&gt;" select="\1" />
					   
		<propertyregex property="assembly.name.info" input="${assembly.name}"
					   regexp="&lt;AssemblyName&gt;(.*?)&lt;/AssemblyName&gt;" select="\1" />
		
		<propertyregex property="item.path" input="${item.file}"
                       regexp="(.*)\\" select="\1" />

		<echo message="output.type.info = ${output.type.info}"/>
		<echo message="output.path = ${output.path}"/>
		
		<if>
			<contains string="WinExe" substring="${output.type.info}"/>
			<then>
				<property name="file.name.ext" value="${assembly.name.info}.exe"/>
			</then>
			<elseif>
				<contains string="Exe" substring="${output.type.info}"/>
				<then>
					<property name="file.name.ext" value="${assembly.name.info}.exe"/>
				</then>
			</elseif>
			<else>
				<property name="file.name.ext" value="${assembly.name.info}.dll"/>
			</else>
		</if>
		
		<echo file="${dynafile.path}\${dynafilename}" append="true">				&lt;arg value="/f:${item.path}\${output.path.info}${file.name.ext}"/&gt;
		</echo>
	</target>
	
	
	<target name="write-head-part">	  
		<echo file="${dynafile.path}\${dynafilename}">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;project name="dyna-fxcop-build" default="run-fx-cop-report-creation" basedir="."&gt;
	&lt;target name="run-fx-cop-report-creation"&gt;
		&lt;exec executable="${fxcop.path}\FxCopCmd.exe" failonerror="false"&gt;</echo>
	</target>
	
	<target name="write-footer-part">
		<echo file="${dynafile.path}\${dynafilename}" append="true">				&lt;arg value="/r:${fxcop.path}\Rules"/&gt;
						&lt;arg value="/o:${fxcop.report.full.path}"/&gt;
		&lt;/exec&gt;
	&lt;/target&gt;
&lt;/project&gt;</echo>
	</target>
</project>