Динамическое создание билд-плана, для анализа файлов .NET проекта, посредством FxCop

в 4:30, , рубрики: .net, fxcop, разработка, метки: , ,

Так получилось, что год назад, мне пришлось написать билд-план с использованием ant. Он предназначался для нашего небольшого веб-проекта, исполнялся на Hudson и должен был производить: компиляцию, прогон NUnit тестов, подсчет % покрытия кода тестам, поиск дублирующегося кода и выявление основных стилистических несоответствий в коде. Но это вступление, а далее поговорим, о написании билд-плана для анализа файлов проекта, посредством FxCop.

И так! Поехали!

Вводная

Как водится, я разбил билд-план на несколько составных частей:

  1. dbdeploy.build.xml — отвечает за создание тестовой базы данных и накат появляющихся скриптов
  2. fxcop.build.xml — отвечает за запуск анализа и обработки FxCop'ом файлов проекта и построения отчета о найденных проблемах
  3. main.build.xml — тут производятся основные действия по заполнению конфигов, автоматическому поиску sln файлов для их сборки
  4. ncover.build.xml — в этой части производится построение отчета, о покрытии кода тестами
  5. simian.build.xml — а тут производится построение отчета, о дублировании в коде
  6. tests.build.xml — ну а тут производится поиск всех NUnit тестов в папке проекта и их запуск

Такое модульное построение позволяет легко исключать отдельные части, разделенные по конкретной ответственности. Нам же с Вами, предстоит рассмотреть именно устройство fxcop.build.xml файла.

Приступим

Сначала я пробовал передавать, список подготовленных путей до анализируемых файлов, посредством командной строки, но как показала практика, это занятие муторное и долгое. И не очень надежное, так как при расширении проекта, нужно будет обновлять и список файлов для анализа. Тогда я стал искать способы динамического формирования списка файлов и передачи FxCop посредством Ant. Так как анализируемых файлов было не мало, нужна была именно автоматическая система поиска нужных файлов и передача их FxCop. Покопавшись в интернете и почитав мануал по командам Ant-Contrib и Ant, нашел, то что надо. Именно команда subant позволила достичь поставленной цели. Но об этом ниже!

Реализация

Рассмотрим устройство файла. В нем присутствуют несколько задач:

  • clean-fxcop-result-folder — очищает папку отчетов FxCop и удаляет динамически сформированных файл параметров для FxCop
  • run-fxcop — главная задача, которая производит запуск анализа файлов FxCop'ом
  • create-arguments — задача, которая обрабатывает пути к файлам, пригодным для анализа и записывает построчно в динамически формируемый файл суб билд-плана
  • write-head-part — производит запись заголовка в динамически формируемый файл суб билд-плана
  • write-footer-part — производит запись команд FxCop, завершающей список путей до анализируемых файлов

Далее рассмотрим основные команды Apache Ant для решения задачи.

basename — позволяет получить имя файла с расширением из полного пути.
loadfile — позволяет загрузить определенные данных из файла. В данном случае, таска используется для разбора proj файла .NET проекта.
subant — позволяет выполнить таску из другого билд файла, в данном случае из динамически сформированного для FxCop
propertyregex — позволяет выполнить выборку данных посредством заданного регулярного выражения на входной строке.
if — позволяет добавить в билд файл логику выполнения зависящую от логических выражений.

Теперь можно перейти к рассмотрению каждой из задач отдельно.

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>

В данной задаче, производится запись стандартного заголовка билд-плана в файл с использованием &lt; &gt; для экранирования символов < >, в файл находящийся по пути ${dynafile.path}${dynafilename}. А так же производится запись задачи exec для передачи параметров приложению FxCop, необходимых параметров. Именно таким образом передавая параметры посредством arg, можно решить проблему передачи длинного списка путей до анализируемых файлов.

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>

В данной задаче производится обработка файлов проекта с расширением proj. Из файла выделяются данные тегов OutputPath, OutputType и AssemblyName для того, чтобы можно было не ориентироваться на название файла проекта (так как попадались такие файлы проектов в которых было изменено имя сборки). И в динамически создаваемый файл билд-плана записываются строки arg для задачи exec с указанием флага /f:.

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>

Эта задача производит запись заключительной части динамически формируемого билд-плана для FxCop, дописывая директивы FxCop, которые предназначены для указания пути папки с правилами /r:${fxcop.path}Rules и папки вывода отчета /o:${fxcop.report.full.path}. Так же производится запись закрывающих тегов для exec, traget и 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>

Ну и самая главная задача билд-плана для FxCop, которая и производит динамическое создание так сказать суб билд-плана для выполнения анализа файлов проекта .NET. В данной задаче посредством write-head-part записывается заголовок в файл, который создается по пути ${dynafile.path}${dynafilename}. Далее производится перевод файла в режим добавления данных в конец, посредством команды echo с параметром append="true".

После этих действий производится перебор файлов с расширением csproj при помощи foreach. При этом путь до файла записывается в переменную item.file, которая определена, посредством param="item.file". Ну и для того чтобы ant не просматривал содержимое obj/Debug, при помощи инструкции /> эта папка заносится в игнор.

Далее при помощи write-footer-part записывается заключительная часть динамически формируемого файла билда.

И теперь самое интересное! Так как мы, в динамическом билд-плане, создали задачу с именем run-fx-cop-report-creation, то теперь мы можем ее исполнить, посредством таски subant, с указанием пути до динамически сформированного файла билд-плана из которого собственно и будет выполнена указанная задача.

Заключение

Надеюсь что данный материал был интересен :) Спасибо за внимание!

Полный код xml билд-файла для 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>

Автор: CyberLight

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js