├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── scripts └── nuget-pack.bat └── src ├── T4Immutable-netstd2 ├── Attributes.cs ├── Helpers.cs ├── KeyValuePairHelper.cs ├── OptParam.cs ├── T4Immutable-netstd2.csproj └── T4Immutable.nuspec ├── T4Immutable.sln ├── T4Immutable.sln.DotSettings ├── content └── T4Immutable │ ├── Attributes.cs_ │ ├── Class.ttinclude │ ├── Field.ttinclude │ ├── Parser.ttinclude │ ├── Prop.ttinclude │ ├── Shared.ttinclude │ ├── Struct.ttinclude │ ├── T4Immutable.tt │ ├── TemplateFileManagerV2.1.ttinclude │ └── VisualStudioHelper.ttinclude └── readme.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /output/ 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # DNX 46 | project.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.[Pp]ublish.xml 144 | *.azurePubxml 145 | # TODO: Comment the next line if you want to checkin your web deploy settings 146 | # but database connection strings (with potential passwords) will be unencrypted 147 | *.pubxml 148 | *.publishproj 149 | 150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 151 | # checkin your Azure Web App publish settings, but sensitive information contained 152 | # in these scripts will be unencrypted 153 | PublishScripts/ 154 | 155 | # NuGet Packages 156 | *.nupkg 157 | # The packages folder can be ignored because of Package Restore 158 | **/packages/* 159 | # except build/, which is used as an MSBuild target. 160 | !**/packages/build/ 161 | # Uncomment if necessary however generally it will be regenerated when needed 162 | #!**/packages/repositories.config 163 | # NuGet v3's project.json files produces more ignoreable files 164 | *.nuget.props 165 | *.nuget.targets 166 | 167 | # Microsoft Azure Build Output 168 | csx/ 169 | *.build.csdef 170 | 171 | # Microsoft Azure Emulator 172 | ecf/ 173 | rcf/ 174 | 175 | # Windows Store app package directories and files 176 | AppPackages/ 177 | BundleArtifacts/ 178 | Package.StoreAssociation.xml 179 | _pkginfo.txt 180 | 181 | # Visual Studio cache files 182 | # files ending in .cache can be ignored 183 | *.[Cc]ache 184 | # but keep track of directories ending in .cache 185 | !*.[Cc]ache/ 186 | 187 | # Others 188 | ClientBin/ 189 | ~$* 190 | *~ 191 | *.dbmdl 192 | *.dbproj.schemaview 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # T4Immutable 2 | ### T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes. 3 | 4 | [![NuGet package](https://img.shields.io/nuget/v/T4Immutable.svg)](https://nuget.org/packages/T4Immutable) 5 | 6 | ## Table of contents 7 | * [Why use this?](#why) 8 | * [How do I start?](#starting) 9 | * [What's needed to make an immutable class?](#basics) 10 | * [How are collection (Array, List, Set, Dictionary... plus their Immutable versions) based properties handled?](#collections) 11 | * [Do generated classes serialize/deserialize correctly with JSON.NET / Protobuf.NET / others?](#serialization) 12 | * [I don't want X. Can I control what gets generated?](#codegen-options) 13 | * [Can I control the access level (public/private/...) of the constructor or the builder?](#constructor-builder-access-level) 14 | * [Constructor post-initalization / validation](#constructor-init-validation) 15 | * [Can I add extra attributes to each constructor parameter?](#constructor-param-attribs) 16 | * [How do I enforce automatic null checking for the constructor parameters? What about for the properties?](#null-checks) 17 | * [Constructor overrides](#constructor-overrides) 18 | * [How do I change the order of the arguments in the generated constructor?](#constructor-argument-order) 19 | * [How to specify property default values?](#default-values) 20 | * [Does it work with generic classes? Custom methods? Nested classes?](#supported-features) 21 | * [What if I want to make the class smarter though not strictly immutable, like caching a point distance after it has been requested the first time?](#non-immutability) 22 | * [Does Intellisense and all that stuff work after using this?](#intellisense) 23 | * [Can I suggest new features or whatever?](#suggestions) 24 | * [Can I see the extra code generated for the very first example?](#generated-code-sample) 25 | 26 | #### Release notes 27 | * **[v1.4.4]** Added proper support for .NET Standard 2.0. Fixed an issued with ImmutableGetHashCode generation. 28 | * **[v1.3.3]** ImmutableEquals now uses ImmutableGetHashCode as a speed optimization. 29 | * **[v1.3.2]** Fixed the generated equals operator (sometimes it would crash when the first item was null). 30 | * **[v1.3.1]** Made the library portable, however please check the notes inside 'How do I start?' about portable projects. 31 | * **[v1.2.1]** Now supports generating ToBuilder() and a better OptParam implementation. 32 | * **[v1.2.0]** Now supports generating builders. 33 | * **[v1.2.0]** WithParam class is now called OptParam. 34 | * **[v1.1.5]** Added a PreConstructor option to write code such as atributtes before generated constructors. 35 | * **[v1.1.5]** Added ExcludeConstructor and AllowCustomConstructors options. 36 | * **[v1.1.4]** Collection special cases are done when they inherit from ICollection instead of IEnumerable. 37 | * **[v1.1.3]** Using the dynamic keyword instead of reflection for faster KeyValuePair handling. 38 | * **[v1.1.2]** Generated Equals, GetHashCode and ToString now properly support collections as long as they implement IEnumerator. This means that arrays, List, Set, Dictionary, plus its Immutable variants are properly handled. 39 | * **[v1.1.0]** ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX 40 | * **[v1.1.0]** preConstructorParam code comment has been changed to the PreConstructorParam attribute 41 | 42 | ## Why use this? 43 | Creating proper immutable objects in C# requires a lot boilerplate code. The aim of this project is to reduce this to a minimum by means of automatic code generation via T4 templates. For instance, given the following class: 44 | ```c# 45 | [ImmutableClass(Options = ImmutableClassOptions.IncludeOperatorEquals)] 46 | class Person { 47 | private const int AgeDefaultValue = 18; 48 | 49 | public string FirstName { get; } 50 | public string LastName { get; } 51 | public int Age { get; } 52 | 53 | [ComputedProperty] 54 | public string FullName => $"{FirstName} {LastName}"; 55 | } 56 | ``` 57 | 58 | It will automatically generate for you in a separate partial class file the following: 59 | * A constructor such as `public Person(string firstName, string lastName, int age = 18)` that will initialize the values. 60 | * Working implementations for `Equals(object other)` and `Equals(Person other)`. 61 | * Working implementations for `operator==` and `operator!=` 62 | * A working implementation of `GetHashCode()`. 63 | * A better `ToString()` with output such as `"Person { FirstName=John, LastName=Doe, Age=21 }"` 64 | * A `Person With(...)` method that can be used to generate a new immutable clone with 0 or more properties changed (e.g. `var janeDoe = johnDoe.With(firstName: "Jane", age: 20)` 65 | * A `Builder` subclass that can be used to create the objects with the builder pattern such as: 66 | ```c# 67 | var builder = new Person.Builder().With(firstName: "John").With(lastName: "Doe"); // fluid way 68 | builder.Age = 21; // or via properties 69 | // that can be read back 70 | string firstName = builder.FirstName; // "John" 71 | var lastName = builder.LastName.Value; // "Doe" 72 | Person johnDoe = b.Build(); 73 | var janeDoe = johnDoe.ToBuilder().With(firstName: "Jane", age: 20).Build(); // back and forth 74 | ``` 75 | 76 | ## How do I start? 77 | 1. Install the T4Immutable nuget package 78 | 2. For **.NET Framework projects** use "**Build - Transform All T4 Templates**" or right click on the _T4Immutable/T4Immutable.tt_ file and click "**Run custom tool**". The files will be generated inside _T4Immutable_ as children of _T4Immutable.tt_. 79 | 3. For **.NET Core/Standard/etc projects** right click on the _T4Immutable/T4Immutable.tt_ file and click "**Run custom tool**". The files will be generated in a folder named _T4Immutable_generated_. "**Build - Transform All T4 Templates**" **won't work** 80 | 81 | *Remember to do this everytime you update the package or any of your immutable classes change.* If you want to automate it there are plugins out there that auto-run T4 templates before build such as [AutoT4](https://github.com/bennor/AutoT4). 82 | 83 | ## What's needed to make an immutable class? 84 | Just mark the class with the use the `[ImmutableClass]` attribute. The class will be auto-checked to meet the following constraints before code generation takes place: 85 | * Any properties _not_ marked as `ComputedProperty` will need to be either auto properites or have a non-public setter. 86 | * It should not have any custom constructors since one will be auto-generated, however please check the "Constructor overrides" section below to see ways to overcome this limitation. 87 | * Any default values (see "How to specify property default values?") will be checked to have the same type than the properties. 88 | * It cannot be static. 89 | * It cannot have any extra partials besides the generated one (this support is still TODO). 90 | * It cannot have a base class (probably to be lifted in a future update if anybody can show a proper use case), it can however have any interfaces. 91 | 92 | Besides those checks it is your responsibility to make the immutable object behave correctly. For example you should use ImmutableList instead of List and so on. This project is just made to reduce the boilerplate after all, not ensure correctness. 93 | 94 | ## How are collection (Array, List, Set, Dictionary... plus their Immutable versions) based properties handled? 95 | They just work as long as they inherit from `ICollection` (as all of the basic ones do). The generated `Equals()` will check they are equivalent by checking their contents, as well as the generated `GetHashCode()`. Nested collections are not a problem as well. 96 | 97 | ## Do generated classes serialize/deserialize correctly with JSON.NET / Protobuf.NET / others? 98 | #### JSON.NET 99 | ##### If you use a *public generated constructor* 100 | Just add `JsonIgnore` to computed properties. 101 | ##### If you use a *non-public generated constructor* 102 | Use `PreConstructor = "[Newtonsoft.Json.JsonConstructor]"` inside the `ImmutableClass` attribute. (Recommended over the next option) 103 | 104 | Alternatively: 105 | 1. Add the `ImmutableClassOptions.AllowCustomConstructors` to the `Options` parameter of the `ImmutableClass` attribute. 106 | 2. Add a constructor with no arguments. 107 | 3. Add a private/protected setter to all your non-computed properties if they didn't have any. 108 | 4. Add `JsonIgnore` to computed properties. 109 | 5. NB: In this case JSON.net will call the constructor and then _later_ set the properties one by one. 110 | 111 | #### Protobuf.NET 112 | 1. Mark your class as `[ProtoContract]`. 113 | 2. Add the `ImmutableClassOptions.AllowCustomConstructors` to the `Options` parameter of the `ImmutableClass` attribute. 114 | 3. Add a constructor with no arguments. 115 | 4. Mark the non-computed properties with `[ProtoMember(unique number)]`. 116 | 5. Add to all non-computed properties a private/protected setter if they didn't have any. 117 | 6. NB: In this case Protobuf.NET will call the constructor and then _later_ set the properties one by one. 118 | 119 | #### Others 120 | * Let me know :) 121 | 122 | ## I don't want X. Can I control what gets generated? 123 | You sure can, just add to the ImmutableClass attribute something like this: 124 | ```c# 125 | [ImmutableClass(Options = 126 | ImmutableClassOptions.ExcludeEquals | // do not generate an Equals() method 127 | ImmutableClassOptions.ExcludeGetHashCode | // do not generate a GetHashCode() method 128 | ImmutableClassOptions.IncludeOperatorEquals | // generate operator== and operator!= methods 129 | ImmutableClassOptions.ExcludeToString | // do not generate a ToString() method 130 | ImmutableClassOptions.ExcludeWith | // do not generate a With() method 131 | ImmutableClassOptions.ExcludeConstructor | // do not generate a constructor 132 | ImmutableClassOptions.ExcludeBuilder | // do not generate a builder or ImmutableToBuilder() - implies ExcludeToBuilder (usually used alongside ExcludeConstructor) 133 | ImmutableClassOptions.ExcludeToBuilder | // do not generate a builder or ToBuilder() method 134 | ImmutableClassOptions.AllowCustomConstructors)] // allow custom constructors 135 | ``` 136 | Note that even if you exclude for example the `Equals()` method implementation you can still use them internally by invoking the `private bool ImmutableEquals(...)` implementation. This is done in case you might want to write your own `Equals()` yet still use the generated one as a base. 137 | Take care you do *not* use "using Foo = ImmutableClassOptions" to save some typing. Due to limitations with T4 it won't work. 138 | 139 | ## Can I control the access level (public/private/...) of the constructor or the builder? 140 | Yes. Do something like this: 141 | ``` 142 | [ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private, BuilderAccessLevel = BuilderAccessLevel.Protected)] 143 | ``` 144 | Valid options are `Public`, `Protected`, `ProtectedInternal`, `Internal` and `Private`. 145 | 146 | ## Constructor post-initalization / validation 147 | If you need to do extra initialization / validation on the generated constructor just define a `void PostConstructor()` method (access modifier doesn't matter) and do your work there. It will be invoked inside the generated constructor after all assignations are done. 148 | 149 | Alternatively (and recommended) it is of course also possible to do validation inside the properties private/protected setters. E.g: 150 | ```c# 151 | private int _Age; 152 | public int Age { 153 | get { return _Age; } 154 | set { 155 | if (value < 18) throw new Exception("You are too young!"); 156 | _Age = value; 157 | } 158 | } 159 | ``` 160 | 161 | ## Can I add extra attributes to each constructor parameter? 162 | Yes, use the following when defining a property: 163 | ```c# 164 | [PreConstructorParam("[JetBrains.Annotations.NotNull]")] 165 | public string FirstName { get; } 166 | ``` 167 | Bear in mind that if you use it to specify attributes they must have the full name (including namespace) or else there would be compilation errors. Also bear in mind that due to T4 limitations the string has to be constant, this is, it shouldn't depend on other const values. 168 | 169 | ## How do I enforce automatic null checking for the constructor parameters? What about for the properties? 170 | If you use this: 171 | ```c# 172 | [PreNotNullCheck, PostNotNullCheck] 173 | public string FirstName { get; } 174 | ``` 175 | The constructor will be this: 176 | ```c# 177 | public Person(string firstName) { 178 | // pre not null check 179 | if (firstName == null) throw new ArgumentNullException(nameof(firstName)); 180 | 181 | // assignations + PostConstructor() if needed 182 | 183 | // post not null check 184 | if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName)); 185 | } 186 | ``` 187 | 188 | Having said this, if you use JetBrains Annotations for null checking, you can also do this: 189 | ```c# 190 | [JetBrains.Annotations.NotNull, ConstructorParamNotNull] 191 | public string FirstName { get; } 192 | ``` 193 | And the constructor will be this: 194 | ```c# 195 | public Person([JetBrains.Annotations.NotNull] string firstName) { 196 | // pre not null check is implied by ConstructorParamNotNull 197 | if (firstName == null) throw new ArgumentNullException(nameof(firstName)); 198 | 199 | // assignations + PostConstructor() if needed 200 | 201 | // post not null check implied by JetBrains.Annotations.NotNull on the property 202 | if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName)); 203 | } 204 | ``` 205 | 206 | ## Constructor overrides 207 | If you need to do alternate constructors (for example having a `Point(T x, T y)` immutable class and you want to generate a point from a distance and an angle) then you can do something like: 208 | ```c# 209 | public static Point FromAngleAndDistance(T distance, double angle) { 210 | // your code here 211 | return new Point(x, y); 212 | } 213 | ``` 214 | Still, if you still aren't satisfied by this you can enable the `ImmutableOptions.AllowCustomConstructors` and create your own alternate constructor to your own risk. 215 | 216 | ## How do I change the order of the arguments in the generated constructor? 217 | Just change the order of the properties. 218 | 219 | ## How to specify property default values? 220 | If you want a property to have a given default value on the auto-generated constructor there are two ways. Say that you have a property named int Age, and you want it to have the default value of 18: 221 | * Way 1: private/protected/public/whatever `const int AgeDefaultValue = 18;` 222 | * Way 2: private/protected/public/whatever `static readonly int AgeDefaultValue = 18;` 223 | 224 | If you wonder why there are two alternatives it is because sometimes it is possible to add stuff such as `new Foo()` as a default parameter for constructors and that expression works as a readonly but does not work as a const. 225 | 226 | Please note that default values, like in a constructor, should not have gaps. 227 | This is, if you have int x, int y then you should have a default value for y or for x and y. 228 | If you want a default value for x then move it to the end. 229 | 230 | ## Does it work with generic classes? Custom methods? Nested classes? 231 | It sure does! 232 | 233 | ## What if I want to make the class smarter though not strictly immutable, like caching a point distance after it has been requested the first time? 234 | This is more about reducing boilerplate than ensuring immutability, so you can. E.g.: 235 | ```c# 236 | [ImmutableClass] 237 | class Point { 238 | public double X { get; } 239 | public double Y { get; } 240 | 241 | private double _Distance; 242 | 243 | [ComputedProperty] 244 | public double Distance { 245 | get { 246 | if (_Distance == null) _Distance = Math.Sqrt(X*X + Y*Y); 247 | return _Distance.Value; 248 | } 249 | } 250 | } 251 | ``` 252 | However if you do stuff like this, then since internally it has become mutable-ish you will need to use a lock or some other method if you want it to work properly when the object is used concurrently. A probably better solution would be to initialize the ```_Distance``` member inside the `PostConstructor()`. It all depends on your use case. 253 | 254 | ## Does Intellisense and all that stuff work after using this? 255 | Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems after the T4 template is built. 256 | 257 | ## Can I suggest new features or whatever? 258 | Please do! 259 | 260 | ## Can I see the extra code generated for the very first example? 261 | Here you go (excluding some redundant attributes): 262 | ```c# 263 | using System; 264 | 265 | partial class Person : IEquatable { 266 | public Person(string firstName, string lastName, int age = 18) { 267 | this.FirstName = firstName; 268 | this.LastName = lastName; 269 | this.Age = age; 270 | _ImmutableHashCode = T4Immutable.Helpers.GetHashCodeFor(this.FirstName, this.LastName, this.Age); 271 | } 272 | 273 | private bool ImmutableEquals(Person obj) { 274 | if (ReferenceEquals(this, obj)) return true; 275 | if (ReferenceEquals(obj, null)) return false; 276 | if (ImmutableGetHashCode() != obj.ImmutableGetHashCode()) return false; 277 | return T4Immutable.Helpers.AreEqual(this.FirstName, obj.FirstName) && T4Immutable.Helpers.AreEqual(this.LastName, obj.LastName) && T4Immutable.Helpers.AreEqual(this.Age, obj.Age); 278 | } 279 | 280 | public override bool Equals(object obj) { 281 | return ImmutableEquals(obj as Person); 282 | } 283 | 284 | public bool Equals(Person obj) { 285 | return ImmutableEquals(obj); 286 | } 287 | 288 | public static bool operator ==(Person a, Person b) { 289 | return T4Immutable.Helpers.BasicAreEqual(a, b); 290 | } 291 | 292 | public static bool operator !=(Person a, Person b) { 293 | return !T4Immutable.Helpers.BasicAreEqual(a, b); 294 | } 295 | 296 | private readonly int _ImmutableHashCode; 297 | 298 | private int ImmutableGetHashCode() { 299 | return _ImmutableHashCode; 300 | } 301 | 302 | public override int GetHashCode() { 303 | return ImmutableGetHashCode(); 304 | } 305 | 306 | private string ImmutableToString() { 307 | return T4Immutable.Helpers.ToStringFor(nameof(Person), new System.Tuple(nameof(this.FirstName), this.FirstName), new System.Tuple(nameof(this.LastName), this.LastName), new System.Tuple(nameof(this.Age), this.Age)); 308 | } 309 | 310 | public override string ToString() { 311 | return ImmutableToString(); 312 | } 313 | 314 | private Person ImmutableWith(T4Immutable.OptParam firstName = default(T4Immutable.OptParam), T4Immutable.OptParam lastName = default(T4Immutable.OptParam), T4Immutable.OptParam age = default(T4Immutable.OptParam)) { 315 | return new Person( 316 | firstName.HasValue ? firstName.Value : this.FirstName, 317 | lastName.HasValue ? lastName.Value : this.LastName, 318 | age.HasValue ? age.Value : this.Age 319 | ); 320 | } 321 | 322 | public Person With(T4Immutable.OptParam firstName = default(T4Immutable.OptParam), T4Immutable.OptParam lastName = default(T4Immutable.OptParam), T4Immutable.OptParam age = default(T4Immutable.OptParam)) { 323 | return ImmutableWith(firstName, lastName, age); 324 | } 325 | 326 | public Person.Builder ToBuilder() { 327 | return ImmutableToBuilder(); 328 | } 329 | 330 | private Person.Builder ImmutableToBuilder() { 331 | return new Person.Builder().With( 332 | new T4Immutable.OptParam(this.FirstName), 333 | new T4Immutable.OptParam(this.LastName), 334 | new T4Immutable.OptParam(this.Age) 335 | ); 336 | } 337 | 338 | public class Builder { 339 | public T4Immutable.OptParam FirstName { get; set; } 340 | public T4Immutable.OptParam LastName { get; set; } 341 | public T4Immutable.OptParam Age { get; set; } 342 | 343 | public Builder With(T4Immutable.OptParam firstName = default(T4Immutable.OptParam), T4Immutable.OptParam lastName = default(T4Immutable.OptParam), T4Immutable.OptParam age = default(T4Immutable.OptParam)) { 344 | if (firstName.HasValue) this.FirstName = firstName; 345 | if (lastName.HasValue) this.LastName = lastName; 346 | if (age.HasValue) this.Age = age; 347 | return this; 348 | } 349 | 350 | public Person Build() { 351 | if (!this.FirstName.HasValue) throw new InvalidOperationException("Builder property 'FirstName' cannot be left unassigned"); 352 | if (!this.LastName.HasValue) throw new InvalidOperationException("Builder property 'LastName' cannot be left unassigned"); 353 | if (!this.Age.HasValue) this.Age = 18; 354 | if (!this.Age.HasValue) throw new InvalidOperationException("Builder property 'Age' cannot be left unassigned"); 355 | return new Person(this.FirstName.Value, this.LastName.Value, this.Age.Value); 356 | } 357 | } 358 | } 359 | ``` 360 | -------------------------------------------------------------------------------- /scripts/nuget-pack.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd .. 3 | md output 4 | cd src\T4Immutable-netstd2 5 | nuget pack T4Immutable.nuspec -build -properties Configuration=Release -outputdirectory ..\..\output 6 | cd ..\..\scripts 7 | -------------------------------------------------------------------------------- /src/T4Immutable-netstd2/Attributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable RedundantAttributeUsageProperty 4 | 5 | // note we can't use features > c#4 since it needs to be compiled by the template 6 | // this means for example no auto-property initializers 7 | // ReSharper disable ConvertToAutoProperty 8 | 9 | namespace T4Immutable { 10 | 11 | #region For classes 12 | 13 | /// 14 | /// Marks a class so it can be processed by T4Immutable. 15 | /// 16 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 17 | public sealed class ImmutableClassAttribute : Attribute { 18 | private ConstructorAccessLevel _constructorAccessLevel = ConstructorAccessLevel.Public; 19 | private ImmutableClassOptions _options = ImmutableClassOptions.None; 20 | private string _preConstructor; 21 | private BuilderAccessLevel _builderAccessLevel = BuilderAccessLevel.Public; 22 | 23 | /// 24 | /// Immutable class generation options. 25 | /// 26 | public ImmutableClassOptions Options { 27 | get { return _options; } 28 | set { _options = value; } 29 | } 30 | 31 | /// 32 | /// Generated constructor access level (modifier). 33 | /// 34 | public ConstructorAccessLevel ConstructorAccessLevel { 35 | get { return _constructorAccessLevel; } 36 | set { _constructorAccessLevel = value; } 37 | } 38 | 39 | /// 40 | /// String with code to add before the constructor. 41 | /// 42 | public string PreConstructor { 43 | get { return _preConstructor; } 44 | set { _preConstructor = value; } 45 | } 46 | 47 | /// 48 | /// Generated builder class access level (modifier). 49 | /// 50 | public BuilderAccessLevel BuilderAccessLevel { 51 | get { return _builderAccessLevel; } 52 | set { _builderAccessLevel = value; } 53 | } 54 | } 55 | 56 | /// 57 | /// Immutable class generation options for T4Immutable. 58 | /// 59 | [Flags] 60 | public enum ImmutableClassOptions { 61 | /// 62 | /// Default. 63 | /// 64 | None = 0, 65 | 66 | /// 67 | /// Do not generate Equals() implementation or add the IEquatable interface. 68 | /// 69 | ExcludeEquals = 1 << 0, 70 | 71 | /// 72 | /// Do not generate a GetHashCode() implementation. 73 | /// 74 | ExcludeGetHashCode = 1 << 1, 75 | 76 | /// 77 | /// Generate operator == and operator !=. 78 | /// 79 | IncludeOperatorEquals = 1 << 2, 80 | 81 | /// 82 | /// Do not generate a ToString() implementation. 83 | /// 84 | ExcludeToString = 1 << 3, 85 | 86 | /// 87 | /// Do not generate a With() implementation. 88 | /// 89 | ExcludeWith = 1 << 4, 90 | 91 | /// 92 | /// Do not generate a constructor. 93 | /// 94 | ExcludeConstructor = 1 << 5, 95 | 96 | /// 97 | /// Allow the user to define his own constructors. 98 | /// 99 | AllowCustomConstructors = 1 << 6, 100 | 101 | /// 102 | /// Do not generate a builder class or ImmutableToBuilder() implementation. Implies ExcludeToBuilder. 103 | /// 104 | ExcludeBuilder = 1 << 7, 105 | 106 | /// 107 | /// Do not generate a ToBuilder() implementation. 108 | /// 109 | ExcludeToBuilder = 1 << 8, 110 | } 111 | 112 | /// 113 | /// Access level (modifier) for constructors generated by T4Immutable. 114 | /// 115 | public enum ConstructorAccessLevel { 116 | Public, 117 | Protected, 118 | Internal, 119 | Private, 120 | ProtectedInternal 121 | } 122 | 123 | /// 124 | /// Access level (modifier) for builders generated by T4Immutable. 125 | /// 126 | public enum BuilderAccessLevel { 127 | Public, 128 | Protected, 129 | Internal, 130 | Private, 131 | ProtectedInternal 132 | } 133 | 134 | #endregion 135 | 136 | #region For properties 137 | 138 | /// 139 | /// Adds a JetBrains.Annotations.NotNull attribute to the constructor parameter. 140 | /// Also enables a not null precheck implicitely. 141 | /// 142 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 143 | public sealed class ConstructorParamNotNullAttribute : Attribute { 144 | } 145 | 146 | /// 147 | /// Generate a not null check at the beginning of the constructor. 148 | /// 149 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 150 | public sealed class PreNotNullCheckAttribute : Attribute { 151 | } 152 | 153 | /// 154 | /// Generate a not null check at the end of the constructor. 155 | /// 156 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 157 | public sealed class PostNotNullCheckAttribute : Attribute { 158 | } 159 | 160 | /// 161 | /// Marks a property as computed, effectively making T4Immutable ignore it. 162 | /// 163 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 164 | public sealed class ComputedPropertyAttribute : Attribute { 165 | } 166 | 167 | /// 168 | /// String with code to add before the constructor parameter. 169 | /// 170 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 171 | public sealed class PreConstructorParamAttribute : Attribute { 172 | public string Pre { get; private set; } 173 | 174 | public PreConstructorParamAttribute(string pre) { 175 | Pre = pre; 176 | } 177 | } 178 | 179 | #endregion 180 | 181 | #region Internal 182 | 183 | /// 184 | /// Attribute used internally by T4Immutable to mark generated code. Not for public usage. 185 | /// 186 | [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] 187 | public sealed class GeneratedCodeAttribute : Attribute { 188 | } 189 | 190 | #endregion 191 | } 192 | -------------------------------------------------------------------------------- /src/T4Immutable-netstd2/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace T4Immutable { 8 | /// 9 | /// Collection of helper methods for T4Immutable. 10 | /// 11 | public static class Helpers { 12 | /// 13 | /// Check if two objects are equal. 14 | /// 15 | /// Object type. 16 | /// First object. 17 | /// Second object. 18 | /// true if they are equal, false otherwise. 19 | public static bool BasicAreEqual(T a, T b) { 20 | bool aIsNull = ReferenceEquals(a, null), bIsNull = ReferenceEquals(b, null); 21 | if (aIsNull && bIsNull) { 22 | return true; 23 | } 24 | if (aIsNull || bIsNull) { 25 | return false; 26 | } 27 | return a.Equals(b); 28 | } 29 | 30 | /// 31 | /// Check if two objects are equal, plus some special checking for KeyValuePairs and ICollections. 32 | /// 33 | /// Object type. 34 | /// First object. 35 | /// Second object. 36 | /// true if they are equal, false otherwise. 37 | public static bool AreEqual(T a, T b) { 38 | bool aIsNull = ReferenceEquals(a, null), bIsNull = ReferenceEquals(b, null); 39 | if (aIsNull && bIsNull) { 40 | return true; 41 | } 42 | if (aIsNull || bIsNull) { 43 | return false; 44 | } 45 | if (a.Equals(b)) { 46 | return true; 47 | } 48 | 49 | // check for the special case of KeyValuePair (items of a dictionary) 50 | var aKvp = KeyValuePairHelper.TryExtractKeyValuePair(a); 51 | if (aKvp != null) { 52 | var bKvp = KeyValuePairHelper.TryExtractKeyValuePair(b); 53 | return AreEqual(aKvp.Item1, bKvp.Item1) && AreEqual(aKvp.Item2, bKvp.Item2); 54 | } 55 | 56 | // one extra check for collections 57 | 58 | var aCollection = a as ICollection; 59 | var bCollection = b as ICollection; 60 | if ((aCollection == null) || (bCollection == null)) { 61 | return false; 62 | } 63 | 64 | if (aCollection.Count != bCollection.Count) { 65 | return false; 66 | } 67 | 68 | var aEnum = aCollection.GetEnumerator(); 69 | var bEnum = bCollection.GetEnumerator(); 70 | 71 | while (aEnum.MoveNext()) { 72 | if (!bEnum.MoveNext()) { 73 | return false; 74 | } 75 | object aCurrent = aEnum.Current, bCurrent = bEnum.Current; 76 | if (!AreEqual(aCurrent, bCurrent)) { 77 | return false; 78 | } 79 | } 80 | 81 | // all items so far are the same, but does b have one more? 82 | return !bEnum.MoveNext(); 83 | } 84 | 85 | /// 86 | /// Gets the hashcode of a single object. If the object is a collection it will make a hashcode of the collection. 87 | /// 88 | /// Object to make the hashcode for. 89 | /// A hashcode. 90 | public static int GetHashCodeForSingleObject(object o) { 91 | if (ReferenceEquals(o, null)) { 92 | return 0; 93 | } 94 | 95 | var oCollection = o as ICollection; 96 | if (oCollection == null) { 97 | // check for the special case of KeyValuePair (items of a dictionary) 98 | var kvp = KeyValuePairHelper.TryExtractKeyValuePair(o); 99 | if (kvp != null) { 100 | return GetHashCodeFor(kvp.Item1, kvp.Item2); 101 | } 102 | 103 | return o.GetHashCode(); 104 | } 105 | 106 | // make a hash of the items if it is a collection 107 | var oEnum = oCollection.GetEnumerator(); 108 | 109 | var list = new List(); 110 | while (oEnum.MoveNext()) { 111 | list.Add(oEnum.Current); 112 | } 113 | 114 | return GetHashCodeFor(list.ToArray()); 115 | } 116 | 117 | /// 118 | /// Returns the hash code of the combination of a series of objects. 119 | /// 120 | /// Objects to make the hash code for. 121 | /// A hashcode. 122 | public static int GetHashCodeFor(params object[] objs) { 123 | if (objs.Length == 0) { 124 | return 0; 125 | } 126 | 127 | const int prime = 486187739; 128 | // overflow is fine 129 | unchecked { 130 | var hash = 17; 131 | foreach (var o in objs) { 132 | hash = hash * prime + GetHashCodeForSingleObject(o); 133 | } 134 | return hash; 135 | } 136 | } 137 | 138 | /// 139 | /// Returns the string representation of an object, null if null or [items] if a collection. 140 | /// 141 | /// 142 | /// The string representation. 143 | public static string ToStringForSingleObject(object o) { 144 | if (ReferenceEquals(o, null)) { 145 | return "null"; 146 | } 147 | 148 | var oCollection = o as ICollection; 149 | if (oCollection == null) { 150 | // check for the special case of KeyValuePair (items of a dictionary) 151 | var kvp = KeyValuePairHelper.TryExtractKeyValuePair(o); 152 | if (kvp != null) { 153 | return "(" + ToStringForSingleObject(kvp.Item1) + ", " + ToStringForSingleObject(kvp.Item2) + ")"; 154 | } 155 | 156 | return o.ToString(); 157 | } 158 | 159 | // make a list of the items if it is a collection 160 | var oEnum = oCollection.GetEnumerator(); 161 | 162 | var list = new List(); 163 | while (oEnum.MoveNext()) { 164 | list.Add(oEnum.Current); 165 | } 166 | 167 | var sb = new StringBuilder(); 168 | sb.Append("[ "); 169 | // TODO: this could be optimized by not using select and using the append on each object instead 170 | sb.Append(string.Join(", ", list.Select(ToStringForSingleObject))); 171 | sb.Append(" ]"); 172 | return sb.ToString(); 173 | } 174 | 175 | /// 176 | /// Returns the string representation of an immutable object. 177 | /// 178 | /// Immutable object type name. 179 | /// Properties of the immutable objects (name and value). 180 | /// The string representation. 181 | public static string ToStringFor(string name, params Tuple[] objs) { 182 | var sb = new StringBuilder(); 183 | sb.Append(name + " { "); 184 | // TODO: this could be optimized by not using select and using the append on each object instead 185 | sb.Append(string.Join(", ", objs.Select(v => v.Item1 + "=" + ToStringForSingleObject(v.Item2)))); 186 | sb.Append(" }"); 187 | return sb.ToString(); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/T4Immutable-netstd2/KeyValuePairHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace T4Immutable { 6 | internal static class KeyValuePairHelper { 7 | public static Tuple TryExtractKeyValuePair(object obj) { 8 | if (!IsKeyValuePair(obj.GetType())) { 9 | return null; 10 | } 11 | dynamic dobj = obj; 12 | return new Tuple(dobj.Key, dobj.Value); 13 | } 14 | 15 | public static bool IsKeyValuePair(Type type) { 16 | var typeInfo = type.GetTypeInfo(); 17 | if (!typeInfo.IsGenericType || typeInfo.IsGenericTypeDefinition) { 18 | return false; 19 | } 20 | var genericType = type.GetGenericTypeDefinition(); 21 | return ReferenceEquals(genericType, typeof(KeyValuePair<,>)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/T4Immutable-netstd2/OptParam.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace T4Immutable { 5 | /// 6 | /// Struct used to pass parameters to With/Builder methods. 7 | /// It is a struct because structs are not nullable and then we can use null and get it transformed to the value type. 8 | /// 9 | /// Value type 10 | public struct OptParam { 11 | // ReSharper disable once FieldCanBeMadeReadOnly.Local 12 | private bool _hasValue; 13 | // ReSharper disable once FieldCanBeMadeReadOnly.Local 14 | private T _value; 15 | 16 | /// 17 | /// Actual value passed. 18 | /// 19 | public T Value { 20 | get { 21 | if (!HasValue) { 22 | throw new InvalidOperationException("No value set"); 23 | } 24 | return _value; 25 | } 26 | } 27 | 28 | /// 29 | /// true if it holds a value, false otherwise. 30 | /// 31 | public bool HasValue => _hasValue; 32 | 33 | /// 34 | /// Constructor. 35 | /// 36 | /// Value to hold. 37 | public OptParam(T value) { 38 | _value = value; 39 | _hasValue = true; 40 | } 41 | 42 | /// 43 | /// Converts automatically between this class and the inner parameter type. 44 | /// 45 | /// 46 | public static implicit operator OptParam(T val) { 47 | return new OptParam(val); 48 | } 49 | 50 | /// 51 | /// Converts this class to the inner parameter type. 52 | /// 53 | /// 54 | public static explicit operator T(OptParam val) { 55 | return val.Value; 56 | } 57 | 58 | /// 59 | /// Gets the value or default if none. 60 | /// 61 | /// 62 | public T GetValueOrDefault() { 63 | return Value; 64 | } 65 | 66 | /// 67 | /// Checks for equality. 68 | /// 69 | /// The other object. 70 | /// true if they are equals, false otherwise. 71 | public override bool Equals(object obj) { 72 | var otherOptParam = obj as OptParam?; 73 | 74 | // if it doesn't have a value then the other object must be the same type and not have a value 75 | if (!HasValue) { 76 | if (otherOptParam == null) { 77 | return false; 78 | } 79 | return !otherOptParam.Value.HasValue; 80 | } 81 | 82 | // this has a value 83 | 84 | if (otherOptParam != null) { 85 | return otherOptParam.Value.HasValue && Helpers.BasicAreEqual(Value, otherOptParam.Value.Value); 86 | } 87 | return Helpers.BasicAreEqual(Value, obj); 88 | } 89 | 90 | /// 91 | /// Get the hash code. 92 | /// 93 | /// A hash code. 94 | public override int GetHashCode() { 95 | if (!HasValue) { 96 | return -1; 97 | } 98 | if (Value == null) { 99 | return 0; 100 | } 101 | return Value.GetHashCode(); 102 | } 103 | 104 | /// 105 | /// To string. 106 | /// 107 | /// 108 | public override string ToString() { 109 | if (!HasValue) { 110 | return "No value"; 111 | } 112 | if (Value == null) { 113 | return "null"; 114 | } 115 | return Value.ToString(); 116 | } 117 | } 118 | 119 | public static class OptParam { 120 | /// 121 | /// If the type provided is not an OptParam returns null. 122 | /// Otherwise returns the underlying type of the OptParam type. 123 | /// 124 | /// 125 | /// 126 | public static Type GetUnderlyingType(Type optParamType) { 127 | if (optParamType == null) { 128 | throw new ArgumentNullException(nameof(optParamType)); 129 | } 130 | 131 | var typeInfo = optParamType.GetTypeInfo(); 132 | if (!typeInfo.IsGenericType || typeInfo.IsGenericTypeDefinition) { 133 | return null; 134 | } 135 | 136 | var genericType = optParamType.GetGenericTypeDefinition(); 137 | return ReferenceEquals(genericType, typeof(OptParam<>)) ? typeInfo.GenericTypeArguments[0] : null; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/T4Immutable-netstd2/T4Immutable-netstd2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | T4Immutable 6 | T4Immutable 7 | T4Immutable 8 | T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes. 9 | 1.4.4 10 | Javier González Garcés 11 | 12 | T4Immutable 13 | Copyright © 2016-2017 Javier González Garcés 14 | https://github.com/xaviergonz/T4Immutable/blob/master/LICENSE 15 | https://github.com/xaviergonz/T4Immutable 16 | t4 immutable codegen 17 | 18 | false 19 | 20 | 21 | 22 | bin\Release\netstandard2.0\T4Immutable.xml 23 | bin\Release\ 24 | 25 | 26 | 27 | bin\Debug\netstandard2.0\ 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/T4Immutable-netstd2/T4Immutable.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | T4Immutable 5 | 1.4.4 6 | T4Immutable 7 | Javier González Garcés 8 | Javier González Garcés 9 | false 10 | https://github.com/xaviergonz/T4Immutable/blob/master/LICENSE 11 | https://github.com/xaviergonz/T4Immutable 12 | T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes. 13 | Ported library to .NET standard 2.0 14 | Copyright © 2016-2017 Javier González Garcés 15 | t4 immutable codegen 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/T4Immutable.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{9B18F400-9723-41A7-BFE9-9566F5BBF812}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "T4Immutable", "T4Immutable", "{1CC7F4B4-E394-495B-90BD-5B1C6420CC90}" 9 | ProjectSection(SolutionItems) = preProject 10 | content\T4Immutable\Attributes.cs_ = content\T4Immutable\Attributes.cs_ 11 | content\T4Immutable\Class.ttinclude = content\T4Immutable\Class.ttinclude 12 | content\T4Immutable\Field.ttinclude = content\T4Immutable\Field.ttinclude 13 | content\T4Immutable\Parser.ttinclude = content\T4Immutable\Parser.ttinclude 14 | content\T4Immutable\Prop.ttinclude = content\T4Immutable\Prop.ttinclude 15 | content\T4Immutable\Shared.ttinclude = content\T4Immutable\Shared.ttinclude 16 | content\T4Immutable\Struct.ttinclude = content\T4Immutable\Struct.ttinclude 17 | content\T4Immutable\T4Immutable.tt = content\T4Immutable\T4Immutable.tt 18 | content\T4Immutable\TemplateFileManagerV2.1.ttinclude = content\T4Immutable\TemplateFileManagerV2.1.ttinclude 19 | content\T4Immutable\VisualStudioHelper.ttinclude = content\T4Immutable\VisualStudioHelper.ttinclude 20 | EndProjectSection 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6E1256F3-7EC3-4676-9CE5-7D6379A12D86}" 23 | ProjectSection(SolutionItems) = preProject 24 | readme.txt = readme.txt 25 | EndProjectSection 26 | EndProject 27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T4Immutable-netstd2", "T4Immutable-netstd2\T4Immutable-netstd2.csproj", "{0A801419-750F-461A-B2B7-33636AFD08A1}" 28 | EndProject 29 | Global 30 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 31 | Debug|Any CPU = Debug|Any CPU 32 | Release|Any CPU = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {0A801419-750F-461A-B2B7-33636AFD08A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {0A801419-750F-461A-B2B7-33636AFD08A1}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {0A801419-750F-461A-B2B7-33636AFD08A1}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {0A801419-750F-461A-B2B7-33636AFD08A1}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(SolutionProperties) = preSolution 41 | HideSolutionNode = FALSE 42 | EndGlobalSection 43 | GlobalSection(NestedProjects) = preSolution 44 | {1CC7F4B4-E394-495B-90BD-5B1C6420CC90} = {9B18F400-9723-41A7-BFE9-9566F5BBF812} 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {D070AD69-D534-4B89-9B92-7EB6068A5580} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /src/T4Immutable.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | END_OF_LINE 3 | END_OF_LINE 4 | END_OF_LINE 5 | END_OF_LINE 6 | END_OF_LINE 7 | END_OF_LINE 8 | END_OF_LINE 9 | True 10 | True 11 | True 12 | True -------------------------------------------------------------------------------- /src/content/T4Immutable/Attributes.cs_: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable RedundantAttributeUsageProperty 4 | 5 | // note we can't use features > c#4 since it needs to be compiled by the template 6 | // this means for example no auto-property initializers 7 | // ReSharper disable ConvertToAutoProperty 8 | 9 | namespace T4Immutable { 10 | 11 | #region For classes 12 | 13 | /// 14 | /// Marks a class so it can be processed by T4Immutable. 15 | /// 16 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 17 | public sealed class ImmutableClassAttribute : Attribute { 18 | private ConstructorAccessLevel _constructorAccessLevel = ConstructorAccessLevel.Public; 19 | private ImmutableClassOptions _options = ImmutableClassOptions.None; 20 | private string _preConstructor; 21 | private BuilderAccessLevel _builderAccessLevel = BuilderAccessLevel.Public; 22 | 23 | /// 24 | /// Immutable class generation options. 25 | /// 26 | public ImmutableClassOptions Options { 27 | get { return _options; } 28 | set { _options = value; } 29 | } 30 | 31 | /// 32 | /// Generated constructor access level (modifier). 33 | /// 34 | public ConstructorAccessLevel ConstructorAccessLevel { 35 | get { return _constructorAccessLevel; } 36 | set { _constructorAccessLevel = value; } 37 | } 38 | 39 | /// 40 | /// String with code to add before the constructor. 41 | /// 42 | public string PreConstructor { 43 | get { return _preConstructor; } 44 | set { _preConstructor = value; } 45 | } 46 | 47 | /// 48 | /// Generated builder class access level (modifier). 49 | /// 50 | public BuilderAccessLevel BuilderAccessLevel { 51 | get { return _builderAccessLevel; } 52 | set { _builderAccessLevel = value; } 53 | } 54 | } 55 | 56 | /// 57 | /// Immutable class generation options for T4Immutable. 58 | /// 59 | [Flags] 60 | public enum ImmutableClassOptions { 61 | /// 62 | /// Default. 63 | /// 64 | None = 0, 65 | 66 | /// 67 | /// Do not generate Equals() implementation or add the IEquatable interface. 68 | /// 69 | ExcludeEquals = 1 << 0, 70 | 71 | /// 72 | /// Do not generate a GetHashCode() implementation. 73 | /// 74 | ExcludeGetHashCode = 1 << 1, 75 | 76 | /// 77 | /// Generate operator == and operator !=. 78 | /// 79 | IncludeOperatorEquals = 1 << 2, 80 | 81 | /// 82 | /// Do not generate a ToString() implementation. 83 | /// 84 | ExcludeToString = 1 << 3, 85 | 86 | /// 87 | /// Do not generate a With() implementation. 88 | /// 89 | ExcludeWith = 1 << 4, 90 | 91 | /// 92 | /// Do not generate a constructor. 93 | /// 94 | ExcludeConstructor = 1 << 5, 95 | 96 | /// 97 | /// Allow the user to define his own constructors. 98 | /// 99 | AllowCustomConstructors = 1 << 6, 100 | 101 | /// 102 | /// Do not generate a builder class or ImmutableToBuilder() implementation. Implies ExcludeToBuilder. 103 | /// 104 | ExcludeBuilder = 1 << 7, 105 | 106 | /// 107 | /// Do not generate a ToBuilder() implementation. 108 | /// 109 | ExcludeToBuilder = 1 << 8, 110 | } 111 | 112 | /// 113 | /// Access level (modifier) for constructors generated by T4Immutable. 114 | /// 115 | public enum ConstructorAccessLevel { 116 | Public, 117 | Protected, 118 | Internal, 119 | Private, 120 | ProtectedInternal 121 | } 122 | 123 | /// 124 | /// Access level (modifier) for builders generated by T4Immutable. 125 | /// 126 | public enum BuilderAccessLevel { 127 | Public, 128 | Protected, 129 | Internal, 130 | Private, 131 | ProtectedInternal 132 | } 133 | 134 | #endregion 135 | 136 | #region For properties 137 | 138 | /// 139 | /// Adds a JetBrains.Annotations.NotNull attribute to the constructor parameter. 140 | /// Also enables a not null precheck implicitely. 141 | /// 142 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 143 | public sealed class ConstructorParamNotNullAttribute : Attribute { 144 | } 145 | 146 | /// 147 | /// Generate a not null check at the beginning of the constructor. 148 | /// 149 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 150 | public sealed class PreNotNullCheckAttribute : Attribute { 151 | } 152 | 153 | /// 154 | /// Generate a not null check at the end of the constructor. 155 | /// 156 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 157 | public sealed class PostNotNullCheckAttribute : Attribute { 158 | } 159 | 160 | /// 161 | /// Marks a property as computed, effectively making T4Immutable ignore it. 162 | /// 163 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 164 | public sealed class ComputedPropertyAttribute : Attribute { 165 | } 166 | 167 | /// 168 | /// String with code to add before the constructor parameter. 169 | /// 170 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 171 | public sealed class PreConstructorParamAttribute : Attribute { 172 | public string Pre { get; private set; } 173 | 174 | public PreConstructorParamAttribute(string pre) { 175 | Pre = pre; 176 | } 177 | } 178 | 179 | #endregion 180 | 181 | #region Internal 182 | 183 | /// 184 | /// Attribute used internally by T4Immutable to mark generated code. Not for public usage. 185 | /// 186 | [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] 187 | public sealed class GeneratedCodeAttribute : Attribute { 188 | } 189 | 190 | #endregion 191 | } 192 | -------------------------------------------------------------------------------- /src/content/T4Immutable/Class.ttinclude: -------------------------------------------------------------------------------- 1 | <#@ assembly Name="System.Core" #> 2 | <#@ assembly name="EnvDTE" #> 3 | <#@ assembly name="EnvDTE80" #> 4 | <#@ import namespace="System" #> 5 | <#@ import namespace="System.IO" #> 6 | <#@ import namespace="System.Diagnostics" #> 7 | <#@ import namespace="System.Linq" #> 8 | <#@ import namespace="System.Collections" #> 9 | <#@ import namespace="System.Collections.Generic" #> 10 | <#@ import namespace="EnvDTE" #> 11 | <#+ 12 | public class ClassInfo { 13 | public EnvDTE80.CodeClass2 CodeClass { get; } 14 | public EnvDTE80.CodeAttribute2 ImmutableAttribute { get; } 15 | public bool Immutable { get { return ImmutableAttribute != null; } } 16 | public bool Generated { get; } 17 | public ClassInfo ParentClass { get; } 18 | public StructInfo ParentStruct { get; } 19 | 20 | public string Name { get { return CodeClass.Name; } } 21 | public string FullName { get { return CodeClass.FullName; } } 22 | public EnvDTE.CodeNamespace CodeNamespace { get { return CodeClass.Namespace; } } 23 | public string GenericString { get; } 24 | 25 | public string Namespace { 26 | get { 27 | return CodeNamespace != null ? CodeNamespace.FullName : ""; 28 | } 29 | } 30 | 31 | public string TypeString { get { return "class"; } } 32 | 33 | public bool ShouldBeProcessed { get { return Immutable && !Generated; } } 34 | 35 | // these are evaluated after processing 36 | public List RelevantFields { get; private set; } 37 | public List RelevantProps { get; private set; } 38 | public bool HasPostConstructor { get; private set; } 39 | public bool EnableConstructor { get; private set; } 40 | public bool EnableEquals { get; private set; } 41 | public bool EnableOperatorEquals { get; private set; } 42 | public bool EnableGetHashCode { get; private set; } 43 | public bool EnableWith { get; private set; } 44 | public bool EnableToString { get; private set; } 45 | public string ConstructorAccessLevel { get; private set; } 46 | public bool AllowCustomConstructors { get; private set; } 47 | public string PreConstructor { get; private set; } 48 | public bool EnableBuilder { get; private set; } 49 | public string BuilderAccessLevel { get; private set; } 50 | public bool EnableToBuilder { get; private set; } 51 | 52 | public ClassInfo(CodeClass thisClass) { 53 | CodeClass = (EnvDTE80.CodeClass2)thisClass; 54 | 55 | ImmutableAttribute = FindAttribute(thisClass.Attributes, CustomImmutableClassAttribute) as EnvDTE80.CodeAttribute2; 56 | Generated = FindAttribute(thisClass.Attributes, CustomGeneratedCodeAttribute) != null; 57 | 58 | var parentClass = CodeClass.Parent as EnvDTE80.CodeClass2; 59 | var parentStruct = CodeClass.Parent as EnvDTE80.CodeStruct2; 60 | ParentClass = parentClass == null ? null : new ClassInfo(parentClass); 61 | ParentStruct = parentStruct == null ? null : new StructInfo(parentStruct); 62 | 63 | // fix for Foo.Bar 64 | var strippedFullName = FullName; 65 | if (ParentClass != null) strippedFullName = strippedFullName.Substring(ParentClass.FullName.Length + 1); 66 | if (ParentStruct != null) strippedFullName = strippedFullName.Substring(ParentStruct.FullName.Length + 1); 67 | GenericString = ExtractGeneric(strippedFullName); 68 | } 69 | 70 | private static bool IsPostConstructor(EnvDTE80.CodeFunction2 m) { 71 | return m.Name == "PostConstructor"; 72 | } 73 | 74 | private void ValidatePostConstructor(EnvDTE80.CodeFunction2 m) { 75 | string fileName = CodeClass.ProjectItem.Name; 76 | 77 | if (m.Name != "PostConstructor") 78 | throw new T4ImmutableException(fileName, FullName, "Immutable class PostConstructor() has a wrong name (internal error)"); 79 | 80 | if (m.IsGeneric) 81 | throw new T4ImmutableException(fileName, FullName, "Immutable class PostConstructor() must not be generic"); 82 | if (m.IsShared) 83 | throw new T4ImmutableException(fileName, FullName, "Immutable class PostConstructor() must not be static"); 84 | if (m.Type.TypeKind != vsCMTypeRef.vsCMTypeRefVoid) 85 | throw new T4ImmutableException(fileName, FullName, "Immutable class PostConstructor() must return void"); 86 | if (m.IsOverloaded) 87 | throw new T4ImmutableException(fileName, FullName, "Immutable class PostConstructor() must not have overloads"); 88 | if (m.Parameters.Count > 0) 89 | throw new T4ImmutableException(fileName, FullName, "Immutable class PostConstructor() must have no parameters"); 90 | } 91 | 92 | public void ProcessAndValidate() { 93 | string fileName = CodeClass.ProjectItem.Name; 94 | 95 | if (!ShouldBeProcessed) { 96 | throw new T4ImmutableException(fileName, FullName, "Unrelevant immutable class (internal error)"); 97 | } 98 | 99 | // process 100 | RelevantFields = CodeClass.Members.OfType().Select(p => new FieldInfo(p)).Where(p => p.Relevant).ToList(); 101 | RelevantProps = CodeClass.Members.OfType().Select(p => new PropInfo(p)).Where(p => p.Relevant).ToList(); 102 | foreach (var prop in RelevantProps) { 103 | prop.UpdateDefaultValue(RelevantFields); 104 | } 105 | 106 | HasPostConstructor = CodeClass.Members.OfType().Any(m => IsPostConstructor(m)); 107 | 108 | // default settings 109 | EnableConstructor = true; 110 | EnableEquals = true; 111 | EnableOperatorEquals = false; 112 | EnableGetHashCode = true; 113 | EnableWith = true; 114 | EnableToString = true; 115 | ConstructorAccessLevel = "public"; 116 | AllowCustomConstructors = false; 117 | PreConstructor = null; 118 | EnableBuilder = true; 119 | BuilderAccessLevel = "public"; 120 | EnableToBuilder = true; 121 | 122 | // disable certain features depending on the attrib props 123 | foreach(var arg in ImmutableAttribute.Arguments.OfType()) { 124 | var val = arg.Value.Trim(); 125 | 126 | const string optionsArg = "Options"; 127 | const string constructorAccessLevelArg = "ConstructorAccessLevel"; 128 | const string preConstructorArg = "PreConstructor"; 129 | const string builderAccessLevelArg = "BuilderAccessLevel"; 130 | 131 | if (arg.Name == optionsArg) { 132 | object parsed; 133 | try { 134 | parsed = Parser.ParseLiteral(CustomNamespace + ".ImmutableClassOptions", val); 135 | } 136 | catch(ParserException ex) { 137 | throw new T4ImmutableException(fileName, FullName, ex.Message); 138 | } 139 | 140 | var options = Parser.SplitEnumFields(parsed); 141 | foreach (var option in options) { 142 | switch(option) { 143 | case "ExcludeEquals": 144 | EnableEquals = false; 145 | break; 146 | case "ExcludeGetHashCode": 147 | EnableGetHashCode = false; 148 | break; 149 | case "IncludeOperatorEquals": 150 | EnableOperatorEquals = true; 151 | break; 152 | case "ExcludeToString": 153 | EnableToString = false; 154 | break; 155 | case "ExcludeWith": 156 | EnableWith = false; 157 | break; 158 | case "ExcludeConstructor": 159 | EnableConstructor = false; 160 | break; 161 | case "AllowCustomConstructors": 162 | AllowCustomConstructors = true; 163 | break; 164 | case "ExcludeBuilder": 165 | EnableBuilder = false; 166 | // implies 167 | EnableToBuilder = false; 168 | break; 169 | case "ExcludeToBuilder": 170 | EnableToBuilder = false; 171 | break; 172 | default: 173 | throw new T4ImmutableException(fileName, FullName, "Error parsing " + optionsArg + " argument of ImmutableClassAttribute"); 174 | } 175 | } 176 | } 177 | else if (arg.Name == constructorAccessLevelArg) { 178 | object parsed; 179 | try { 180 | parsed = Parser.ParseLiteral(CustomNamespace + ".ConstructorAccessLevel", val); 181 | } 182 | catch(ParserException ex) { 183 | throw new T4ImmutableException(fileName, FullName, ex.Message); 184 | } 185 | 186 | switch(parsed.ToString()) { 187 | case "Public": 188 | ConstructorAccessLevel = "public"; 189 | break; 190 | case "Protected": 191 | ConstructorAccessLevel = "protected"; 192 | break; 193 | case "Internal": 194 | ConstructorAccessLevel = "internal"; 195 | break; 196 | case "Private": 197 | ConstructorAccessLevel = "private"; 198 | break; 199 | case "ProtectedInternal": 200 | ConstructorAccessLevel = "protected internal"; 201 | break; 202 | default: 203 | throw new T4ImmutableException(fileName, FullName, "Error parsing " + constructorAccessLevelArg + " argument of ImmutableClassAttribute"); 204 | } 205 | } 206 | else if (arg.Name == builderAccessLevelArg) { 207 | object parsed; 208 | try { 209 | parsed = Parser.ParseLiteral(CustomNamespace + ".BuilderAccessLevel", val); 210 | } 211 | catch(ParserException ex) { 212 | throw new T4ImmutableException(fileName, FullName, ex.Message); 213 | } 214 | 215 | switch(parsed.ToString()) { 216 | case "Public": 217 | BuilderAccessLevel = "public"; 218 | break; 219 | case "Protected": 220 | BuilderAccessLevel = "protected"; 221 | break; 222 | case "Internal": 223 | BuilderAccessLevel = "internal"; 224 | break; 225 | case "Private": 226 | BuilderAccessLevel = "private"; 227 | break; 228 | case "ProtectedInternal": 229 | BuilderAccessLevel = "protected internal"; 230 | break; 231 | default: 232 | throw new T4ImmutableException(fileName, FullName, "Error parsing " + builderAccessLevelArg + " argument of ImmutableClassAttribute"); 233 | } 234 | } 235 | else if (arg.Name == preConstructorArg) { 236 | try { 237 | PreConstructor = Parser.ParseLiteral("string", val); 238 | } 239 | catch(ParserException ex) { 240 | throw new T4ImmutableException(fileName, FullName, ex.Message); 241 | } 242 | } 243 | } 244 | 245 | // validation 246 | var partialClasses = VSH.CodeModel.GetPartialClasses(CodeClass).Select(c => new ClassInfo(c)).ToList(); 247 | 248 | // do not support static classes 249 | if (CodeClass.IsShared) { 250 | throw new T4ImmutableException(fileName, FullName, "Immutable classes cannot be static"); 251 | } 252 | 253 | // don't support partial immutable classes (not counting generated ones) 254 | // TODO: probably we should in the future 255 | { 256 | int partials = partialClasses.Count(c => !c.Generated); 257 | if (partials > 1) { 258 | throw new T4ImmutableException(fileName, FullName, "Immutable classes cannot be partial (but for generated partials)"); 259 | } 260 | } 261 | 262 | // do not support base classes 263 | // TODO: maybe we should in the future? maybe only base classes with constructors without args? 264 | var baseClass = VSH.CodeModel.GetBaseClass(CodeClass); 265 | if (baseClass != null && baseClass.FullName != "System.Object") { 266 | throw new T4ImmutableException(fileName, FullName, "Immutable classes cannot have a base class"); 267 | } 268 | 269 | if (!AllowCustomConstructors) { 270 | // no constructors allowed 271 | var memberFuncs = CodeClass.Members.OfType().ToList(); 272 | var constructors = memberFuncs.Where(m => m.FunctionKind == vsCMFunction.vsCMFunctionConstructor).ToList(); 273 | if (constructors.Count > 0) { 274 | throw new T4ImmutableException(fileName, FullName, "Immutable classes cannot have constructors but for generated ones (use ImmutableClassOptions.AllowCustomConstructors if you want to remove this constraint)"); 275 | } 276 | } 277 | 278 | if (HasPostConstructor) { 279 | foreach (var m in CodeClass.Members.OfType().Where(m => IsPostConstructor(m))) { 280 | ValidatePostConstructor(m); 281 | } 282 | } 283 | 284 | // validate each field 285 | foreach (var field in RelevantFields) { 286 | field.Validate(); 287 | } 288 | 289 | // process and validate each property 290 | foreach (var prop in RelevantProps) { 291 | prop.ProcessAndValidate(); 292 | } 293 | } 294 | 295 | public void MarkAsPartial() { 296 | string fileName = CodeClass.ProjectItem.Name; 297 | if (CodeClass.DataTypeKind != EnvDTE80.vsCMDataTypeKind.vsCMDataTypeKindPartial) { 298 | try { 299 | CodeClass.DataTypeKind = EnvDTE80.vsCMDataTypeKind.vsCMDataTypeKindPartial; 300 | } 301 | catch { 302 | AddWarning(fileName + " - Unable to mark class " + FullName + " as partial. Please change it manually if possible"); 303 | } 304 | } 305 | } 306 | 307 | } 308 | #> -------------------------------------------------------------------------------- /src/content/T4Immutable/Field.ttinclude: -------------------------------------------------------------------------------- 1 | <#@ assembly Name="System.Core" #> 2 | <#@ assembly name="EnvDTE" #> 3 | <#@ assembly name="EnvDTE80" #> 4 | <#@ import namespace="System" #> 5 | <#@ import namespace="System.IO" #> 6 | <#@ import namespace="System.Diagnostics" #> 7 | <#@ import namespace="System.Linq" #> 8 | <#@ import namespace="System.Collections" #> 9 | <#@ import namespace="System.Collections.Generic" #> 10 | <#@ import namespace="EnvDTE" #> 11 | <#+ 12 | public class FieldInfo { 13 | public EnvDTE80.CodeVariable2 CodeVariable { get; } 14 | public string Name { get { return CodeVariable.Name; } } 15 | public string FullName { get { return CodeVariable.FullName; } } 16 | public string TypeString { get; } 17 | public bool Relevant { get; } 18 | public string InitExpressionString { get; } 19 | 20 | public FieldInfo(CodeVariable field) { 21 | CodeVariable = (EnvDTE80.CodeVariable2)field; 22 | TypeString = CodeVariable.Type.AsString; 23 | InitExpressionString = CodeVariable.InitExpression as string; 24 | 25 | // a field is relevant if it ends with "DefaultValue" and is "static readonly" or "const" 26 | bool relevantName = Name.EndsWith("DefaultValue") && Name != "DefaultValue"; 27 | bool relevantModifiers = CodeVariable.IsConstant || (CodeVariable.IsShared && CodeVariable.ConstKind == EnvDTE80.vsCMConstKind.vsCMConstKindReadOnly); 28 | Relevant = relevantName && relevantModifiers; 29 | } 30 | 31 | public void Validate() { 32 | string fileName = CodeVariable.ProjectItem.Name; 33 | 34 | if (!Relevant) { 35 | throw new T4ImmutableException(fileName, FullName, "Unrelevant default value field (internal error)"); 36 | } 37 | 38 | if (InitExpressionString == null) { 39 | throw new T4ImmutableException(fileName, FullName, "Default value field cannot be uninitialized"); 40 | } 41 | } 42 | } 43 | #> -------------------------------------------------------------------------------- /src/content/T4Immutable/Parser.ttinclude: -------------------------------------------------------------------------------- 1 | <#@ assembly Name="System.Core" #> 2 | <#@ import namespace="System" #> 3 | <#@ import namespace="System.IO" #> 4 | <#@ import namespace="System.Diagnostics" #> 5 | <#@ import namespace="System.Linq" #> 6 | <#@ import namespace="System.Collections" #> 7 | <#@ import namespace="System.Collections.Generic" #> 8 | <# 9 | CustomCommentOptions[CustomCommentOptionDomain.Class] = new string[] { 10 | }; 11 | 12 | CustomCommentOptions[CustomCommentOptionDomain.Property] = new string[] { 13 | }; 14 | #> 15 | <#+ 16 | public class ParserException : Exception { 17 | public ParserException(string msg) : base(msg) { 18 | } 19 | } 20 | 21 | public static class Parser { 22 | private static string _AttributesCs = null; 23 | 24 | public static void ReadAttributesCs(string projPath) { 25 | // read the file contents 26 | _AttributesCs = File.ReadAllText(Path.Combine(projPath, "Attributes.cs_")); 27 | } 28 | 29 | public static string[] SplitEnumFields(object e) { 30 | return e.ToString().Split(new char[] { ' ', ',', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); 31 | } 32 | 33 | public static T ParseLiteral(string type, string value) 34 | { 35 | if (_AttributesCs == null) { 36 | throw new Exception("Attributes.cs_ should be read before parsing is allowed"); 37 | } 38 | 39 | using (var provider = System.CodeDom.Compiler.CodeDomProvider.CreateProvider("CSharp", new Dictionary() { { "CompilerVersion", "v4.0" } })) { 40 | var cp = new System.CodeDom.Compiler.CompilerParameters { 41 | GenerateExecutable = false, 42 | GenerateInMemory = true, 43 | IncludeDebugInformation = true 44 | }; 45 | 46 | var src = "using " + CustomNamespace + ";\n" + _AttributesCs + string.Format( 47 | "\nnamespace T4ImmutableGenerated {{ public static class FromLiteral {{ public static {0} Convert() {{ return {1}; }} }} }}", 48 | type, value); 49 | 50 | var results = provider.CompileAssemblyFromSource(cp, new string[] { src }); 51 | 52 | if (results.Errors.HasErrors) { 53 | var errList = new System.CodeDom.Compiler.CompilerError[results.Errors.Count]; 54 | results.Errors.CopyTo(errList, 0); 55 | throw new ParserException( 56 | string.Format("Error parsing value '{0}' of type '{1}' - ", value, type) 57 | + string.Join("; ", errList.Where(p => !p.IsWarning).Select(p => p.ErrorText))); 58 | } 59 | 60 | var asm = results.CompiledAssembly; 61 | var asmType = asm.ExportedTypes.First(t => t.FullName == "T4ImmutableGenerated.FromLiteral"); 62 | var mi = asmType.GetMethod("Convert"); 63 | return (T)mi.Invoke(null, null); 64 | } 65 | } 66 | } 67 | 68 | // comment options 69 | enum CustomCommentOptionDomain { 70 | Class, 71 | Property 72 | } 73 | 74 | static readonly Dictionary CustomCommentOptions = new Dictionary(); 75 | 76 | static Dictionary ParseCommentOptions(string fileName, string itemName, string comment, CustomCommentOptionDomain domain) { 77 | const string keyValueSep = ":"; 78 | var dict = new Dictionary(); 79 | if (!string.IsNullOrWhiteSpace(comment)) { 80 | var lines = comment.Split(new string[] {"\r", "\n"}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList(); 81 | foreach (var line in lines) { 82 | // get option name 83 | var sepIndex = line.IndexOf(keyValueSep); 84 | if (sepIndex < 0) continue; 85 | 86 | var optionName = line.Substring(0, sepIndex).Trim(); 87 | if (string.IsNullOrWhiteSpace(optionName)) continue; 88 | if (!optionName.StartsWith(CustomNamespace + ".")) continue; 89 | 90 | if (dict.ContainsKey(optionName)) { 91 | AddWarning(fileName + " - " + itemName + " - Comment option \"" + optionName + "\" duplicated"); 92 | } 93 | 94 | if (!CustomCommentOptions[domain].Any(o => o == optionName)) { 95 | AddWarning(fileName + " - " + itemName + " - Comment option \"" + optionName + "\" is unknown for domain " + domain); 96 | continue; 97 | } 98 | 99 | dict[optionName] = line.Substring(sepIndex + 1).Trim(); 100 | } 101 | } 102 | return dict; 103 | } 104 | 105 | #> -------------------------------------------------------------------------------- /src/content/T4Immutable/Prop.ttinclude: -------------------------------------------------------------------------------- 1 | <#@ assembly Name="System.Core" #> 2 | <#@ assembly name="EnvDTE" #> 3 | <#@ assembly name="EnvDTE80" #> 4 | <#@ import namespace="System" #> 5 | <#@ import namespace="System.IO" #> 6 | <#@ import namespace="System.Diagnostics" #> 7 | <#@ import namespace="System.Linq" #> 8 | <#@ import namespace="System.Collections" #> 9 | <#@ import namespace="System.Collections.Generic" #> 10 | <#@ import namespace="EnvDTE" #> 11 | <#+ 12 | public class PropInfo { 13 | public EnvDTE80.CodeProperty2 CodeProperty { get; } 14 | public string Name { get { return CodeProperty.Name; } } 15 | public string FullName { get { return CodeProperty.FullName; } } 16 | public CodeFunction Getter { get { return CodeProperty.Getter; } } 17 | public CodeFunction Setter { get { return CodeProperty.Setter; } } 18 | public bool Computed { get; } 19 | public bool JetBrainsNotNull { get; } 20 | public string TypeString { get; } 21 | public string ParamName { get; } 22 | public FieldInfo DefaultValue { get; private set; } 23 | 24 | public bool Relevant { get { return !Computed; } } 25 | 26 | // these are evaluated after processing 27 | public bool ConstructorParamNotNull { get; private set; } 28 | public bool PreNotNullCheck { get; private set; } 29 | public bool PostNotNullCheck { get; private set; } 30 | public string PreConstructorParamString { get; private set; } 31 | 32 | public PropInfo(CodeProperty prop) { 33 | CodeProperty = (EnvDTE80.CodeProperty2)prop; 34 | 35 | Computed = FindAttribute(prop.Attributes, CustomImmutableComputedPropertyAttribute) != null; 36 | TypeString = CodeProperty.Type.AsString; 37 | ParamName = ToCamelCase(Name); 38 | 39 | JetBrainsNotNull = FindAttribute(prop.Attributes, JetBrainsNotNullAttribute) != null; 40 | PostNotNullCheck = JetBrainsNotNull || FindAttribute(prop.Attributes, CustomPostNotNullCheckAttribute) != null; 41 | } 42 | 43 | public string ToFullParam(bool includeNotNull, bool includeAssign, bool includePre) { 44 | var str = ""; 45 | if (includePre && !string.IsNullOrWhiteSpace(PreConstructorParamString)) { 46 | str += PreConstructorParamString + " "; 47 | } 48 | if (includeNotNull && ConstructorParamNotNull) { 49 | str += "[" + RemoveAttributeTail(JetBrainsNotNullAttribute) + "] "; 50 | } 51 | str += TypeString + " " + ParamName; 52 | if (includeAssign && DefaultValue != null) { 53 | str += " = " + DefaultValue.InitExpressionString; 54 | } 55 | return str; 56 | } 57 | 58 | public string ToFullOptParam(bool useParamName, bool includeDefault) { 59 | var name = useParamName ? ParamName : Name; 60 | var defaultPart = includeDefault ? " = default(T4Immutable.OptParam<" + TypeString + ">)" : ""; 61 | return ToOptParam() + " " + name + defaultPart; 62 | } 63 | 64 | public string ToOptParam() { 65 | return CustomNamespace + ".OptParam<" + TypeString + ">"; 66 | } 67 | 68 | public void UpdateDefaultValue(List fields) { 69 | DefaultValue = fields.FirstOrDefault(f => f.Name == Name + "DefaultValue"); 70 | } 71 | 72 | public void ProcessAndValidate() { 73 | string fileName = CodeProperty.ProjectItem.Name; 74 | 75 | if (!Relevant) { 76 | throw new T4ImmutableException(fileName, FullName, "Unrelevant immutable property (internal error)"); 77 | } 78 | 79 | // process 80 | ConstructorParamNotNull = FindAttribute(CodeProperty.Attributes, CustomConstructorParamNotNullAttribute) != null; 81 | PreNotNullCheck = ConstructorParamNotNull || FindAttribute(CodeProperty.Attributes, CustomPreNotNullCheckAttribute) != null; 82 | 83 | var preAttrib = FindAttribute(CodeProperty.Attributes, CustomPreConstructorParamAttribute); 84 | if (preAttrib != null) { 85 | var val = preAttrib.Value.Trim(); 86 | string parsed; 87 | try { 88 | parsed = Parser.ParseLiteral("string", val); 89 | } 90 | catch(ParserException ex) { 91 | throw new T4ImmutableException(fileName, FullName, CustomPreConstructorParamAttribute + " - " + ex.Message); 92 | } 93 | 94 | PreConstructorParamString = parsed; 95 | } 96 | 97 | // validation 98 | // make sure the default value (if any) type matches 99 | if (DefaultValue != null) { 100 | if (DefaultValue.TypeString != TypeString) { 101 | throw new T4ImmutableException(fileName, FullName, "Immutable property type (" + TypeString + ") does not match " + DefaultValue.Name + " type (" + DefaultValue.TypeString + ")"); 102 | } 103 | } 104 | 105 | // make sure all of them have getters 106 | if (Getter == null) { 107 | throw new T4ImmutableException(fileName, FullName, "Immutable property must have a getter"); 108 | } 109 | 110 | // make sure none of them have public setters 111 | if (Setter != null && Setter.Access == vsCMAccess.vsCMAccessPublic) { 112 | throw new T4ImmutableException(fileName, FullName, "Immutable property setter must not be public"); 113 | } 114 | 115 | // and they are not static 116 | if (CodeProperty.IsShared) { 117 | throw new T4ImmutableException(fileName, FullName, "Immutable property must not be static"); 118 | } 119 | 120 | // TODO: check override? not sure why we would though 121 | } 122 | } 123 | #> -------------------------------------------------------------------------------- /src/content/T4Immutable/Shared.ttinclude: -------------------------------------------------------------------------------- 1 | <#@ assembly Name="System.Core" #> 2 | <#@ assembly name="EnvDTE" #> 3 | <#@ assembly name="EnvDTE80" #> 4 | <#@ import namespace="System" #> 5 | <#@ import namespace="System.IO" #> 6 | <#@ import namespace="System.Diagnostics" #> 7 | <#@ import namespace="System.Linq" #> 8 | <#@ import namespace="System.Collections" #> 9 | <#@ import namespace="System.Collections.Generic" #> 10 | <#@ import namespace="EnvDTE" #> 11 | <# 12 | TT = this; 13 | VSH = this.VisualStudioHelper; 14 | #> 15 | <#+ 16 | // constants 17 | const string ToolName = "T4Immutable"; 18 | const string ToolVersion = "1.4.4"; 19 | const string CustomNamespace = "T4Immutable"; 20 | 21 | const string CustomGeneratedCodeAttribute = CustomNamespace + ".GeneratedCodeAttribute"; 22 | 23 | // property attributes 24 | const string CustomPostNotNullCheckAttribute = CustomNamespace + ".PostNotNullCheckAttribute"; 25 | const string CustomPreNotNullCheckAttribute = CustomNamespace + ".PreNotNullCheckAttribute"; 26 | const string CustomConstructorParamNotNullAttribute = CustomNamespace + ".ConstructorParamNotNullAttribute"; 27 | const string CustomImmutableComputedPropertyAttribute = CustomNamespace + ".ComputedPropertyAttribute"; 28 | const string CustomPreConstructorParamAttribute = CustomNamespace + ".PreConstructorParamAttribute"; 29 | 30 | // class attributes 31 | const string CustomImmutableClassAttribute = CustomNamespace + ".ImmutableClassAttribute"; 32 | 33 | // misc attributes 34 | const string CompilerGeneratedCodeAttribute = "System.CodeDom.Compiler.GeneratedCodeAttribute"; 35 | const string JetBrainsNotNullAttribute = "JetBrains.Annotations.NotNullAttribute"; 36 | const string DebuggerNonUserCodeAttribute = "System.Diagnostics.DebuggerNonUserCodeAttribute"; 37 | 38 | static TextTransformation TT; 39 | static AutomationHelper VSH; 40 | 41 | static void AddWarning(string str) { 42 | TT.Warning(ToolName + ": " + str); 43 | } 44 | 45 | static void AddError(string str) { 46 | TT.Error(ToolName + ": " + str); 47 | } 48 | 49 | static CodeAttribute FindAttribute(CodeElements attribs, string fullName) { 50 | return attribs.OfType().FirstOrDefault(att => att.FullName == fullName); 51 | } 52 | 53 | static string ToCamelCase(string theString) { 54 | // convert from pascal to camel case 55 | string outStr = ""; 56 | bool converting = true; 57 | for (int i = 0; i < theString.Length; i++) { 58 | string c = theString[i].ToString(); 59 | if (converting) { 60 | string lowerC = c.ToLowerInvariant(); 61 | bool isLowerCase = lowerC == c; 62 | if (isLowerCase) { 63 | converting = false; 64 | } 65 | else { 66 | c = lowerC; 67 | } 68 | } 69 | outStr += c; 70 | } 71 | return outStr; 72 | } 73 | 74 | static string ExtractGeneric(string str) { 75 | int indexOf = str.IndexOf("<"); 76 | if (indexOf < 0) return ""; 77 | return str.Substring(indexOf); 78 | } 79 | 80 | static string RemoveAttributeTail(string attr) { 81 | const string attrTail = "Attribute"; 82 | if (attr.EndsWith(attrTail)) { 83 | return attr.Substring(0, attr.Length - attrTail.Length); 84 | } 85 | return attr; 86 | } 87 | 88 | public class T4ImmutableException : Exception { 89 | public T4ImmutableException(string fileName, string itemName, string msg) : base("[" + fileName + "] " + itemName + " - " + msg) { 90 | } 91 | } 92 | 93 | public class ClassWriter { 94 | private List Lines { get; } 95 | 96 | public ClassWriter() { 97 | Lines = new List(); 98 | } 99 | 100 | public void WI(int indentation, string line) { 101 | const int indentSize = 2; 102 | string str = ""; 103 | for (int i = 0; i < indentation * indentSize; i++) { 104 | str += " "; 105 | } 106 | Lines.Add(str + line); 107 | } 108 | 109 | public void WIGenerated(int indentation, bool includeDebuggerNonUserCode = true, bool includeCompilerGenerated = true) { 110 | string str = "[" + RemoveAttributeTail(CustomGeneratedCodeAttribute); 111 | if (includeCompilerGenerated) { 112 | str += ", " + RemoveAttributeTail(CompilerGeneratedCodeAttribute) + "(\"" + ToolName + "\", \"" + ToolVersion + "\")"; 113 | } 114 | if (includeDebuggerNonUserCode) { 115 | str += ", " + RemoveAttributeTail(DebuggerNonUserCodeAttribute); 116 | } 117 | str += "]"; 118 | WI(indentation, str); 119 | } 120 | 121 | public void Clear() { 122 | Lines.Clear(); 123 | } 124 | 125 | public void WriteOut() { 126 | foreach (var line in Lines) { 127 | TT.WriteLine(line); 128 | } 129 | } 130 | } 131 | #> -------------------------------------------------------------------------------- /src/content/T4Immutable/Struct.ttinclude: -------------------------------------------------------------------------------- 1 | <#@ assembly Name="System.Core" #> 2 | <#@ assembly name="EnvDTE" #> 3 | <#@ assembly name="EnvDTE80" #> 4 | <#@ import namespace="System" #> 5 | <#@ import namespace="System.IO" #> 6 | <#@ import namespace="System.Diagnostics" #> 7 | <#@ import namespace="System.Linq" #> 8 | <#@ import namespace="System.Collections" #> 9 | <#@ import namespace="System.Collections.Generic" #> 10 | <#@ import namespace="EnvDTE" #> 11 | <#+ 12 | public class StructInfo { 13 | public EnvDTE80.CodeStruct2 CodeStruct { get; } 14 | public bool Generated { get; } 15 | public ClassInfo ParentClass { get; } 16 | public StructInfo ParentStruct { get; } 17 | 18 | public string Name { get { return CodeStruct.Name; } } 19 | public string FullName { get { return CodeStruct.FullName; } } 20 | public EnvDTE.CodeNamespace CodeNamespace { get { return CodeStruct.Namespace; } } 21 | public string GenericString { get; } 22 | 23 | public string Namespace { 24 | get { 25 | return CodeNamespace != null ? CodeNamespace.FullName : ""; 26 | } 27 | } 28 | 29 | public string TypeString { get { return "struct"; } } 30 | 31 | public StructInfo(CodeStruct thisStruct) { 32 | CodeStruct = (EnvDTE80.CodeStruct2)thisStruct; 33 | 34 | Generated = FindAttribute(thisStruct.Attributes, CustomGeneratedCodeAttribute) != null; 35 | 36 | var parentClass = CodeStruct.Parent as EnvDTE80.CodeClass2; 37 | var parentStruct = CodeStruct.Parent as EnvDTE80.CodeStruct2; 38 | ParentClass = parentClass == null ? null : new ClassInfo(parentClass); 39 | ParentStruct = parentStruct == null ? null : new StructInfo(parentStruct); 40 | 41 | // fix for Foo.Bar 42 | var strippedFullName = FullName; 43 | if (ParentClass != null) strippedFullName = strippedFullName.Substring(ParentClass.FullName.Length + 1); 44 | if (ParentStruct != null) strippedFullName = strippedFullName.Substring(ParentStruct.FullName.Length + 1); 45 | GenericString = ExtractGeneric(strippedFullName); 46 | } 47 | 48 | public void MarkAsPartial() { 49 | string fileName = CodeStruct.ProjectItem.Name; 50 | if (CodeStruct.DataTypeKind != EnvDTE80.vsCMDataTypeKind.vsCMDataTypeKindPartial) { 51 | try { 52 | CodeStruct.DataTypeKind = EnvDTE80.vsCMDataTypeKind.vsCMDataTypeKindPartial; 53 | } 54 | catch { 55 | AddWarning(fileName + " - Unable to mark struct " + FullName + " as partial. Please change it manually if possible"); 56 | } 57 | } 58 | } 59 | 60 | } 61 | #> -------------------------------------------------------------------------------- /src/content/T4Immutable/T4Immutable.tt: -------------------------------------------------------------------------------- 1 | <#@ template language="C#" debug="true" hostSpecific="true" #> 2 | <#@ output extension=".log" #> 3 | <#@ include file=".\VisualStudioHelper.ttinclude" #> 4 | <#@ include file=".\TemplateFileManagerV2.1.ttinclude" #> 5 | <#@ include file=".\Shared.ttinclude" #> 6 | <#@ include file=".\Parser.ttinclude" #> 7 | <#@ include file=".\Field.ttinclude" #> 8 | <#@ include file=".\Prop.ttinclude" #> 9 | <#@ include file=".\Struct.ttinclude" #> 10 | <#@ include file=".\Class.ttinclude" #> 11 | <#@ assembly Name="System.Core" #> 12 | <#@ assembly name="EnvDTE" #> 13 | <#@ assembly name="EnvDTE80" #> 14 | <#@ import namespace="System" #> 15 | <#@ import namespace="System.IO" #> 16 | <#@ import namespace="System.Diagnostics" #> 17 | <#@ import namespace="System.Linq" #> 18 | <#@ import namespace="System.Collections" #> 19 | <#@ import namespace="System.Collections.Generic" #> 20 | <#@ import namespace="EnvDTE" #> 21 | <# 22 | // To debug, uncomment the next two lines !! 23 | // Debugger.Launch(); 24 | // Debugger.Break(); 25 | 26 | var curProject = VSH.CurrentProject; 27 | var curProjectDir = System.IO.Path.GetDirectoryName(curProject.FullName); 28 | var t4ProjectDir = Path.Combine(curProjectDir, "T4Immutable_generated"); 29 | 30 | // we are in isolated mode (e.g. .net core) if the template folder is not directly a subfolder of the project 31 | var isolatedMode = Path.GetFullPath(Path.Combine(curProjectDir, "T4Immutable")) != Path.GetFullPath(Path.GetDirectoryName(VSH.Host.TemplateFile)); 32 | 33 | var manager = TemplateFileManager.Create(this, isolatedMode ? t4ProjectDir : null); 34 | //manager.IsAutoIndentEnabled = true; // this makes generation unreliable (and slower) when set to true 35 | manager.CanOverrideExistingFile = true; 36 | var outFileNamesUsed = new HashSet(); 37 | 38 | // init the parser 39 | Parser.ReadAttributesCs(Path.GetDirectoryName(VSH.Host.TemplateFile)); 40 | 41 | // .net core does not automatically remove old generated files and does not allow writing to the t4 folder directly, so we do it ourselves 42 | if (isolatedMode) { 43 | Directory.CreateDirectory(t4ProjectDir); 44 | var oldFiles = Directory.GetFiles(t4ProjectDir); 45 | foreach (var oldFile in oldFiles) { 46 | if (oldFile.EndsWith(".t4g.cs")) { 47 | File.Delete(oldFile); 48 | } 49 | } 50 | } 51 | 52 | var allProjectItems = VSH.GetAllProjectItems(); 53 | foreach (ProjectItem pitem in allProjectItems) { 54 | var fileName = pitem.Name; 55 | if (!fileName.ToLowerInvariant().EndsWith(".cs")) continue; 56 | if (fileName.ToLowerInvariant().EndsWith(".t4g.cs")) continue; 57 | 58 | var codeModel = pitem.FileCodeModel; 59 | if (codeModel == null || codeModel.CodeElements == null) continue; 60 | 61 | var classes = VSH.CodeModel.GetAllCodeElementsOfType(codeModel.CodeElements, vsCMElement.vsCMElementClass, false).OfType(); 62 | foreach (var thisClass in classes) { 63 | var info = new ClassInfo(thisClass); 64 | if (!info.ShouldBeProcessed) continue; 65 | 66 | var outFileName = info.Name; 67 | 68 | // find a proper filename four the output 69 | // in case the class is defined in several parts then we will use name, name-2, name-3, etc 70 | int number = 1; 71 | var realOutFileName = outFileName; 72 | while (outFileNamesUsed.Contains(realOutFileName.ToLowerInvariant())) { 73 | number++; 74 | realOutFileName = outFileName + "-" + number; 75 | } 76 | outFileNamesUsed.Add(realOutFileName.ToLowerInvariant()); 77 | 78 | realOutFileName += ".t4g.cs"; 79 | 80 | manager.StartNewFile(realOutFileName); 81 | 82 | var fw = new ClassWriter(); 83 | try { 84 | ProcessClass(fileName, info, fw); 85 | } 86 | catch (T4ImmutableException ex) { 87 | fw.Clear(); 88 | WriteHeader(fw); 89 | fw.WI(0, "// Generation not done due to the following errors:"); 90 | fw.WI(0, "// - " + ex.Message); 91 | 92 | AddError(ex.Message); 93 | } 94 | finally { 95 | fw.WriteOut(); 96 | } 97 | } 98 | } 99 | 100 | manager.Process(); 101 | #> 102 | <#+ 103 | static void WriteHeader(ClassWriter fw) { 104 | fw.WI(0, "// "); 105 | fw.WI(0, "// This file was generated by T4Immutable"); 106 | fw.WI(0, "// Don't change it directly as your change would get overwritten. Run T4Immutable.tt or \"Build - Transform All T4 Templates\" instead"); 107 | fw.WI(0, ""); 108 | } 109 | 110 | static void ProcessClass(string fileName, ClassInfo info, ClassWriter fw) { 111 | WriteHeader(fw); 112 | 113 | fw.WI(0, "using System;"); 114 | fw.WI(0, ""); 115 | 116 | int ind = 0; 117 | 118 | info.ProcessAndValidate(); 119 | var allProps = info.RelevantProps; 120 | 121 | // make our partial class with all the boilerplate 122 | bool enableConstructor = info.EnableConstructor; 123 | bool enableEquals = info.EnableEquals; 124 | bool enableOperatorEquals = info.EnableOperatorEquals; 125 | bool enableGetHashCode = info.EnableGetHashCode; 126 | bool enableWith = info.EnableWith; 127 | bool enableToString = info.EnableToString; 128 | string constructorAccessLevel = info.ConstructorAccessLevel; 129 | bool allowCustomConstructors = info.AllowCustomConstructors; 130 | bool enableBuilder = info.EnableBuilder; 131 | string builderAccessLevel = info.BuilderAccessLevel; 132 | bool enableToBuilder = info.EnableToBuilder; 133 | 134 | // namespace 135 | var namesp = info.Namespace; 136 | bool hasNamesp = !string.IsNullOrWhiteSpace(namesp); 137 | if (hasNamesp) { 138 | fw.WI(ind, "namespace " + namesp + " {"); 139 | ind++; 140 | } 141 | 142 | // get the list of parents 143 | var parents = new List(); 144 | { 145 | var currentClass = info.ParentClass; 146 | var currentStruct = info.ParentStruct; 147 | while (currentClass != null || currentStruct != null) { 148 | if (currentClass != null) { 149 | parents.Add(currentClass); 150 | 151 | // the order matters! 152 | currentStruct = currentClass.ParentStruct; 153 | currentClass = currentClass.ParentClass; 154 | } 155 | else { 156 | parents.Add(currentStruct); 157 | 158 | // the order matters! 159 | currentClass = currentStruct.ParentClass; 160 | currentStruct = currentStruct.ParentStruct; 161 | } 162 | } 163 | } 164 | parents.Reverse(); 165 | 166 | // parent classes / structs 167 | foreach (var p in parents) { 168 | var pClass = p as ClassInfo; 169 | var pStruct = p as StructInfo; 170 | fw.WIGenerated(ind, false, false); 171 | if (pClass != null) { 172 | fw.WI(ind, "partial class " + pClass.Name + pClass.GenericString + " {"); 173 | } 174 | else { 175 | fw.WI(ind, "partial struct " + pStruct.Name + pStruct.GenericString + " {"); 176 | } 177 | ind++; 178 | } 179 | 180 | // type header 181 | { 182 | var interfaces = ""; 183 | if (enableEquals) { 184 | interfaces = " : IEquatable<" + info.FullName + ">"; 185 | } 186 | 187 | fw.WIGenerated(ind); 188 | fw.WI(ind, "partial " + info.TypeString + " " + info.Name + info.GenericString + interfaces + " {"); 189 | ind++; 190 | } 191 | 192 | // constructor 193 | if (enableConstructor) { 194 | fw.WIGenerated(ind); 195 | if (!string.IsNullOrWhiteSpace(info.PreConstructor)) { 196 | fw.WI(ind, info.PreConstructor); 197 | } 198 | fw.WI(ind, constructorAccessLevel + " " + info.Name + "(" + string.Join(", ", allProps.Select(p => p.ToFullParam(true, true, true))) + ") {"); 199 | ind++; 200 | 201 | // pre not-null checks 202 | foreach (var p in allProps.Where(p => p.PreNotNullCheck)) { 203 | fw.WI(ind, "if (ReferenceEquals(" + p.ParamName + ", null)) throw new ArgumentNullException(nameof(" + p.ParamName + "));"); 204 | } 205 | 206 | // assignations 207 | foreach (var prop in allProps) { 208 | fw.WI(ind, "this." + prop.Name + " = " + prop.ParamName + ";"); 209 | } 210 | 211 | // PostConstructor 212 | if (info.HasPostConstructor) { 213 | fw.WI(ind, "PostConstructor();"); 214 | } 215 | 216 | // post not-null checks 217 | foreach (var p in allProps.Where(p => p.PostNotNullCheck)) { 218 | fw.WI(ind, "if (ReferenceEquals(this." + p.Name + ", null)) throw new NullReferenceException(nameof(this." + p.Name + "));"); 219 | } 220 | 221 | fw.WI(ind, "_ImmutableHashCode = T4Immutable.Helpers.GetHashCodeFor(" + string.Join(", ", allProps.Select(p => "this." + p.Name)) + ");"); 222 | 223 | ind--; 224 | fw.WI(ind, "}"); 225 | fw.WI(ind, ""); 226 | } 227 | 228 | // Equals 229 | { 230 | fw.WIGenerated(ind); 231 | fw.WI(ind, "private bool ImmutableEquals(" + info.FullName + " obj) {"); 232 | ind++; 233 | fw.WI(ind, "if (ReferenceEquals(this, obj)) return true;"); 234 | fw.WI(ind, "if (ReferenceEquals(obj, null)) return false;"); 235 | if (allProps.Count == 0) { 236 | fw.WI(ind, "return true;"); 237 | } 238 | else { 239 | fw.WI(ind, "if (ImmutableGetHashCode() != obj.ImmutableGetHashCode()) return false;"); 240 | fw.WI(ind, "return " + string.Join(" && ", allProps.Select(p => CustomNamespace + ".Helpers.AreEqual(this." + p.Name + ", obj." + p.Name + ")")) + ";"); 241 | } 242 | ind--; 243 | fw.WI(ind, "}"); 244 | fw.WI(ind, ""); 245 | 246 | if (enableEquals) { 247 | fw.WIGenerated(ind); 248 | fw.WI(ind, "public override bool Equals(object obj) {"); 249 | ind++; 250 | fw.WI(ind, "return ImmutableEquals(obj as " + info.FullName + ");"); 251 | ind--; 252 | fw.WI(ind, "}"); 253 | fw.WI(ind, ""); 254 | 255 | fw.WIGenerated(ind); 256 | fw.WI(ind, "public bool Equals(" + info.FullName + " obj) {"); 257 | ind++; 258 | fw.WI(ind, "return ImmutableEquals(obj);"); 259 | ind--; 260 | fw.WI(ind, "}"); 261 | fw.WI(ind, ""); 262 | } 263 | } 264 | 265 | // operator==, operator!= 266 | if (enableOperatorEquals) { 267 | fw.WIGenerated(ind); 268 | fw.WI(ind, "public static bool operator ==(" + info.FullName + " a, " + info.FullName + " b) {"); 269 | ind++; 270 | fw.WI(ind, "return T4Immutable.Helpers.BasicAreEqual(a, b);"); 271 | ind--; 272 | fw.WI(ind, "}"); 273 | fw.WI(ind, ""); 274 | 275 | fw.WIGenerated(ind); 276 | fw.WI(ind, "public static bool operator !=(" + info.FullName + " a, " + info.FullName + " b) {"); 277 | ind++; 278 | fw.WI(ind, "return !T4Immutable.Helpers.BasicAreEqual(a, b);"); 279 | ind--; 280 | fw.WI(ind, "}"); 281 | fw.WI(ind, ""); 282 | } 283 | 284 | // GetHashCode 285 | { 286 | fw.WIGenerated(ind, false); 287 | fw.WI(ind, "private readonly int _ImmutableHashCode;"); 288 | fw.WI(ind, ""); 289 | fw.WIGenerated(ind); 290 | fw.WI(ind, "private int ImmutableGetHashCode() {"); 291 | ind++; 292 | fw.WI(ind, "return _ImmutableHashCode;"); 293 | ind--; 294 | fw.WI(ind, "}"); 295 | fw.WI(ind, ""); 296 | 297 | if (enableGetHashCode) { 298 | fw.WIGenerated(ind); 299 | fw.WI(ind, "public override int GetHashCode() {"); 300 | ind++; 301 | fw.WI(ind, "return ImmutableGetHashCode();"); 302 | ind--; 303 | fw.WI(ind, "}"); 304 | fw.WI(ind, ""); 305 | } 306 | } 307 | 308 | // ToString 309 | { 310 | fw.WIGenerated(ind); 311 | fw.WI(ind, "private string ImmutableToString() {"); 312 | ind++; 313 | fw.WI(ind, "return T4Immutable.Helpers.ToStringFor(nameof(" + info.Name + info.GenericString + "), " + string.Join(", ", allProps.Select(p => "new System.Tuple(nameof(this." + p.Name + "), this." + p.Name + ")")) + ");") ; 314 | ind--; 315 | fw.WI(ind, "}"); 316 | fw.WI(ind, ""); 317 | 318 | if (enableToString) { 319 | fw.WIGenerated(ind); 320 | fw.WI(ind, "public override string ToString() {"); 321 | ind++; 322 | fw.WI(ind, "return ImmutableToString();"); 323 | ind--; 324 | fw.WI(ind, "}"); 325 | fw.WI(ind, ""); 326 | } 327 | } 328 | 329 | // With 330 | { 331 | fw.WIGenerated(ind); 332 | fw.WI(ind, "private " + info.FullName + " ImmutableWith(" + string.Join(", ", allProps.Select(p => p.ToFullOptParam(true, true))) + ") {"); 333 | ind++; 334 | fw.WI(ind, "return new " + info.FullName + "("); 335 | ind++; 336 | int i = 0; 337 | foreach (var p in allProps) { 338 | bool lastOne = i == allProps.Count - 1; 339 | fw.WI(ind, p.ParamName + ".HasValue ? " + p.ParamName + ".Value : this." + p.Name + (lastOne ? "" : ",")); 340 | i++; 341 | } 342 | ind--; 343 | fw.WI(ind, ");"); 344 | ind--; 345 | fw.WI(ind, "}"); 346 | fw.WI(ind, ""); 347 | 348 | if (enableWith) { 349 | fw.WIGenerated(ind); 350 | fw.WI(ind, "public " + info.FullName + " With(" + string.Join(", ", allProps.Select(p => p.ToFullOptParam(true, true))) + ") {"); 351 | ind++; 352 | fw.WI(ind, "return ImmutableWith(" + string.Join(", ", allProps.Select(p => p.ParamName)) + ");"); 353 | ind--; 354 | fw.WI(ind, "}"); 355 | fw.WI(ind, ""); 356 | } 357 | } 358 | 359 | // ToBuilder 360 | if (enableToBuilder) { 361 | fw.WIGenerated(ind); 362 | fw.WI(ind, "public " + info.FullName + ".Builder ToBuilder() {"); 363 | ind++; 364 | fw.WI(ind, "return ImmutableToBuilder();"); 365 | ind--; 366 | fw.WI(ind, "}"); 367 | fw.WI(ind, ""); 368 | } 369 | 370 | // Builder 371 | if (enableBuilder) { 372 | fw.WIGenerated(ind); 373 | fw.WI(ind, "private " + info.FullName + ".Builder ImmutableToBuilder() {"); 374 | ind++; 375 | fw.WI(ind, "return new " + info.FullName + ".Builder().With("); 376 | ind++; 377 | int i = 0; 378 | foreach (var p in allProps) { 379 | bool lastOne = i == allProps.Count - 1; 380 | fw.WI(ind, "new " + p.ToOptParam() + "(this." + p.Name + ")" + (lastOne ? "" : ",")); 381 | i++; 382 | } 383 | ind--; 384 | fw.WI(ind, ");"); 385 | ind--; 386 | fw.WI(ind, "}"); 387 | fw.WI(ind, ""); 388 | 389 | fw.WIGenerated(ind); 390 | fw.WI(ind, builderAccessLevel + " class Builder {"); 391 | ind++; 392 | 393 | // properties 394 | foreach (var p in allProps) { 395 | fw.WIGenerated(ind); 396 | fw.WI(ind, "public " + p.ToFullOptParam(false, false) + " { get; set; }"); 397 | } 398 | fw.WI(ind, ""); 399 | 400 | // with method 401 | fw.WIGenerated(ind); 402 | fw.WI(ind, "public Builder With(" + string.Join(", ", allProps.Select(p => p.ToFullOptParam(true, true))) + ") {"); 403 | ind++; 404 | foreach (var p in allProps) { 405 | fw.WI(ind, "if (" + p.ParamName + ".HasValue) this." + p.Name + " = " + p.ParamName + ";"); 406 | } 407 | fw.WI(ind, "return this;"); 408 | ind--; 409 | fw.WI(ind, "}"); 410 | fw.WI(ind, ""); 411 | 412 | // build method 413 | fw.WIGenerated(ind); 414 | fw.WI(ind, "public " + info.FullName + " Build() {"); 415 | ind++; 416 | // set defaults and check values 417 | foreach (var p in allProps) { 418 | if (p.DefaultValue != null) { 419 | fw.WI(ind, "if (!this." + p.Name + ".HasValue) this." + p.Name + " = " + p.DefaultValue.InitExpressionString + ";"); 420 | } 421 | fw.WI(ind, "if (!this." + p.Name + ".HasValue) throw new InvalidOperationException(\"Builder property '" + p.Name + "' cannot be left unassigned\");"); 422 | } 423 | fw.WI(ind, "return new " + info.FullName + "(" + string.Join(", ", allProps.Select(p => "this." + p.Name + ".Value")) + ");"); 424 | ind--; 425 | fw.WI(ind, "}"); 426 | 427 | ind--; 428 | fw.WI(ind, "}"); 429 | fw.WI(ind, ""); 430 | } 431 | 432 | // end of type header 433 | { 434 | ind--; 435 | fw.WI(ind, "}"); 436 | } 437 | 438 | info.MarkAsPartial(); 439 | 440 | // end of parent classes / structs 441 | foreach (var p in parents) { 442 | var currentClass = p as ClassInfo; 443 | var currentStruct = p as StructInfo; 444 | ind--; 445 | fw.WI(ind, "}"); 446 | 447 | if (currentClass != null) { 448 | currentClass.MarkAsPartial(); 449 | } 450 | else { 451 | currentStruct.MarkAsPartial(); 452 | } 453 | } 454 | 455 | // end of namespace 456 | if (hasNamesp) { 457 | ind--; 458 | fw.WI(ind, "}"); 459 | } 460 | 461 | fw.WI(ind, ""); 462 | } 463 | #> -------------------------------------------------------------------------------- /src/content/T4Immutable/TemplateFileManagerV2.1.ttinclude: -------------------------------------------------------------------------------- 1 | <#@ assembly name="System.Core" #> 2 | <#@ assembly name="System.Data" #> 3 | <#@ assembly name="System.Data.Entity" #> 4 | <#@ assembly name="System.Xml" #> 5 | <#@ assembly name="System.Xml.Linq"#> 6 | <#@ assembly name="System.Windows.Forms" #> 7 | <#@ assembly name="EnvDTE"#> 8 | <#@ assembly name="EnvDTE80" #> 9 | <#@ assembly name="Microsoft.VisualStudio.Shell.10.0"#> 10 | <#@ assembly name="Microsoft.VisualStudio.Shell.Interop "#> 11 | <#@ import namespace="Microsoft.VisualStudio.Shell.Interop"#> 12 | <#@ import namespace="System" #> 13 | <#@ import namespace="System.Data" #> 14 | <#@ import namespace="System.Data.Objects" #> 15 | <#@ import namespace="System.Linq" #> 16 | <#@ import namespace="System.IO" #> 17 | <#@ import namespace="System.Collections.Generic" #> 18 | <#@ import namespace="System.Data.Objects.DataClasses" #> 19 | <#@ import namespace="System.Text.RegularExpressions" #> 20 | <#@ import namespace="System.Xml" #> 21 | <#@ import namespace="System.Xml.Linq" #> 22 | <#@ import namespace="System.Globalization" #> 23 | <#@ import namespace="System.Reflection" #> 24 | <#@ import namespace="System.CodeDom" #> 25 | <#@ import namespace="System.CodeDom.Compiler" #> 26 | <#@ import namespace="Microsoft.CSharp"#> 27 | <#@ import namespace="System.Text"#> 28 | <#@ import namespace="EnvDTE" #> 29 | <#@ import namespace="Microsoft.VisualStudio.TextTemplating" #> 30 | <#+ 31 | /* 32 | This software is supplied "AS IS". The authors disclaim all warranties, 33 | expressed or implied, including, without limitation, the warranties of 34 | merchantability and of fitness for any purpose. The authors assume no 35 | liability for direct, indirect, incidental, special, exemplary, or 36 | consequential damages, which may result from the use of this software, 37 | even if advised of the possibility of such damage. 38 | 39 | The TemplateFileManager is based on EntityFrameworkTemplateFileManager (EFTFM) from MS. 40 | 41 | Differences to EFTFM 42 | Version 2.1: 43 | - Replace Enum BuildAction with class for more flexibility 44 | Version 2: 45 | - StartHeader works with Parameter $filename$ 46 | - StartNewFile has a new named parameter FileProperties 47 | - Support for: 48 | - BuildAction 49 | - CustomTool 50 | - user defined parameter for using in StartHeader-Block 51 | - Property IsAutoIndentEnabled for support Format Document (C#, VB) when set to true 52 | 53 | Version: 1.1 54 | Add method WriteLineToBuildPane, WriteToBuildPane 55 | 56 | Version 1: 57 | - StartNewFile with named parameters projectName and folderName for generating files to different locations. 58 | - Property CanOverrideExistingFile, to define whether existing files are can overwritten 59 | - Property Encoding Encode type for output files. 60 | */ 61 | 62 | /// 63 | /// Writes a line to the build pane in visual studio and activates it 64 | /// 65 | /// Text to output - a \n is appended 66 | void WriteLineToBuildPane (string message){ 67 | WriteLineToBuildPane(String.Format("{0}\n", message)); 68 | } 69 | 70 | /// 71 | /// Writes a string to the build pane in visual studio and activates it 72 | /// 73 | /// Text to output 74 | void WriteToBuildPane (string message){ 75 | IVsOutputWindow outWindow = (this.Host as IServiceProvider).GetService( 76 | typeof( SVsOutputWindow ) ) as IVsOutputWindow; 77 | Guid generalPaneGuid = 78 | Microsoft.VisualStudio.VSConstants.OutputWindowPaneGuid.BuildOutputPane_guid; 79 | // P.S. There's also the GUID_OutWindowDebugPane available. 80 | IVsOutputWindowPane generalPane; 81 | outWindow.GetPane( ref generalPaneGuid , out generalPane ); 82 | generalPane.OutputString( message ); 83 | generalPane.Activate(); // Brings this pane into view 84 | } 85 | 86 | /// 87 | /// Responsible for marking the various sections of the generation, 88 | /// so they can be split up into separate files and projects 89 | /// 90 | /// R. Leupold 91 | public class TemplateFileManager 92 | { 93 | private string outputFolder; // CHANGED 94 | private EnvDTE.ProjectItem templateProjectItem; 95 | private Action checkOutAction; 96 | private Action> projectSyncAction; 97 | private EnvDTE.DTE dte; 98 | private List templatePlaceholderList = new List(); 99 | 100 | /// 101 | /// Creates files with VS sync 102 | /// 103 | public static TemplateFileManager Create(object textTransformation, string outputFolder) // CHANGED 104 | { 105 | DynamicTextTransformation2 transformation = DynamicTextTransformation2.Create(textTransformation); 106 | IDynamicHost2 host = transformation.Host; 107 | var instance = new TemplateFileManager(transformation); 108 | instance.outputFolder = outputFolder; // CHANGED 109 | return instance; 110 | } 111 | 112 | private readonly List files = new List(); 113 | private readonly Block footer = new Block(); 114 | private readonly Block header = new Block(); 115 | private readonly DynamicTextTransformation2 _textTransformation; 116 | 117 | // reference to the GenerationEnvironment StringBuilder on the 118 | // TextTransformation object 119 | private readonly StringBuilder _generationEnvironment; 120 | 121 | private Block currentBlock; 122 | 123 | /// 124 | /// Initializes an TemplateFileManager Instance with the 125 | /// TextTransformation (T4 generated class) that is currently running 126 | /// 127 | private TemplateFileManager(object textTransformation) 128 | { 129 | if (textTransformation == null) 130 | { 131 | throw new ArgumentNullException("textTransformation"); 132 | } 133 | 134 | _textTransformation = DynamicTextTransformation2.Create(textTransformation); 135 | _generationEnvironment = _textTransformation.GenerationEnvironment; 136 | 137 | var hostServiceProvider = _textTransformation.Host.AsIServiceProvider(); 138 | if (hostServiceProvider == null) 139 | { 140 | throw new ArgumentNullException("Could not obtain hostServiceProvider"); 141 | } 142 | 143 | dte = (EnvDTE.DTE) hostServiceProvider.GetService(typeof(EnvDTE.DTE)); 144 | if (dte == null) 145 | { 146 | throw new ArgumentNullException("Could not obtain DTE from host"); 147 | } 148 | 149 | this.templateProjectItem = dte.Solution.FindProjectItem(_textTransformation.Host.TemplateFile); 150 | this.CanOverrideExistingFile = true; 151 | this.IsAutoIndentEnabled = false; 152 | this.Encoding = System.Text.Encoding.UTF8; 153 | checkOutAction = fileName => dte.SourceControl.CheckOutItem(fileName); 154 | projectSyncAction = keepFileNames => ProjectSync(templateProjectItem, keepFileNames); 155 | } 156 | 157 | /// 158 | /// If set to false, existing files are not overwritten 159 | /// 160 | /// 161 | public bool CanOverrideExistingFile { get; set; } 162 | 163 | /// 164 | /// If set to true, output files (c#, vb) are formatted based on the vs settings. 165 | /// 166 | /// 167 | public bool IsAutoIndentEnabled { get; set; } 168 | 169 | /// 170 | /// Defines Encoding format for generated output file. (Default UTF8) 171 | /// 172 | /// 173 | public System.Text.Encoding Encoding { get; set; } 174 | 175 | /// 176 | /// Marks the end of the last file if there was one, and starts a new 177 | /// and marks this point in generation as a new file. 178 | /// 179 | /// Filename 180 | /// Name of the target project for the new file. 181 | /// Name of the target folder for the new file. 182 | /// File property settings in vs for the new File 183 | public void StartNewFile(string name 184 | , string projectName = "", string folderName = "", FileProperties fileProperties = null) 185 | { 186 | if (String.IsNullOrWhiteSpace(name) == true) 187 | { 188 | throw new ArgumentException("name"); 189 | } 190 | 191 | CurrentBlock = new Block 192 | { 193 | Name = name, 194 | ProjectName = projectName, 195 | FolderName = folderName, 196 | FileProperties = fileProperties ?? new FileProperties() 197 | }; 198 | } 199 | 200 | public void StartFooter() 201 | { 202 | CurrentBlock = footer; 203 | } 204 | 205 | public void StartHeader() 206 | { 207 | CurrentBlock = header; 208 | } 209 | 210 | public void EndBlock() 211 | { 212 | if (CurrentBlock == null) 213 | { 214 | return; 215 | } 216 | 217 | CurrentBlock.Length = _generationEnvironment.Length - CurrentBlock.Start; 218 | 219 | if (CurrentBlock != header && CurrentBlock != footer) 220 | { 221 | files.Add(CurrentBlock); 222 | } 223 | 224 | currentBlock = null; 225 | } 226 | 227 | /// 228 | /// Produce the template output files. 229 | /// 230 | public virtual IEnumerable Process(bool split = true) 231 | { 232 | var list = new List(); 233 | 234 | if (split) 235 | { 236 | EndBlock(); 237 | 238 | var headerText = _generationEnvironment.ToString(header.Start, header.Length); 239 | var footerText = _generationEnvironment.ToString(footer.Start, footer.Length); 240 | files.Reverse(); 241 | 242 | foreach (var block in files) 243 | { 244 | var outputPath = VSHelper.GetOutputPath(dte, block, outputFolder != null ? outputFolder : Path.GetDirectoryName(_textTransformation.Host.TemplateFile)); // CHANGED 245 | var fileName = Path.Combine(outputPath, block.Name); 246 | var content = this.ReplaceParameter(headerText, block) + 247 | _generationEnvironment.ToString(block.Start, block.Length) + 248 | footerText; 249 | 250 | var file = new OutputFile 251 | { 252 | FileName = fileName, 253 | ProjectName = block.ProjectName, 254 | FolderName = block.FolderName, 255 | FileProperties = block.FileProperties, 256 | Content = content 257 | }; 258 | 259 | CreateFile(file); 260 | _generationEnvironment.Remove(block.Start, block.Length); 261 | 262 | list.Add(file); 263 | } 264 | } 265 | 266 | // fix for .net core projects 267 | //projectSyncAction.EndInvoke(projectSyncAction.BeginInvoke(list, null, null)); 268 | projectSyncAction(list); 269 | 270 | this.CleanUpTemplatePlaceholders(); 271 | var items = VSHelper.GetOutputFilesAsProjectItems(this.dte, list); 272 | this.WriteVsProperties(items, list); 273 | 274 | if (this.IsAutoIndentEnabled == true && split == true) 275 | { 276 | this.FormatProjectItems(items); 277 | } 278 | 279 | this.WriteLog(list); 280 | 281 | return list; 282 | } 283 | 284 | private void FormatProjectItems(IEnumerable items) 285 | { 286 | foreach (var item in items) 287 | { 288 | this._textTransformation.WriteLine( 289 | VSHelper.ExecuteVsCommand(this.dte, item, "Edit.FormatDocument")); //, "Edit.RemoveAndSort")); 290 | this._textTransformation.WriteLine("//-> " + item.Name); 291 | } 292 | } 293 | 294 | private void WriteVsProperties(IEnumerable items, IEnumerable outputFiles) 295 | { 296 | foreach (var file in outputFiles) 297 | { 298 | var item = items.Where(p => p.Name == Path.GetFileName(file.FileName)).FirstOrDefault(); 299 | if (item == null) continue; 300 | 301 | if (String.IsNullOrEmpty(file.FileProperties.CustomTool) == false) 302 | { 303 | VSHelper.SetPropertyValue(item, "CustomTool", file.FileProperties.CustomTool); 304 | } 305 | 306 | if (String.IsNullOrEmpty(file.FileProperties.BuildActionString) == false) 307 | { 308 | VSHelper.SetPropertyValue(item, "ItemType", file.FileProperties.BuildActionString); 309 | } 310 | } 311 | } 312 | 313 | private string ReplaceParameter(string text, Block block) 314 | { 315 | if (String.IsNullOrEmpty(text) == false) 316 | { 317 | text = text.Replace("$filename$", block.Name); 318 | } 319 | 320 | 321 | foreach (var item in block.FileProperties.TemplateParameter.AsEnumerable()) 322 | { 323 | text = text.Replace(item.Key, item.Value); 324 | } 325 | 326 | return text; 327 | } 328 | 329 | /// 330 | /// Write log to the default output file. 331 | /// 332 | /// 333 | private void WriteLog(IEnumerable list) 334 | { 335 | this._textTransformation.WriteLine("// Generated helper templates"); 336 | foreach (var item in templatePlaceholderList) 337 | { 338 | this._textTransformation.WriteLine("// " + this.GetDirectorySolutionRelative(item)); 339 | } 340 | 341 | this._textTransformation.WriteLine("// Generated items"); 342 | foreach (var item in list) 343 | { 344 | this._textTransformation.WriteLine("// " + this.GetDirectorySolutionRelative(item.FileName)); 345 | } 346 | } 347 | 348 | /// 349 | /// Removes old template placeholders from the solution. 350 | /// 351 | private void CleanUpTemplatePlaceholders() 352 | { 353 | string[] activeTemplateFullNames = this.templatePlaceholderList.ToArray(); 354 | string[] allHelperTemplateFullNames = VSHelper.GetAllSolutionItems(this.dte) 355 | .Where(p => p.Name == VSHelper.GetTemplatePlaceholderName(this.templateProjectItem)) 356 | .Select(p => VSHelper.GetProjectItemFullPath(p)) 357 | .ToArray(); 358 | 359 | var delta = allHelperTemplateFullNames.Except(activeTemplateFullNames).ToArray(); 360 | 361 | var dirtyHelperTemplates = VSHelper.GetAllSolutionItems(this.dte) 362 | .Where(p => delta.Contains(VSHelper.GetProjectItemFullPath(p))); 363 | 364 | foreach (ProjectItem item in dirtyHelperTemplates) 365 | { 366 | if (item.ProjectItems != null) 367 | { 368 | foreach (ProjectItem subItem in item.ProjectItems) 369 | { 370 | subItem.Remove(); 371 | } 372 | } 373 | 374 | item.Remove(); 375 | } 376 | } 377 | 378 | /// 379 | /// Gets a list of helper templates from the log. 380 | /// 381 | /// List of generated helper templates. 382 | private string[] GetPreviousTemplatePlaceholdersFromLog() 383 | { 384 | string path = Path.GetDirectoryName(this._textTransformation.Host.ResolvePath(this._textTransformation.Host.TemplateFile)); 385 | string file1 = Path.GetFileNameWithoutExtension(this._textTransformation.Host.TemplateFile) + ".txt"; 386 | string contentPrevious = File.ReadAllText(Path.Combine(path, file1)); 387 | 388 | var result = contentPrevious 389 | .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) 390 | .Select(x => x.Split(new[] { "=>" }, StringSplitOptions.RemoveEmptyEntries).First()) 391 | .Select(x => Regex.Replace(x, "//", String.Empty).Trim()) 392 | .Where(x => x.EndsWith(VSHelper.GetTemplatePlaceholderName(this.templateProjectItem))) 393 | .ToArray(); 394 | 395 | return result; 396 | } 397 | 398 | private string GetDirectorySolutionRelative(string fullName) 399 | { 400 | int slnPos = fullName.IndexOf(Path.GetFileNameWithoutExtension(this.dte.Solution.FileName)); 401 | if (slnPos < 0) 402 | { 403 | slnPos = 0; 404 | } 405 | 406 | return fullName.Substring(slnPos); 407 | } 408 | 409 | protected virtual void CreateFile(OutputFile file) 410 | { 411 | if (this.CanOverrideExistingFile == false && File.Exists(file.FileName) == true) 412 | { 413 | return; 414 | } 415 | 416 | if (IsFileContentDifferent(file)) 417 | { 418 | CheckoutFileIfRequired(file.FileName); 419 | File.WriteAllText(file.FileName, file.Content, this.Encoding); 420 | } 421 | } 422 | 423 | protected bool IsFileContentDifferent(OutputFile file) 424 | { 425 | return !(File.Exists(file.FileName) && File.ReadAllText(file.FileName) == file.Content); 426 | } 427 | 428 | private Block CurrentBlock 429 | { 430 | get { return currentBlock; } 431 | set 432 | { 433 | if (CurrentBlock != null) 434 | { 435 | EndBlock(); 436 | } 437 | 438 | if (value != null) 439 | { 440 | value.Start = _generationEnvironment.Length; 441 | } 442 | 443 | currentBlock = value; 444 | } 445 | } 446 | 447 | private void ProjectSync(EnvDTE.ProjectItem templateProjectItem, IEnumerable keepFileNames) 448 | { 449 | var groupedFileNames = from f in keepFileNames 450 | group f by new { f.ProjectName, f.FolderName } 451 | into l 452 | select new { 453 | ProjectName = l.Key.ProjectName, 454 | FolderName = l.Key.FolderName, 455 | FirstItem = l.First(), 456 | OutputFiles = l 457 | }; 458 | 459 | this.templatePlaceholderList.Clear(); 460 | 461 | foreach (var item in groupedFileNames) 462 | { 463 | EnvDTE.ProjectItem pi = VSHelper.GetTemplateProjectItem(templateProjectItem.DTE, item.FirstItem, templateProjectItem); 464 | ProjectSyncPart(pi, item.OutputFiles); 465 | 466 | if (pi.Name.EndsWith("txt4")) 467 | this.templatePlaceholderList.Add(VSHelper.GetProjectItemFullPath(pi)); 468 | } 469 | 470 | // clean up 471 | bool hasDefaultItems = groupedFileNames.Where(f => String.IsNullOrEmpty(f.ProjectName) && String.IsNullOrEmpty(f.FolderName)).Count() > 0; 472 | if (hasDefaultItems == false) 473 | { 474 | ProjectSyncPart(templateProjectItem, new List()); 475 | } 476 | } 477 | 478 | private static void ProjectSyncPart(EnvDTE.ProjectItem templateProjectItem, IEnumerable keepFileNames) 479 | { 480 | var keepFileNameSet = new HashSet(keepFileNames); 481 | var projectFiles = new Dictionary(); 482 | var originalOutput = Path.GetFileNameWithoutExtension(templateProjectItem.FileNames[0]); 483 | 484 | foreach (EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems) 485 | { 486 | projectFiles.Add(projectItem.FileNames[0], projectItem); 487 | } 488 | 489 | // Remove unused items from the project 490 | foreach (var pair in projectFiles) 491 | { 492 | bool isNotFound = keepFileNames.Where(f=>f.FileName == pair.Key).Count() == 0; 493 | if (isNotFound == true 494 | && !(Path.GetFileNameWithoutExtension(pair.Key) + ".").StartsWith(originalOutput + ".")) 495 | { 496 | pair.Value.Delete(); 497 | } 498 | } 499 | 500 | // Add missing files to the project 501 | foreach (var fileName in keepFileNameSet) 502 | { 503 | if (!projectFiles.ContainsKey(fileName.FileName)) 504 | { 505 | // CHANGED TO IGNORE CERTAIN EXCEPTIONS 506 | try { 507 | templateProjectItem.ProjectItems.AddFromFile(fileName.FileName); 508 | } 509 | catch (System.NotSupportedException) { 510 | // do nothing, probably on a .net core project 511 | } 512 | } 513 | } 514 | } 515 | 516 | private void CheckoutFileIfRequired(string fileName) 517 | { 518 | if (dte.SourceControl == null 519 | || !dte.SourceControl.IsItemUnderSCC(fileName) 520 | || dte.SourceControl.IsItemCheckedOut(fileName)) 521 | { 522 | return; 523 | } 524 | 525 | // run on worker thread to prevent T4 calling back into VS 526 | checkOutAction.EndInvoke(checkOutAction.BeginInvoke(fileName, null, null)); 527 | } 528 | } 529 | 530 | /// 531 | /// Responsible creating an instance that can be passed 532 | /// to helper classes that need to access the TextTransformation 533 | /// members. It accesses member by name and signature rather than 534 | /// by type. This is necessary when the 535 | /// template is being used in Preprocessed mode 536 | /// and there is no common known type that can be 537 | /// passed instead 538 | /// 539 | public class DynamicTextTransformation2 540 | { 541 | private object _instance; 542 | IDynamicHost2 _dynamicHost; 543 | 544 | private readonly MethodInfo _write; 545 | private readonly MethodInfo _writeLine; 546 | private readonly PropertyInfo _generationEnvironment; 547 | private readonly PropertyInfo _errors; 548 | private readonly PropertyInfo _host; 549 | 550 | /// 551 | /// Creates an instance of the DynamicTextTransformation class around the passed in 552 | /// TextTransformation shapped instance passed in, or if the passed in instance 553 | /// already is a DynamicTextTransformation, it casts it and sends it back. 554 | /// 555 | public static DynamicTextTransformation2 Create(object instance) 556 | { 557 | if (instance == null) 558 | { 559 | throw new ArgumentNullException("instance"); 560 | } 561 | 562 | DynamicTextTransformation2 textTransformation = instance as DynamicTextTransformation2; 563 | if (textTransformation != null) 564 | { 565 | return textTransformation; 566 | } 567 | 568 | return new DynamicTextTransformation2(instance); 569 | } 570 | 571 | private DynamicTextTransformation2(object instance) 572 | { 573 | _instance = instance; 574 | Type type = _instance.GetType(); 575 | _write = type.GetMethod("Write", new Type[] { typeof(string) }); 576 | _writeLine = type.GetMethod("WriteLine", new Type[] { typeof(string) }); 577 | _generationEnvironment = type.GetProperty("GenerationEnvironment", BindingFlags.Instance | BindingFlags.NonPublic); 578 | _host = type.GetProperty("Host"); 579 | _errors = type.GetProperty("Errors"); 580 | } 581 | 582 | /// 583 | /// Gets the value of the wrapped TextTranformation instance's GenerationEnvironment property 584 | /// 585 | public StringBuilder GenerationEnvironment { get { return (StringBuilder)_generationEnvironment.GetValue(_instance, null); } } 586 | 587 | /// 588 | /// Gets the value of the wrapped TextTranformation instance's Errors property 589 | /// 590 | public System.CodeDom.Compiler.CompilerErrorCollection Errors { get { return (System.CodeDom.Compiler.CompilerErrorCollection)_errors.GetValue(_instance, null); } } 591 | 592 | /// 593 | /// Calls the wrapped TextTranformation instance's Write method. 594 | /// 595 | public void Write(string text) 596 | { 597 | _write.Invoke(_instance, new object[] { text }); 598 | } 599 | 600 | /// 601 | /// Calls the wrapped TextTranformation instance's WriteLine method. 602 | /// 603 | public void WriteLine(string text) 604 | { 605 | _writeLine.Invoke(_instance, new object[] { text }); 606 | } 607 | 608 | /// 609 | /// Gets the value of the wrapped TextTranformation instance's Host property 610 | /// if available (shows up when hostspecific is set to true in the template directive) and returns 611 | /// the appropriate implementation of IDynamicHost 612 | /// 613 | public IDynamicHost2 Host 614 | { 615 | get 616 | { 617 | if (_dynamicHost == null) 618 | { 619 | if(_host == null) 620 | { 621 | _dynamicHost = new NullHost2(); 622 | } 623 | else 624 | { 625 | _dynamicHost = new DynamicHost2(_host.GetValue(_instance, null)); 626 | } 627 | } 628 | return _dynamicHost; 629 | } 630 | } 631 | } 632 | 633 | /// 634 | /// Reponsible for abstracting the use of Host between times 635 | /// when it is available and not 636 | /// 637 | public interface IDynamicHost2 638 | { 639 | /// 640 | /// An abstracted call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost ResolveParameterValue 641 | /// 642 | string ResolveParameterValue(string id, string name, string otherName); 643 | 644 | /// 645 | /// An abstracted call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost ResolvePath 646 | /// 647 | string ResolvePath(string path); 648 | 649 | /// 650 | /// An abstracted call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost TemplateFile 651 | /// 652 | string TemplateFile { get; } 653 | 654 | /// 655 | /// Returns the Host instance cast as an IServiceProvider 656 | /// 657 | IServiceProvider AsIServiceProvider(); 658 | } 659 | 660 | /// 661 | /// Reponsible for implementing the IDynamicHost as a dynamic 662 | /// shape wrapper over the Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost interface 663 | /// rather than type dependent wrapper. We don't use the 664 | /// interface type so that the code can be run in preprocessed mode 665 | /// on a .net framework only installed machine. 666 | /// 667 | public class DynamicHost2 : IDynamicHost2 668 | { 669 | private readonly object _instance; 670 | private readonly MethodInfo _resolveParameterValue; 671 | private readonly MethodInfo _resolvePath; 672 | private readonly PropertyInfo _templateFile; 673 | 674 | /// 675 | /// Creates an instance of the DynamicHost class around the passed in 676 | /// Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost shapped instance passed in. 677 | /// 678 | public DynamicHost2(object instance) 679 | { 680 | _instance = instance; 681 | Type type = _instance.GetType(); 682 | _resolveParameterValue = type.GetMethod("ResolveParameterValue", new Type[] { typeof(string), typeof(string), typeof(string) }); 683 | _resolvePath = type.GetMethod("ResolvePath", new Type[] { typeof(string) }); 684 | _templateFile = type.GetProperty("TemplateFile"); 685 | 686 | } 687 | 688 | /// 689 | /// A call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost ResolveParameterValue 690 | /// 691 | public string ResolveParameterValue(string id, string name, string otherName) 692 | { 693 | return (string)_resolveParameterValue.Invoke(_instance, new object[] { id, name, otherName }); 694 | } 695 | 696 | /// 697 | /// A call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost ResolvePath 698 | /// 699 | public string ResolvePath(string path) 700 | { 701 | return (string)_resolvePath.Invoke(_instance, new object[] { path }); 702 | } 703 | 704 | /// 705 | /// A call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost TemplateFile 706 | /// 707 | public string TemplateFile 708 | { 709 | get 710 | { 711 | return (string)_templateFile.GetValue(_instance, null); 712 | } 713 | } 714 | 715 | /// 716 | /// Returns the Host instance cast as an IServiceProvider 717 | /// 718 | public IServiceProvider AsIServiceProvider() 719 | { 720 | return _instance as IServiceProvider; 721 | } 722 | } 723 | 724 | /// 725 | /// Reponsible for implementing the IDynamicHost when the 726 | /// Host property is not available on the TextTemplating type. The Host 727 | /// property only exists when the hostspecific attribute of the template 728 | /// directive is set to true. 729 | /// 730 | public class NullHost2 : IDynamicHost2 731 | { 732 | /// 733 | /// An abstraction of the call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost ResolveParameterValue 734 | /// that simply retuns null. 735 | /// 736 | public string ResolveParameterValue(string id, string name, string otherName) 737 | { 738 | return null; 739 | } 740 | 741 | /// 742 | /// An abstraction of the call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost ResolvePath 743 | /// that simply retuns the path passed in. 744 | /// 745 | public string ResolvePath(string path) 746 | { 747 | return path; 748 | } 749 | 750 | /// 751 | /// An abstraction of the call to Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost TemplateFile 752 | /// that returns null. 753 | /// 754 | public string TemplateFile 755 | { 756 | get 757 | { 758 | return null; 759 | } 760 | } 761 | 762 | /// 763 | /// Returns null. 764 | /// 765 | public IServiceProvider AsIServiceProvider() 766 | { 767 | return null; 768 | } 769 | } 770 | 771 | public sealed class Block 772 | { 773 | public String Name; 774 | public int Start, Length; 775 | public string ProjectName { get; set; } 776 | public string FolderName { get; set; } 777 | public FileProperties FileProperties { get; set; } 778 | } 779 | 780 | public class ParamTextTemplate 781 | { 782 | private ITextTemplatingEngineHost Host { get; set; } 783 | 784 | private ParamTextTemplate(ITextTemplatingEngineHost host) 785 | { 786 | this.Host = host; 787 | } 788 | 789 | public static ParamTextTemplate Create(ITextTemplatingEngineHost host) 790 | { 791 | return new ParamTextTemplate(host); 792 | } 793 | 794 | public static TextTemplatingSession GetSessionObject() 795 | { 796 | return new TextTemplatingSession(); 797 | } 798 | 799 | public string TransformText(string templateName, TextTemplatingSession session) 800 | { 801 | return this.GetTemplateContent(templateName, session); 802 | } 803 | 804 | public string GetTemplateContent(string templateName, TextTemplatingSession session) 805 | { 806 | string fullName = this.Host.ResolvePath(templateName); 807 | string templateContent = File.ReadAllText(fullName); 808 | 809 | var sessionHost = this.Host as ITextTemplatingSessionHost; 810 | sessionHost.Session = session; 811 | 812 | Engine engine = new Engine(); 813 | return engine.ProcessTemplate(templateContent, this.Host); 814 | } 815 | } 816 | 817 | public class VSHelper 818 | { 819 | /// 820 | /// Execute Visual Studio commands against the project item. 821 | /// 822 | /// The current project item. 823 | /// The vs command as string. 824 | /// An error message if the command fails. 825 | public static string ExecuteVsCommand(EnvDTE.DTE dte, EnvDTE.ProjectItem item, params string[] command) 826 | { 827 | if (item == null) 828 | { 829 | throw new ArgumentNullException("item"); 830 | } 831 | 832 | string error = String.Empty; 833 | 834 | try 835 | { 836 | EnvDTE.Window window = item.Open(); 837 | window.Activate(); 838 | 839 | foreach (var cmd in command) 840 | { 841 | if (String.IsNullOrWhiteSpace(cmd) == true) 842 | { 843 | continue; 844 | } 845 | 846 | EnvDTE80.DTE2 dte2 = dte as EnvDTE80.DTE2; 847 | dte2.ExecuteCommand(cmd, String.Empty); 848 | } 849 | 850 | item.Save(); 851 | window.Visible = false; 852 | // window.Close(); // Ends VS, but not the tab :( 853 | } 854 | catch (Exception ex) 855 | { 856 | error = String.Format("Error processing file {0} {1}", item.Name, ex.Message); 857 | } 858 | 859 | return error; 860 | } 861 | 862 | /// 863 | /// Sets a property value for the vs project item. 864 | /// 865 | public static void SetPropertyValue(EnvDTE.ProjectItem item, string propertyName, object value) 866 | { 867 | EnvDTE.Property property = item.Properties.Item(propertyName); 868 | if (property == null) 869 | { 870 | throw new ArgumentException(String.Format("The property {0} was not found.", propertyName)); 871 | } 872 | else 873 | { 874 | property.Value = value; 875 | } 876 | } 877 | 878 | public static IEnumerable GetOutputFilesAsProjectItems(EnvDTE.DTE dte, IEnumerable outputFiles) 879 | { 880 | var fileNames = (from o in outputFiles 881 | select Path.GetFileName(o.FileName)).ToArray(); 882 | 883 | return VSHelper.GetAllSolutionItems(dte).Where(f => fileNames.Contains(f.Name)); 884 | } 885 | 886 | public static string GetOutputPath(EnvDTE.DTE dte, Block block, string defaultPath) 887 | { 888 | if (String.IsNullOrEmpty(block.ProjectName) == true && String.IsNullOrEmpty(block.FolderName) == true) 889 | { 890 | return defaultPath; 891 | } 892 | 893 | EnvDTE.Project prj = null; 894 | EnvDTE.ProjectItem item = null; 895 | 896 | if (String.IsNullOrEmpty(block.ProjectName) == false) 897 | { 898 | prj = GetProject(dte, block.ProjectName); 899 | } 900 | 901 | if (String.IsNullOrEmpty(block.FolderName) == true && prj != null) 902 | { 903 | return Path.GetDirectoryName(prj.FullName); 904 | } 905 | else if (prj != null && String.IsNullOrEmpty(block.FolderName) == false) 906 | { 907 | item = GetAllProjectItemsRecursive(prj.ProjectItems).Where(i=>i.Name == block.FolderName).First(); 908 | } 909 | else if (String.IsNullOrEmpty(block.FolderName) == false) 910 | { 911 | item = GetAllProjectItemsRecursive( 912 | dte.ActiveDocument.ProjectItem.ContainingProject.ProjectItems). 913 | Where(i=>i.Name == block.FolderName).First(); 914 | } 915 | 916 | if (item != null) 917 | { 918 | return GetProjectItemFullPath(item); 919 | } 920 | 921 | return defaultPath; 922 | } 923 | public static string GetTemplatePlaceholderName(EnvDTE.ProjectItem item) 924 | { 925 | return String.Format("{0}.txt4", Path.GetFileNameWithoutExtension(item.Name)); 926 | } 927 | 928 | public static EnvDTE.ProjectItem GetTemplateProjectItem(EnvDTE.DTE dte, OutputFile file, EnvDTE.ProjectItem defaultItem) 929 | { 930 | if (String.IsNullOrEmpty(file.ProjectName) == true && String.IsNullOrEmpty(file.FolderName) == true) 931 | { 932 | return defaultItem; 933 | } 934 | 935 | string templatePlaceholder = GetTemplatePlaceholderName(defaultItem); 936 | string itemPath = Path.GetDirectoryName(file.FileName); 937 | string fullName = Path.Combine(itemPath, templatePlaceholder); 938 | EnvDTE.Project prj = null; 939 | EnvDTE.ProjectItem item = null; 940 | 941 | if (String.IsNullOrEmpty(file.ProjectName) == false) 942 | { 943 | prj = GetProject(dte, file.ProjectName); 944 | } 945 | 946 | if (String.IsNullOrEmpty(file.FolderName) == true && prj != null) 947 | { 948 | return FindProjectItem(prj.ProjectItems, fullName, true); 949 | } 950 | else if (prj != null && String.IsNullOrEmpty(file.FolderName) == false) 951 | { 952 | item = GetAllProjectItemsRecursive(prj.ProjectItems).Where(i=>i.Name == file.FolderName).First(); 953 | } 954 | else if (String.IsNullOrEmpty(file.FolderName) == false) 955 | { 956 | item = GetAllProjectItemsRecursive( 957 | dte.ActiveDocument.ProjectItem.ContainingProject.ProjectItems). 958 | Where(i=>i.Name == file.FolderName).First(); 959 | } 960 | 961 | if (item != null) 962 | { 963 | return FindProjectItem(item.ProjectItems, fullName, true); 964 | } 965 | 966 | return defaultItem; 967 | } 968 | 969 | private static EnvDTE.ProjectItem FindProjectItem(EnvDTE.ProjectItems items, string fullName, bool canCreateIfNotExists) 970 | { 971 | EnvDTE.ProjectItem item = (from i in items.Cast() 972 | where i.Name == Path.GetFileName(fullName) 973 | select i).FirstOrDefault(); 974 | if (item == null) 975 | { 976 | File.CreateText(fullName); 977 | item = items.AddFromFile(fullName); 978 | } 979 | 980 | return item; 981 | } 982 | 983 | public static EnvDTE.Project GetProject(EnvDTE.DTE dte, string projectName) 984 | { 985 | return GetAllProjects(dte).Where(p=>p.Name == projectName).First(); 986 | } 987 | 988 | public static IEnumerable GetAllProjects(EnvDTE.DTE dte) 989 | { 990 | List projectList = new List(); 991 | 992 | var folders = dte.Solution.Projects.Cast().Where(p=>p.Kind == EnvDTE80.ProjectKinds.vsProjectKindSolutionFolder); 993 | 994 | foreach (EnvDTE.Project folder in folders) 995 | { 996 | if (folder.ProjectItems == null) continue; 997 | 998 | foreach (EnvDTE.ProjectItem item in folder.ProjectItems) 999 | { 1000 | if (item.Object is EnvDTE.Project) 1001 | projectList.Add(item.Object as EnvDTE.Project); 1002 | } 1003 | } 1004 | 1005 | var projects = dte.Solution.Projects.Cast().Where(p=>p.Kind != EnvDTE80.ProjectKinds.vsProjectKindSolutionFolder); 1006 | 1007 | if (projects.Count() > 0) 1008 | projectList.AddRange(projects); 1009 | 1010 | return projectList; 1011 | } 1012 | 1013 | public static EnvDTE.ProjectItem GetProjectItemWithName(EnvDTE.ProjectItems items, string itemName) 1014 | { 1015 | return GetAllProjectItemsRecursive(items).Cast().Where(i=>i.Name == itemName).First(); 1016 | } 1017 | 1018 | public static string GetProjectItemFullPath(EnvDTE.ProjectItem item) 1019 | { 1020 | return item.Properties.Item("FullPath").Value.ToString(); 1021 | } 1022 | 1023 | public static IEnumerable GetAllSolutionItems(EnvDTE.DTE dte) 1024 | { 1025 | List itemList = new List(); 1026 | 1027 | foreach (Project item in GetAllProjects(dte)) 1028 | { 1029 | if (item == null || item.ProjectItems == null) continue; 1030 | 1031 | itemList.AddRange(GetAllProjectItemsRecursive(item.ProjectItems)); 1032 | } 1033 | 1034 | return itemList; 1035 | } 1036 | 1037 | public static IEnumerable GetAllProjectItemsRecursive(EnvDTE.ProjectItems projectItems) 1038 | { 1039 | foreach (EnvDTE.ProjectItem projectItem in projectItems) 1040 | { 1041 | if (projectItem.ProjectItems == null) continue; 1042 | 1043 | foreach (EnvDTE.ProjectItem subItem in GetAllProjectItemsRecursive(projectItem.ProjectItems)) 1044 | { 1045 | yield return subItem; 1046 | } 1047 | 1048 | 1049 | yield return projectItem; 1050 | } 1051 | } 1052 | } 1053 | 1054 | public sealed class OutputFile 1055 | { 1056 | public OutputFile() 1057 | { 1058 | this.FileProperties = new FileProperties 1059 | { 1060 | CustomTool = String.Empty, 1061 | BuildAction = BuildAction.None 1062 | }; 1063 | } 1064 | 1065 | public string FileName { get; set; } 1066 | public string ProjectName { get; set; } 1067 | public string FolderName { get; set; } 1068 | public string Content { get; set; } 1069 | public FileProperties FileProperties { get; set; } 1070 | } 1071 | 1072 | public class BuildAction 1073 | { 1074 | public const string None = "None"; 1075 | public const string Compile = "Compile"; 1076 | public const string Content = "Content"; 1077 | public const string EmbeddedResource = "EmbeddedResource"; 1078 | public const string EntityDeploy = "EntityDeploy"; 1079 | } 1080 | 1081 | public sealed class FileProperties 1082 | { 1083 | public FileProperties () 1084 | { 1085 | this.TemplateParameter = new Dictionary(); 1086 | } 1087 | 1088 | public string CustomTool { get; set; } 1089 | public string BuildAction { get; set; } 1090 | public Dictionary TemplateParameter { get; set; } 1091 | 1092 | internal string BuildActionString 1093 | { 1094 | get 1095 | { 1096 | return this.BuildAction; 1097 | } 1098 | } 1099 | } 1100 | 1101 | 1102 | #> -------------------------------------------------------------------------------- /src/content/T4Immutable/VisualStudioHelper.ttinclude: -------------------------------------------------------------------------------- 1 | <#@ assembly name="System.Core" #> 2 | <#@ assembly name="EnvDTE"#> 3 | <#@ assembly name="EnvDTE80" #> 4 | <#@ import namespace="System" #> 5 | <#@ import namespace="System.Collections.Generic" #> 6 | <#@ import namespace="System.Linq" #> 7 | <#@ import namespace="System.IO" #> 8 | <#@ import namespace="Microsoft.VisualStudio.TextTemplating" #> 9 | <# 10 | // create an instance of the AutomationHelper class so 11 | // that it is accessible from everywhere within the template 12 | this.VisualStudioHelper = new AutomationHelper(this.Host); 13 | #> 14 | <#+ 15 | /// 16 | /// Object that provides functionality for automating Visual Studio. 17 | /// 18 | public AutomationHelper VisualStudioHelper; 19 | 20 | /// 21 | /// This class provides functionality for automating Visual Studio. 22 | /// 23 | public class AutomationHelper 24 | { 25 | /// 26 | /// Creates a new instance of this class 27 | /// 28 | public AutomationHelper(object host) 29 | { 30 | // store a reference to the template host 31 | // we will need this frequently 32 | this.Host = host as ITextTemplatingEngineHost; 33 | 34 | // initialize the code model API 35 | this.CodeModel = new VsCodeModel(this); 36 | } 37 | 38 | private EnvDTE.DTE _DTE = null; 39 | /// 40 | /// Returns a reference to the primary management object of Visual Studio 41 | /// 42 | public EnvDTE.DTE DTE 43 | { 44 | get 45 | { 46 | if (_DTE == null) 47 | { 48 | var hostServiceProvider = this.Host as IServiceProvider; 49 | if (hostServiceProvider != null) 50 | _DTE = hostServiceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE; 51 | } 52 | return _DTE; 53 | } 54 | } 55 | 56 | /// 57 | /// Stores a reference to the Host of the t4 template 58 | /// 59 | public ITextTemplatingEngineHost Host { get; private set; } 60 | 61 | 62 | #region Solution and Projects 63 | /// 64 | /// Gets the full path of the solution file 65 | /// 66 | public string SolutionFile 67 | { 68 | get 69 | { 70 | return this.DTE.Solution.FileName; 71 | } 72 | } 73 | /// 74 | /// Gets the file name of the currently opened solution. 75 | /// 76 | public string SolutionFileName 77 | { 78 | get 79 | { 80 | return System.IO.Path.GetFileName(this.DTE.Solution.FileName); 81 | } 82 | } 83 | /// 84 | /// Gets the name of the currently opened solution 85 | /// 86 | public string SolutionName 87 | { 88 | get 89 | { 90 | return this.DTE.Solution.Properties.Item("Name").Value.ToString(); 91 | } 92 | } 93 | 94 | /// 95 | /// Gets a list of all Projects within the solution 96 | /// 97 | public IEnumerable GetAllProjects() 98 | { 99 | var ret = new List(); 100 | 101 | // take all projects that are at top level of the solution 102 | // and recursively search Project folders 103 | var topLevelProjects = this.DTE.Solution.Projects; 104 | 105 | foreach(EnvDTE.Project project in topLevelProjects) 106 | { 107 | if (project.Kind == vsProjectType.SolutionFolder) 108 | ret.AddRange(GetProjectsFromItemsCollection(project.ProjectItems)); 109 | else 110 | ret.Add(project); 111 | } 112 | 113 | return ret; 114 | } 115 | /// 116 | /// Gets the project object within the current solution by a given project name. 117 | /// 118 | public EnvDTE.Project GetProject(string projectName) 119 | { 120 | return this.GetAllProjects() 121 | .Where(p => p.Name == projectName) 122 | .First(); 123 | } 124 | /// 125 | /// Gets the project containing the .tt-File 126 | /// 127 | public EnvDTE.Project CurrentProject 128 | { 129 | get 130 | { 131 | return this.FindProjectItem(this.Host.TemplateFile).ContainingProject; 132 | } 133 | } 134 | #endregion 135 | 136 | #region Project Items 137 | public EnvDTE.ProjectItem FindProjectItem(string fileName) 138 | { 139 | return this.DTE.Solution.FindProjectItem(fileName); 140 | } 141 | /// 142 | /// Gets all project items from the current solution 143 | /// 144 | public IEnumerableGetAllSolutionItems() 145 | { 146 | var ret = new List(); 147 | 148 | // iterate all projects and add their items 149 | foreach(EnvDTE.Project project in this.GetAllProjects()) 150 | ret.AddRange(GetAllProjectItems(project)); 151 | 152 | return ret; 153 | } 154 | /// 155 | /// Gets all project items from the current project 156 | /// 157 | public IEnumerableGetAllProjectItems() 158 | { 159 | // get the project of the template file and reeturn all its items 160 | var project = this.CurrentProject; 161 | return GetAllProjectItems(project); 162 | } 163 | /// 164 | /// Gets all Project items from a given project. 165 | /// 166 | public IEnumerableGetAllProjectItems(EnvDTE.Project project) 167 | { 168 | return this.GetProjectItemsRecursively(project.ProjectItems); 169 | } 170 | #endregion 171 | 172 | #region CodeModel 173 | public VsCodeModel CodeModel { get; private set; } 174 | #endregion 175 | 176 | #region Auxiliary stuff 177 | private List GetProjectsFromItemsCollection(EnvDTE.ProjectItems items) 178 | { 179 | var ret = new List(); 180 | 181 | foreach(EnvDTE.ProjectItem item in items) 182 | { 183 | if (item.SubProject == null) 184 | continue; 185 | else if (item.SubProject.Kind == vsProjectType.SolutionFolder) 186 | ret.AddRange(GetProjectsFromItemsCollection(item.SubProject.ProjectItems)); 187 | else if (item.SubProject.Kind == vsProjectType.VisualBasic 188 | || item.SubProject.Kind == vsProjectType.VisualCPlusPlus 189 | || item.SubProject.Kind == vsProjectType.VisualCSharp 190 | || item.SubProject.Kind == vsProjectType.VisualJSharp 191 | || item.SubProject.Kind == vsProjectType.WebProject) 192 | ret.Add(item.SubProject); 193 | } 194 | 195 | return ret; 196 | } 197 | private List GetProjectItemsRecursively(EnvDTE.ProjectItems items) 198 | { 199 | var ret = new List(); 200 | if (items == null) return ret; 201 | 202 | foreach(EnvDTE.ProjectItem item in items) 203 | { 204 | ret.Add(item); 205 | ret.AddRange(GetProjectItemsRecursively(item.ProjectItems)); 206 | } 207 | 208 | return ret; 209 | } 210 | #endregion 211 | 212 | } 213 | 214 | /// 215 | /// Provides functionality to assist "reflecting" the Visual Studio Code Model 216 | /// 217 | public class VsCodeModel 218 | { 219 | internal VsCodeModel(AutomationHelper helper) 220 | { 221 | this.VisualStudioHelper = helper; 222 | } 223 | private AutomationHelper VisualStudioHelper { get; set; } 224 | 225 | /// 226 | /// Searches a given collection of CodeElements recursively for objects of the given elementType. 227 | /// 228 | /// Collection of CodeElements to recursively search for matching objects in. 229 | /// Objects of this CodeModelElement-Type will be returned. 230 | /// If set to true objects that are not part of this solution are retrieved, too. E.g. the INotifyPropertyChanged interface from the System.ComponentModel namespace. 231 | /// A list of CodeElement objects matching the desired elementType. 232 | public IEnumerable GetAllCodeElementsOfType(EnvDTE.CodeElements elements, EnvDTE.vsCMElement elementType, bool includeExternalTypes) 233 | { 234 | var ret = new List(); 235 | 236 | foreach (EnvDTE.CodeElement elem in elements) 237 | { 238 | // iterate all namespaces (even if they are external) 239 | // > they might contain project code 240 | if (elem.Kind == EnvDTE.vsCMElement.vsCMElementNamespace) 241 | { 242 | ret.AddRange(GetAllCodeElementsOfType(((EnvDTE.CodeNamespace)elem).Members, elementType, includeExternalTypes)); 243 | } 244 | // if its not a namespace but external 245 | // > ignore it 246 | else if (elem.InfoLocation == EnvDTE.vsCMInfoLocation.vsCMInfoLocationExternal 247 | && !includeExternalTypes) 248 | continue; 249 | // if its from the project 250 | // > check its members 251 | else if (elem.IsCodeType) 252 | { 253 | ret.AddRange(GetAllCodeElementsOfType(((EnvDTE.CodeType)elem).Members, elementType, includeExternalTypes)); 254 | } 255 | 256 | // if this item is of the desired type 257 | // > store it 258 | if (elem.Kind == elementType) 259 | ret.Add(elem); 260 | } 261 | 262 | return ret; 263 | } 264 | 265 | /// 266 | /// Recursively gets all methods and functions implemented either by the class itself, or one of its base classes. 267 | /// 268 | public IEnumerable GetAllMethods(EnvDTE.CodeClass codeClass) 269 | { 270 | var implFunctions = new List(); 271 | 272 | implFunctions.AddRange(GetMethods(codeClass)); 273 | var baseClass = GetBaseClass(codeClass); 274 | if (baseClass != null) 275 | implFunctions.AddRange(GetAllMethods(baseClass)); 276 | 277 | return implFunctions.Distinct(new CodeFunctionEqualityComparer()); 278 | } 279 | 280 | /// 281 | /// Gets all methods and functions directly implemented by a code class 282 | /// 283 | public IEnumerable GetMethods(EnvDTE.CodeClass codeClass) 284 | { 285 | return GetAllCodeElementsOfType(codeClass.Members, EnvDTE.vsCMElement.vsCMElementFunction, true).OfType(); 286 | } 287 | 288 | 289 | /// 290 | /// Recursively gets all interfaces a given CodeClass implements either itself, one of its base classes or as an inherited interface. 291 | /// Respects partial classes. 292 | /// 293 | public IEnumerable GetAllImplementedInterfaces(EnvDTE.CodeClass codeClass) 294 | { 295 | var implInterfaces = new List(); 296 | 297 | foreach(var partialClass in GetPartialClasses(codeClass)) 298 | { 299 | foreach(EnvDTE.CodeInterface ci in GetImplementedInterfaces(partialClass)) 300 | { 301 | implInterfaces.Add(ci); 302 | implInterfaces.AddRange(GetAllBaseInterfaces(ci)); 303 | } 304 | 305 | var baseClass = GetBaseClass(partialClass); 306 | if (baseClass != null) 307 | implInterfaces.AddRange(GetAllImplementedInterfaces(baseClass)); 308 | } 309 | 310 | return implInterfaces.Distinct(new CodeInterfaceEqualityComparer()); 311 | } 312 | /// 313 | /// Gets all interfaces a given CodeClass implements directly. 314 | /// 315 | public IEnumerable GetImplementedInterfaces(EnvDTE.CodeClass codeClass) 316 | { 317 | return GetAllCodeElementsOfType(codeClass.ImplementedInterfaces, EnvDTE.vsCMElement.vsCMElementInterface, true).OfType(); 318 | } 319 | /// 320 | /// Recursively gets all interfaces a given CodeInterface implements/inherits from. 321 | /// 322 | public IEnumerable GetAllBaseInterfaces(EnvDTE.CodeInterface codeInterface) 323 | { 324 | var ret = new List(); 325 | 326 | var directBases = GetBaseInterfaces(codeInterface); 327 | ret.AddRange(directBases); 328 | 329 | foreach(var baseInterface in directBases) 330 | ret.AddRange(GetAllBaseInterfaces(baseInterface)); 331 | 332 | return ret; 333 | } 334 | /// 335 | /// Returns a list of all base interfaces a given CodeInterface implements/inherits from. 336 | /// 337 | public IEnumerable GetBaseInterfaces(EnvDTE.CodeInterface codeInterface) 338 | { 339 | return codeInterface.Bases.OfType(); 340 | } 341 | 342 | /// 343 | /// Recursively gets all base classes of the given CodeClass respecting partial implementations. 344 | /// 345 | public IEnumerable GetAllBaseClasses(EnvDTE.CodeClass codeClass) 346 | { 347 | var ret = new List(); 348 | 349 | // iterate all partial implementations 350 | foreach(EnvDTE.CodeClass partialClass in GetPartialClasses(codeClass)) 351 | { 352 | // climb up the inheritance tree 353 | var cc = partialClass; 354 | while(cc != null) 355 | { 356 | cc = GetBaseClass(cc); 357 | if (cc != null) ret.Add(cc); 358 | } 359 | } 360 | 361 | return ret; 362 | } 363 | /// 364 | /// Returns the base class of a given CodeClass, if it has any. 365 | /// 366 | public EnvDTE.CodeClass GetBaseClass(EnvDTE.CodeClass codeClass) 367 | { 368 | return codeClass.Bases.OfType().FirstOrDefault(); 369 | } 370 | 371 | /// 372 | /// Checks if the given CodeClass has partial implementations. 373 | /// 374 | /// A list of the partial CodeClasses that form the given class. If the class is not partial, the class itself is returned in the list. 375 | public IEnumerable GetPartialClasses(EnvDTE.CodeClass codeClass) 376 | { 377 | var classParts = new List(); 378 | 379 | // partial classes are a new feature and only available in the CodeClass2 interface 380 | // check if the given class is a CodeClass2 381 | if (codeClass is EnvDTE80.CodeClass2) 382 | { 383 | // yes, it is 384 | EnvDTE80.CodeClass2 cc2 = (EnvDTE80.CodeClass2)codeClass; 385 | // check if it consists of multiple partial classes 386 | if (cc2.ClassKind != EnvDTE80.vsCMClassKind.vsCMClassKindPartialClass) 387 | // no > only return the class itself 388 | classParts.Add(cc2); 389 | else 390 | // yes > add all partial classes 391 | classParts.AddRange(cc2.PartialClasses.OfType()); 392 | } 393 | else 394 | // this is no CodeClass2 > return itself 395 | classParts.Add(codeClass); 396 | 397 | return classParts; 398 | } 399 | 400 | #region private classes 401 | private class CodeInterfaceEqualityComparer : IEqualityComparer 402 | { 403 | public bool Equals(EnvDTE.CodeInterface x, EnvDTE.CodeInterface y) 404 | { 405 | var n1 = x.FullName; 406 | var n2 = y.FullName; 407 | return n1 == n2; 408 | } 409 | 410 | public int GetHashCode(EnvDTE.CodeInterface obj) 411 | { 412 | return obj.FullName.GetHashCode(); 413 | } 414 | } 415 | private class CodeFunctionEqualityComparer : IEqualityComparer 416 | { 417 | public bool Equals(EnvDTE.CodeFunction x, EnvDTE.CodeFunction y) 418 | { 419 | var n1 = x.FullName; 420 | var n2 = y.FullName; 421 | return n1 == n2; 422 | } 423 | 424 | public int GetHashCode(EnvDTE.CodeFunction obj) 425 | { 426 | return obj.FullName.GetHashCode(); 427 | } 428 | } 429 | #endregion 430 | } 431 | 432 | public class vsProjectType 433 | { 434 | public const string SolutionFolder = "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}"; 435 | public const string VisualBasic = "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"; 436 | public const string VisualCSharp = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; 437 | public const string VisualCPlusPlus = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"; 438 | public const string VisualJSharp = "{E6FDF86B-F3D1-11D4-8576-0002A516ECE8}"; 439 | public const string WebProject = "{E24C65DC-7377-472b-9ABA-BC803B73C61A}"; 440 | } 441 | #> -------------------------------------------------------------------------------- /src/readme.txt: -------------------------------------------------------------------------------- 1 | =========== 2 | T4Immutable 3 | =========== 4 | 5 | Please run the generator by using "Build - Transform All T4 Templates" or if that doesn't work (e.g. 6 | .NET Core/Standard/etc projects) right click the file "T4Immutable/T4Immutable.tt" and click on 7 | "Run custom tool" whenever you want to regenerate your templates. 8 | 9 | You should also do this after each time you update this nuget package. 10 | 11 | For .NET Framework projects the files will be generated inside "T4Immutable" as children of "T4Immutable.tt". 12 | For .NET Core/Standard/etc projects they will be generated in a folder named "T4Immutable_generated". 13 | --------------------------------------------------------------------------------