やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

C#プロジェクトにNullable要素を追加するツールを作った

 いちいち手作業で書くの面倒だったので。

成果物

概要

 .csprojファイルに<Nullable>enable</Nullable>を挿入する。ただし、<Nullable>要素が既存なら何もしない。

想定ユースケース

 dotnet3.0.100にてdotnet new ...コマンドを使いプロジェクトを作成した。しかし、<Nullable>が設定されていない。dotnet3のC#8.0以降は、常にnull安全なコードを書くようにしたい。そこで、C#8.0プロジェクトファイルに<Nullable>enable</Nullable>を挿入したい。

想定外

 dotnet3のC#8.0以降における.csprojプロジェクトファイルを想定している。ただし、それ以外のファイルにも書けてしまえる場合がある。拡張子が.csprojXML形式なら対象となる。バージョンが古いものなども含む。

 いずれにせよ、ファイルが破壊されてしまっても一切責任を持たない。

コマンド

csnull    # カレントディレクトリにある.csprojが対象

 将来、以下の機能を追加する可能性はある。だが、使う場面が少ないと判断したら実装しないと思う。

csnull -r # 再帰的。サブディレクトリにある.csprojも対象

# 以下 e, d, a, w は併用不可(相互に排他的)
csnull -e # <Nullable>enable</Nullable>(デフォルト。全省略時と同等)
csnull -d # <Nullable>disable</Nullable>
csnull -a # <Nullable>annotations</Nullable>
csnull -w # <Nullable>warnings</Nullable>

csnull -h # ヘルプ
csnull -v # バージョン

 以下のような機能は追記しない。

追加しない機能 理由
対象csprojファイルを検索するディレクトリを指定する カレントディレクトリで代用できる。無駄に複雑化する
<LangVersion>8.0</LangVersion>追記 何のために使うか謎。バージョン管理は本件と別問題。
<LangVersion>があり8.0以上か確認 デフォルトでは<LangVersion>が存在しないため無意味そう。
#nullableディレクティブ追記 今回の要件である新規プロジェクトが対象なら<Nullable>で一括設定したほうがよい。

処理

  1. カレントディレクトリから*.csprojファイルを探す
  2. 1から<Nullable>要素を探す
  3. もし2が存在しなければ<Nullable>enable</Nullable>を挿入する

ゴール

 デフォルトのcsprojは以下のような内容である。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

</Project>

 上記に<PropertyGroup>要素の子として<Nullable>enable</Nullable>を挿入したい。

 つまり以下のように変更したい。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

コード

Program.cs

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Xml; // XmlWriter 
using System.Text; // StringBuilder

namespace CSharpProjectNullabler
{
    class Program
    {
        static void Main(string[] args)
        {
            foreach (string path in GetProjectFiles()) {
                var xml = CreateNullableXDocument(path);
                using (var writer = CreateOmitXmlDeclarationWriter(path)) { xml.Save(writer); }
            }
        }
        static IEnumerable<string> GetProjectFiles()
        {
            //return Directory.GetFiles(System.Environment.CurrentDirectory, "*.csproj");
            return Directory.EnumerateFiles(System.Environment.CurrentDirectory, "*.csproj");
            // return Directory.EnumerateFiles(System.Environment.CurrentDirectory, "*.csproj", SearchOption.AllDirectories);
        }
        static XmlWriter CreateOmitXmlDeclarationWriter(string path)
        {
            XmlWriterSettings xws = new XmlWriterSettings();  
            xws.OmitXmlDeclaration = true; // XML宣言を出力しない
            xws.Indent = true;  
            return XmlWriter.Create(path, xws);
        }
        static XDocument CreateNullableXDocument(string path)
        {
            XDocument xml = XDocument.Load(path);
            Console.WriteLine($"xml: {xml}");
            Console.WriteLine($"<Project>: {xml.Element("Project")}");

            XElement project = xml.Element("Project");
            XElement propertyGroup = project.Element("PropertyGroup");
            XElement? nullable = propertyGroup.Element("Nullable");
            if (null == nullable) { propertyGroup.Add(new XElement("Nullable", "enable")); }
            Console.WriteLine($"propertyGroup: {propertyGroup}");
            // XML宣言が追記されてしまう! <?xml version="1.0" encoding="utf-8"?>
//            xml.Declaration = null; // 効果なし
//            xml.Save(path);
            return xml;
        }
    }
}

Releaseモードでビルドする

$ dotnet build --configuration Release
.NET Core 向け Microsoft (R) Build Engine バージョン 16.3.0+0f4c62fea
Copyright (C) Microsoft Corporation.All rights reserved.

  /tmp/work/CSharpProjectNullabler/CSharpProjectNullabler.csproj の復元が 131.63 ms で完了しました。
  CSharpProjectNullabler -> /tmp/work/CSharpProjectNullabler/bin/Release/netcoreapp3.0/CSharpProjectNullabler.dll

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:54.89

 Debugモードとほとんど変わらなかった。一部のファイルサイズがほんの少し小さくなったくらい。

解決した問題

 .csprojに以下コードが追加されてしまう。

<?xml version="1.0" encoding="utf-8"?>
XDocument xml = XDocument.Load(path);
xml.Declaration = null;

 nullにしても出力されてしまう。一体どうすれば出力されないようにできるの?

 なにこれ超面倒なんですけど。以下も参考にしつつコードを書いて解決できた。

XML宣言が出力されてしまう版

 もしXML宣言が勝手に追記されちゃってもいいなら、以下で済む。

static void Main(string[] args)
{
    foreach (string path in GetProjectFiles()) {
        SetNullable(path);
    }
}
static IEnumerable<string> GetProjectFiles()
{
    return Directory.EnumerateFiles(
        System.Environment.CurrentDirectory, "*.csproj");
}
static void SetNullable(string path)
{
    XDocument xml = XDocument.Load(path);
    XElement project = xml.Element("Project");
    XElement propertyGroup = project.Element("PropertyGroup");
    XElement? nullable = propertyGroup.Element("Nullable");
    if (null == nullable) {
        propertyGroup.Add(new XElement("Nullable", "enable"));
    }
    xml.Save(path);
}

 XML宣言出力のON/OFFくらいもっと簡単に設定したいのに。

所感

 ビルドしてできた実際のコマンド名は、プロジェクト名と同じCSharpProjectNullablerになってしまっている。csnullとか短い名前にしたい。出力ファイルのリネーム設定とかできないのかな?

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux