├── lib ├── Directory.Build.props ├── Directory.Build.targets ├── csharp-models-to-json │ ├── Config.cs │ ├── ExtraInfo.cs │ ├── csharp-models-to-json.csproj │ ├── EnumCollector.cs │ ├── Program.cs │ ├── Util.cs │ └── ModelCollector.cs └── csharp-models-to-json_test │ ├── csharp-models-to-json_test.csproj │ ├── EnumCollector_test.cs │ └── ModelCollector_test.cs ├── .gitignore ├── test-files ├── test-config.json ├── test-file-exists.js └── TestFile.cs ├── package.json ├── .github └── workflows │ ├── node.tests.yml │ └── csharp.tests.yml ├── README.md ├── index.js └── converter.js /lib/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | -------------------------------------------------------------------------------- /lib/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | lib/csharp-models-to-json/bin 4 | lib/csharp-models-to-json/obj 5 | lib/csharp-models-to-json_test/obj/ 6 | lib/csharp-models-to-json_test/bin/ 7 | /test-files/api.d.ts 8 | 9 | **/.vs/* 10 | .vscode/* 11 | .idea/* 12 | *.user -------------------------------------------------------------------------------- /lib/csharp-models-to-json/Config.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CSharpModelsToJson 4 | { 5 | public class Config 6 | { 7 | public List Include { get; set; } 8 | public List Exclude { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/csharp-models-to-json/ExtraInfo.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace CSharpModelsToJson 3 | { 4 | public class ExtraInfo 5 | { 6 | public bool Obsolete { get; set; } 7 | public string ObsoleteMessage { get; set; } 8 | public string Summary { get; set; } 9 | public string Remarks { get; set; } 10 | public bool EmitDefaultValue { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/csharp-models-to-json/csharp-models-to-json.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test-files/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "test-files/*.cs" 4 | ], 5 | "exclude": [ ], 6 | "output": "./test-files/api.d.ts", 7 | "camelCase": false, 8 | "camelCaseEnums": false, 9 | "camelCaseOptions": { 10 | "pascalCase": false, 11 | "preserveConsecutiveUppercase": false, 12 | "locale": "en-US" 13 | }, 14 | "includeComments": true, 15 | "numericEnums": true, 16 | "validateEmitDefaultValue": true, 17 | "omitSemicolon": true, 18 | "omitFilePathComment": true, 19 | "stringLiteralTypesInsteadOfEnums": false, 20 | "customTypeTranslations": { 21 | "ProductName": "string", 22 | "ProductNumber": "string", 23 | "byte": "number", 24 | "uint": "number" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csharp-models-to-typescript", 3 | "version": "1.2.0", 4 | "title": "C# models to TypeScript", 5 | "author": "Jonathan Svenheden ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/svenheden/csharp-models-to-typescript.git" 10 | }, 11 | "engines": { 12 | "node": ">=6.0.0" 13 | }, 14 | "bin": { 15 | "csharp-models-to-typescript": "./index.js" 16 | }, 17 | "scripts": { 18 | "test": "rimraf -I ./test-files/api.d.ts && node index.js --config=./test-files/test-config.json && node ./test-files/test-file-exists.js --config=./test-files/test-config.json" 19 | }, 20 | "dependencies": { 21 | "camelcase": "^6.0.0" 22 | }, 23 | "devDependencies": { 24 | "rimraf": "^5.0.7" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/csharp-models-to-json_test/csharp-models-to-json_test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {BAEFF23F-0CE8-48B5-899A-56251439C30C} 15 | csharp-models-to-json 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/node.tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x, 20.x] 16 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: 'npm' 25 | - run: npm ci 26 | - run: npm run test 27 | -------------------------------------------------------------------------------- /.github/workflows/csharp.tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: C# CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | defaults: 14 | run: 15 | working-directory: ./lib/csharp-models-to-json_test 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: 8.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | 30 | -------------------------------------------------------------------------------- /test-files/test-file-exists.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const configArg = process.argv.find(x => x.startsWith('--config=')); 4 | 5 | if (!configArg) { 6 | throw Error('No configuration file for `csharp-models-to-typescript` provided.'); 7 | } 8 | 9 | const configPath = configArg.substr('--config='.length); 10 | let config; 11 | 12 | try { 13 | unparsedConfig = fs.readFileSync(configPath, 'utf8'); 14 | } catch (error) { 15 | throw Error(`Configuration file "${configPath}" not found.\n${error.message}`); 16 | } 17 | 18 | try { 19 | config = JSON.parse(unparsedConfig); 20 | } catch (error) { 21 | throw Error(`Configuration file "${configPath}" contains invalid JSON.\n${error.message}`); 22 | } 23 | 24 | const output = config.output || 'types.d.ts'; 25 | 26 | if (!fs.existsSync(output)) 27 | throw Error(`Can't find output file: ${output}`) 28 | 29 | const file = fs.readFileSync(output, 'utf8') 30 | if (file.length === 0) 31 | throw Error(`File '${output}' is empty`) -------------------------------------------------------------------------------- /test-files/TestFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Runtime.Serialization; 5 | 6 | namespace TestNamespace 7 | { 8 | /// 9 | /// Sample class comment. 10 | /// 11 | public class TestClass 12 | { 13 | /// 14 | /// Sample comment. 15 | /// 16 | public int IntProperty { get; set; } 17 | 18 | [Obsolete("obsolete test prop")] 19 | public string StringProperty { get; set; } 20 | 21 | [DataMember(EmitDefaultValue = false)] 22 | public DateTime DateTimeProperty { get; set; } 23 | 24 | public bool BooleanProperty { get; set; } 25 | } 26 | 27 | public enum TestEnum { 28 | A = 1, // decimal: 1 29 | B = 1_002, // decimal: 1002 30 | C = 0b011, // binary: 3 in decimal 31 | D = 0b_0000_0100, // binary: 4 in decimal 32 | E = 0x005, // hexadecimal: 5 in decimal 33 | F = 0x000_01a, // hexadecimal: 26 in decimal 34 | [Obsolete("obsolete test enum")] 35 | G // 27 in decimal 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/csharp-models-to-json_test/EnumCollector_test.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using NUnit.Framework; 5 | 6 | namespace CSharpModelsToJson.Tests 7 | { 8 | [TestFixture] 9 | public class EnumCollectorTest 10 | { 11 | [Test] 12 | public void ReturnEnumWithMissingValues() 13 | { 14 | var tree = CSharpSyntaxTree.ParseText(@" 15 | public enum SampleEnum 16 | { 17 | A, 18 | B = 7, 19 | C, 20 | D = 4, 21 | E 22 | }" 23 | ); 24 | 25 | var root = (CompilationUnitSyntax)tree.GetRoot(); 26 | 27 | var enumCollector = new EnumCollector(); 28 | enumCollector.VisitEnumDeclaration(root.DescendantNodes().OfType().First()); 29 | 30 | var model = enumCollector.Enums.First(); 31 | 32 | Assert.That(model, Is.Not.Null); 33 | Assert.That(model.Values, Is.Not.Null); 34 | 35 | Assert.That(model.Values["A"].Value, Is.Null); 36 | Assert.That(model.Values["B"].Value, Is.EqualTo("7")); 37 | Assert.That(model.Values["C"].Value, Is.Null); 38 | Assert.That(model.Values["D"].Value, Is.EqualTo("4")); 39 | Assert.That(model.Values["E"].Value, Is.Null); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C# models to TypeScript 2 | 3 | This is a tool that consumes your C# domain models and types and creates TypeScript declaration files from them. There's other tools that does this but what makes this one different is that it internally uses [Roslyn (the .NET compiler platform)](https://github.com/dotnet/roslyn) to parse the source files, which removes the need to create and maintain our own parser. 4 | 5 | 6 | [![NPM version][npm-image]][npm-url] 7 | 8 | 9 | ## Dependencies 10 | 11 | * [.NET Core SDK](https://www.microsoft.com/net/download/macos) 12 | 13 | 14 | ## Install 15 | 16 | ``` 17 | $ npm install --save csharp-models-to-typescript 18 | ``` 19 | 20 | ## How to use 21 | 22 | 1. Add a config file to your project that contains for example... 23 | 24 | ``` 25 | { 26 | "include": [ 27 | "./models/**/*.cs", 28 | "./enums/**/*.cs" 29 | ], 30 | "exclude": [ 31 | "./models/foo/bar.cs" 32 | ], 33 | "namespace": "Api", 34 | "output": "./api.d.ts", 35 | "includeComments": true, 36 | "camelCase": false, 37 | "camelCaseEnums": false, 38 | "camelCaseOptions": { 39 | "pascalCase": false, 40 | "preserveConsecutiveUppercase": false, 41 | "locale": "en-US" 42 | }, 43 | "numericEnums": false, 44 | "validateEmitDefaultValue": false, 45 | "omitFilePathComment": false, 46 | "omitSemicolon": false, 47 | "stringLiteralTypesInsteadOfEnums": false, 48 | "customTypeTranslations": { 49 | "ProductName": "string", 50 | "ProductNumber": "string" 51 | } 52 | } 53 | ``` 54 | 55 | 2. Add a npm script to your package.json that references your config file... 56 | 57 | ``` 58 | "scripts": { 59 | "generate-types": "csharp-models-to-typescript --config=your-config-file.json" 60 | }, 61 | ``` 62 | 63 | 3. Run the npm script `generate-types` and the output file specified in your config should be created and populated with your models. 64 | 65 | 66 | ## License 67 | 68 | MIT © [Jonathan Svenheden](https://github.com/svenheden) 69 | 70 | [npm-image]: https://img.shields.io/npm/v/csharp-models-to-typescript.svg 71 | [npm-url]: https://npmjs.org/package/csharp-models-to-typescript 72 | -------------------------------------------------------------------------------- /lib/csharp-models-to-json/EnumCollector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace CSharpModelsToJson 6 | { 7 | public class Enum 8 | { 9 | public string Identifier { get; set; } 10 | public ExtraInfo ExtraInfo { get; set; } 11 | public Dictionary Values { get; set; } 12 | } 13 | 14 | public class EnumValue 15 | { 16 | public string Value { get; set; } 17 | public ExtraInfo ExtraInfo { get; set; } 18 | } 19 | 20 | 21 | public class EnumCollector: CSharpSyntaxWalker 22 | { 23 | public readonly List Enums = new List(); 24 | 25 | public override void VisitEnumDeclaration(EnumDeclarationSyntax node) 26 | { 27 | var values = new Dictionary(); 28 | 29 | foreach (var member in node.Members) { 30 | var equalsValue = member.EqualsValue != null 31 | ? member.EqualsValue.Value.ToString() 32 | : null; 33 | 34 | var value = new EnumValue 35 | { 36 | Value = equalsValue?.Replace("_", ""), 37 | ExtraInfo = new ExtraInfo 38 | { 39 | Obsolete = Util.IsObsolete(member.AttributeLists), 40 | ObsoleteMessage = Util.GetObsoleteMessage(member.AttributeLists), 41 | Summary = Util.GetSummaryMessage(member), 42 | Remarks = Util.GetRemarksMessage(member), 43 | } 44 | }; 45 | 46 | values[member.Identifier.ToString()] = value; 47 | } 48 | 49 | this.Enums.Add(new Enum() { 50 | Identifier = node.Identifier.ToString(), 51 | ExtraInfo = new ExtraInfo 52 | { 53 | Obsolete = Util.IsObsolete(node.AttributeLists), 54 | ObsoleteMessage = Util.GetObsoleteMessage(node.AttributeLists), 55 | Summary = Util.GetSummaryMessage(node), 56 | Remarks = Util.GetRemarksMessage(node), 57 | }, 58 | Values = values 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const process = require('process'); 5 | const path = require('path'); 6 | const { spawn } = require('child_process'); 7 | 8 | const createConverter = require('./converter'); 9 | 10 | const configArg = process.argv.find(x => x.startsWith('--config=')); 11 | 12 | if (!configArg) { 13 | return console.error('No configuration file for `csharp-models-to-typescript` provided.'); 14 | } 15 | 16 | const configPath = configArg.substr('--config='.length); 17 | let config; 18 | 19 | try { 20 | unparsedConfig = fs.readFileSync(configPath, 'utf8'); 21 | } catch (error) { 22 | return console.error(`Configuration file "${configPath}" not found.`); 23 | } 24 | 25 | try { 26 | config = JSON.parse(unparsedConfig); 27 | } catch (error) { 28 | return console.error(`Configuration file "${configPath}" contains invalid JSON.`); 29 | } 30 | 31 | const output = config.output || 'types.d.ts'; 32 | 33 | const converter = createConverter({ 34 | customTypeTranslations: config.customTypeTranslations || {}, 35 | namespace: config.namespace, 36 | includeComments: config.includeComments ?? true, 37 | camelCase: config.camelCase || false, 38 | camelCaseOptions: config.camelCaseOptions || {}, 39 | camelCaseEnums: config.camelCaseEnums || false, 40 | numericEnums: config.numericEnums || false, 41 | validateEmitDefaultValue: config.validateEmitDefaultValue || false, 42 | omitFilePathComment: config.omitFilePathComment || false, 43 | omitSemicolon: config.omitSemicolon || false, 44 | stringLiteralTypesInsteadOfEnums: config.stringLiteralTypesInsteadOfEnums || false 45 | }); 46 | 47 | let timer = process.hrtime(); 48 | 49 | const dotnetProject = path.join(__dirname, 'lib/csharp-models-to-json'); 50 | const dotnetProcess = spawn('dotnet', ['run', `--project "${dotnetProject}"`, `"${path.resolve(configPath)}"`], { shell: true }); 51 | 52 | let stdout = ''; 53 | 54 | dotnetProcess.stdout.on('data', data => { 55 | stdout += data; 56 | }); 57 | 58 | dotnetProcess.stderr.on('data', err => { 59 | console.error(err.toString()); 60 | }); 61 | 62 | dotnetProcess.stdout.on('end', () => { 63 | let json; 64 | 65 | //console.log(stdout); 66 | 67 | try { 68 | // Extract the JSON content between the markers 69 | const startMarker = '<<<<<>>>>>'; 70 | const endMarker = '<<<<<>>>>>'; 71 | const startIndex = stdout.indexOf(startMarker); 72 | const endIndex = stdout.indexOf(endMarker); 73 | 74 | if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) { 75 | const jsonString = stdout.substring(startIndex + startMarker.length, endIndex).trim(); 76 | json = JSON.parse(jsonString); 77 | } else { 78 | throw new Error('JSON markers not found or invalid order of markers.'); 79 | } 80 | } catch (error) { 81 | return console.error([ 82 | 'The output from `csharp-models-to-json` contains invalid JSON.', 83 | error.message, 84 | stdout 85 | ].join('\n\n')); 86 | } 87 | 88 | const types = converter(json); 89 | 90 | fs.writeFile(output, types, err => { 91 | if (err) { 92 | return console.error(err); 93 | } 94 | 95 | timer = process.hrtime(timer); 96 | console.log('Done in %d.%d seconds.', timer[0], timer[1]); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /lib/csharp-models-to-json/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using Ganss.IO; 9 | 10 | namespace CSharpModelsToJson 11 | { 12 | class File 13 | { 14 | public string FileName { get; set; } 15 | public IEnumerable Models { get; set; } 16 | public IEnumerable Enums { get; set; } 17 | } 18 | 19 | class Program 20 | { 21 | static void Main(string[] args) 22 | { 23 | Config? config = null; 24 | if (System.IO.File.Exists(args[0])) { 25 | var configJson = System.IO.File.ReadAllText(args[0]); 26 | var opts = new JsonSerializerOptions { 27 | PropertyNameCaseInsensitive = true 28 | }; 29 | config = JsonSerializer.Deserialize(configJson, opts); 30 | } 31 | 32 | var includes = config?.Include ?? []; 33 | var excludes = config?.Exclude ?? []; 34 | 35 | List files = new List(); 36 | 37 | foreach (string fileName in getFileNames(includes, excludes)) { 38 | files.Add(parseFile(fileName)); 39 | } 40 | 41 | JsonSerializerOptions options = new() 42 | { 43 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 44 | }; 45 | 46 | string json = JsonSerializer.Serialize(files, options); 47 | 48 | var sb = new StringBuilder(); 49 | sb.AppendLine("<<<<<>>>>>"); 50 | sb.AppendLine(json); 51 | sb.AppendLine("<<<<<>>>>>"); 52 | 53 | System.Console.OutputEncoding = System.Text.Encoding.UTF8; 54 | System.Console.WriteLine(sb.ToString()); 55 | } 56 | 57 | static List getFileNames(List includes, List excludes) { 58 | List fileNames = new List(); 59 | 60 | foreach (var path in expandGlobPatterns(includes)) { 61 | fileNames.Add(path); 62 | } 63 | 64 | foreach (var path in expandGlobPatterns(excludes)) { 65 | fileNames.Remove(path); 66 | } 67 | 68 | return fileNames; 69 | } 70 | 71 | static List expandGlobPatterns(List globPatterns) { 72 | List fileNames = new List(); 73 | 74 | foreach (string pattern in globPatterns) { 75 | var paths = Glob.Expand(pattern); 76 | 77 | foreach (var path in paths) { 78 | fileNames.Add(path.FullName); 79 | } 80 | } 81 | 82 | return fileNames; 83 | } 84 | 85 | static File parseFile(string path) { 86 | string source = System.IO.File.ReadAllText(path); 87 | SyntaxTree tree = CSharpSyntaxTree.ParseText(source); 88 | var root = (CompilationUnitSyntax) tree.GetRoot(); 89 | 90 | var modelCollector = new ModelCollector(); 91 | var enumCollector = new EnumCollector(); 92 | 93 | modelCollector.Visit(root); 94 | enumCollector.Visit(root); 95 | 96 | return new File() { 97 | FileName = System.IO.Path.GetFullPath(path), 98 | Models = modelCollector.Models, 99 | Enums = enumCollector.Enums 100 | }; 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /lib/csharp-models-to-json/Util.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using System; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace CSharpModelsToJson 9 | { 10 | internal static class Util 11 | { 12 | internal static bool IsObsolete(SyntaxList attributeLists) => 13 | attributeLists.Any(attributeList => 14 | attributeList.Attributes.Any(attribute => 15 | attribute.Name.ToString().Equals("Obsolete") || attribute.Name.ToString().Equals("ObsoleteAttribute"))); 16 | 17 | internal static string GetObsoleteMessage(SyntaxList attributeLists) 18 | { 19 | foreach (var attributeList in attributeLists) 20 | { 21 | var obsoleteAttribute = 22 | attributeList.Attributes.FirstOrDefault(attribute => 23 | attribute.Name.ToString().Equals("Obsolete") || attribute.Name.ToString().Equals("ObsoleteAttribute")); 24 | 25 | if (obsoleteAttribute != null) 26 | { 27 | return obsoleteAttribute.ArgumentList == null 28 | ? null 29 | : obsoleteAttribute.ArgumentList.Arguments.ToString()?.TrimStart('@').Trim('"'); 30 | } 31 | } 32 | 33 | return null; 34 | } 35 | 36 | internal static string GetSummaryMessage(SyntaxNode classItem) 37 | { 38 | return GetCommentTag(classItem, "summary"); 39 | } 40 | 41 | internal static string GetRemarksMessage(SyntaxNode classItem) 42 | { 43 | return GetCommentTag(classItem, "remarks"); 44 | } 45 | 46 | private static string GetCommentTag(SyntaxNode classItem, string xmlTag) 47 | { 48 | var documentComment = classItem.GetDocumentationCommentTriviaSyntax(); 49 | 50 | if (documentComment == null) 51 | return null; 52 | 53 | var summaryElement = documentComment.Content 54 | .OfType() 55 | .FirstOrDefault(_ => _.StartTag.Name.LocalName.Text == xmlTag); 56 | 57 | if (summaryElement == null) 58 | return null; 59 | 60 | var summaryText = summaryElement.DescendantTokens() 61 | .Where(_ => _.Kind() == SyntaxKind.XmlTextLiteralToken) 62 | .Select(_ => _.Text.Trim()) 63 | .ToList(); 64 | 65 | var summaryContent = summaryElement.Content.ToString(); 66 | summaryContent = Regex.Replace(summaryContent, @"^\s*///\s*", string.Empty, RegexOptions.Multiline); 67 | summaryContent = Regex.Replace(summaryContent, "^", Environment.NewLine, RegexOptions.Multiline); 68 | summaryContent = Regex.Replace(summaryContent, "", string.Empty); 69 | 70 | return summaryContent.Trim(); 71 | } 72 | 73 | public static DocumentationCommentTriviaSyntax GetDocumentationCommentTriviaSyntax(this SyntaxNode node) 74 | { 75 | if (node == null) 76 | { 77 | return null; 78 | } 79 | 80 | foreach (var leadingTrivia in node.GetLeadingTrivia()) 81 | { 82 | var structure = leadingTrivia.GetStructure() as DocumentationCommentTriviaSyntax; 83 | 84 | if (structure != null) 85 | { 86 | return structure; 87 | } 88 | } 89 | 90 | return null; 91 | } 92 | 93 | internal static bool GetEmitDefaultValue(SyntaxList attributeLists) 94 | { 95 | var dataMemberAttribute = attributeLists 96 | .SelectMany(attributeList => attributeList.Attributes) 97 | .FirstOrDefault(attribute => attribute.Name.ToString().Equals("DataMember") || attribute.Name.ToString().Equals("DataMemberAttribute")); 98 | 99 | if (dataMemberAttribute?.ArgumentList == null) 100 | return true; 101 | 102 | var emitDefaultValueArgument = dataMemberAttribute.ArgumentList.Arguments.FirstOrDefault(x => x.ToString().StartsWith("EmitDefaultValue")); 103 | 104 | if (emitDefaultValueArgument == null) 105 | return true; 106 | 107 | return !emitDefaultValueArgument.ToString().EndsWith("false"); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/csharp-models-to-json/ModelCollector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | 7 | namespace CSharpModelsToJson 8 | { 9 | public class Model 10 | { 11 | public string ModelName { get; set; } 12 | public IEnumerable Fields { get; set; } 13 | public IEnumerable Properties { get; set; } 14 | public IEnumerable BaseClasses { get; set; } 15 | public ExtraInfo ExtraInfo { get; set; } 16 | } 17 | 18 | public class Field 19 | { 20 | public string Identifier { get; set; } 21 | public string Type { get; set; } 22 | } 23 | 24 | public class Property 25 | { 26 | public string Identifier { get; set; } 27 | public string Type { get; set; } 28 | public ExtraInfo ExtraInfo { get; set; } 29 | } 30 | 31 | public class ModelCollector : CSharpSyntaxWalker 32 | { 33 | public readonly List Models = new List(); 34 | 35 | public override void VisitClassDeclaration(ClassDeclarationSyntax node) 36 | { 37 | var model = CreateModel(node); 38 | 39 | Models.Add(model); 40 | } 41 | 42 | public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) 43 | { 44 | var model = CreateModel(node); 45 | 46 | Models.Add(model); 47 | } 48 | 49 | public override void VisitRecordDeclaration(RecordDeclarationSyntax node) 50 | { 51 | var model = new Model() 52 | { 53 | ModelName = $"{node.Identifier.ToString()}{node.TypeParameterList?.ToString()}", 54 | Fields = node.ParameterList?.Parameters 55 | .Where(field => IsAccessible(field.Modifiers)) 56 | .Where(property => !IsIgnored(property.AttributeLists)) 57 | .Select((field) => new Field 58 | { 59 | Identifier = field.Identifier.ToString(), 60 | Type = field.Type.ToString(), 61 | }), 62 | Properties = node.Members.OfType() 63 | .Where(property => IsAccessible(property.Modifiers)) 64 | .Where(property => !IsIgnored(property.AttributeLists)) 65 | .Select(ConvertProperty), 66 | BaseClasses = new List(), 67 | }; 68 | 69 | Models.Add(model); 70 | } 71 | 72 | private static Model CreateModel(TypeDeclarationSyntax node) 73 | { 74 | return new Model() 75 | { 76 | ModelName = $"{node.Identifier.ToString()}{node.TypeParameterList?.ToString()}", 77 | Fields = node.Members.OfType() 78 | .Where(field => IsAccessible(field.Modifiers)) 79 | .Where(property => !IsIgnored(property.AttributeLists)) 80 | .Select(ConvertField), 81 | Properties = node.Members.OfType() 82 | .Where(property => IsAccessible(property.Modifiers)) 83 | .Where(property => !IsIgnored(property.AttributeLists)) 84 | .Select(ConvertProperty), 85 | BaseClasses = node.BaseList?.Types.Select(s => s.ToString()), 86 | ExtraInfo = new ExtraInfo 87 | { 88 | Obsolete = Util.IsObsolete(node.AttributeLists), 89 | ObsoleteMessage = Util.GetObsoleteMessage(node.AttributeLists), 90 | Summary = Util.GetSummaryMessage(node), 91 | Remarks = Util.GetRemarksMessage(node), 92 | } 93 | }; 94 | } 95 | 96 | private static bool IsIgnored(SyntaxList propertyAttributeLists) => 97 | propertyAttributeLists.Any(attributeList => 98 | attributeList.Attributes.Any(attribute => 99 | attribute.Name.ToString().Equals("JsonIgnore") || 100 | attribute.Name.ToString().Equals("IgnoreDataMember"))); 101 | 102 | private static bool IsAccessible(SyntaxTokenList modifiers) => modifiers.All(modifier => 103 | modifier.ToString() != "const" && 104 | modifier.ToString() != "static" && 105 | modifier.ToString() != "private" 106 | ); 107 | 108 | private static Field ConvertField(FieldDeclarationSyntax field) => new Field 109 | { 110 | Identifier = field.Declaration.Variables.First().GetText().ToString(), 111 | Type = field.Declaration.Type.ToString(), 112 | }; 113 | 114 | private static Property ConvertProperty(PropertyDeclarationSyntax property) => new Property 115 | { 116 | Identifier = property.Identifier.ToString(), 117 | Type = property.Type.ToString(), 118 | ExtraInfo = new ExtraInfo 119 | { 120 | Obsolete = Util.IsObsolete(property.AttributeLists), 121 | ObsoleteMessage = Util.GetObsoleteMessage(property.AttributeLists), 122 | Summary = Util.GetSummaryMessage(property), 123 | Remarks = Util.GetRemarksMessage(property), 124 | EmitDefaultValue = Util.GetEmitDefaultValue(property.AttributeLists), 125 | } 126 | }; 127 | } 128 | } -------------------------------------------------------------------------------- /converter.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const camelcase = require('camelcase'); 3 | 4 | const flatten = arr => arr.reduce((a, b) => a.concat(b), []); 5 | 6 | const arrayRegex = /^(.+)\[\]$/; 7 | const simpleCollectionRegex = /^(?:I?List|IReadOnlyList|IEnumerable|ICollection|IReadOnlyCollection|HashSet)<([\w\d]+)>\??$/; 8 | const collectionRegex = /^(?:I?List|IReadOnlyList|IEnumerable|ICollection|IReadOnlyCollection|HashSet)<(.+)>\??$/; 9 | const simpleDictionaryRegex = /^(?:I?Dictionary|SortedDictionary|IReadOnlyDictionary)<([\w\d]+)\s*,\s*([\w\d]+)>\??$/; 10 | const dictionaryRegex = /^(?:I?Dictionary|SortedDictionary|IReadOnlyDictionary)<([\w\d]+)\s*,\s*(.+)>\??$/; 11 | 12 | const defaultTypeTranslations = { 13 | int: 'number', 14 | double: 'number', 15 | float: 'number', 16 | Int32: 'number', 17 | Int64: 'number', 18 | short: 'number', 19 | long: 'number', 20 | decimal: 'number', 21 | bool: 'boolean', 22 | DateTime: 'string', 23 | DateTimeOffset: 'string', 24 | Guid: 'string', 25 | dynamic: 'any', 26 | object: 'any', 27 | }; 28 | 29 | const createConverter = config => { 30 | const typeTranslations = Object.assign({}, defaultTypeTranslations, config.customTypeTranslations); 31 | 32 | const convert = json => { 33 | const content = json.map(file => { 34 | const filename = path.relative(process.cwd(), file.FileName); 35 | 36 | const rows = flatten([ 37 | ...file.Models.map(model => convertModel(model, filename)), 38 | ...file.Enums.map(enum_ => convertEnum(enum_, filename)), 39 | ]); 40 | 41 | return rows 42 | .map(row => config.namespace ? ` ${row}` : row) 43 | .join('\n'); 44 | }); 45 | 46 | const filteredContent = content.filter(x => x.length > 0); 47 | 48 | if (config.namespace) { 49 | return [ 50 | `declare module ${config.namespace} {`, 51 | ...filteredContent, 52 | '}', 53 | ].join('\n'); 54 | } else { 55 | return filteredContent.join('\n'); 56 | } 57 | }; 58 | 59 | const convertModel = (model, filename) => { 60 | const rows = []; 61 | 62 | if (model.BaseClasses) { 63 | model.IndexSignature = model.BaseClasses.find(type => type.match(dictionaryRegex)); 64 | model.BaseClasses = model.BaseClasses.filter(type => !type.match(dictionaryRegex)); 65 | } 66 | 67 | const members = [...(model.Fields || []), ...(model.Properties || [])]; 68 | const baseClasses = model.BaseClasses && model.BaseClasses.length ? ` extends ${model.BaseClasses.join(', ')}` : ''; 69 | 70 | if (!config.omitFilePathComment) { 71 | rows.push(`// ${filename}`); 72 | } 73 | let classCommentRows = formatComment(model.ExtraInfo, '') 74 | if (classCommentRows) { 75 | rows.push(classCommentRows); 76 | } 77 | 78 | rows.push(`export interface ${model.ModelName}${baseClasses} {`); 79 | 80 | const propertySemicolon = config.omitSemicolon ? '' : ';'; 81 | 82 | if (model.IndexSignature) { 83 | rows.push(` ${convertIndexType(model.IndexSignature)}${propertySemicolon}`); 84 | } 85 | 86 | members.forEach(member => { 87 | let memberCommentRows = formatComment(member.ExtraInfo, ' ') 88 | if (memberCommentRows) { 89 | rows.push(memberCommentRows); 90 | } 91 | 92 | rows.push(` ${convertProperty(member)}${propertySemicolon}`); 93 | }); 94 | 95 | rows.push(`}\n`); 96 | 97 | return rows; 98 | }; 99 | 100 | const convertEnum = (enum_, filename) => { 101 | const rows = []; 102 | if (!config.omitFilePathComment) { 103 | rows.push(`// ${filename}`); 104 | } 105 | 106 | const entries = Object.entries(enum_.Values); 107 | 108 | let classCommentRows = formatComment(enum_.ExtraInfo, '') 109 | if (classCommentRows) { 110 | rows.push(classCommentRows); 111 | } 112 | 113 | const getEnumStringValue = (value) => config.camelCaseEnums 114 | ? camelcase(value) 115 | : value; 116 | 117 | const lastValueSemicolon = config.omitSemicolon ? '' : ';'; 118 | 119 | if (config.stringLiteralTypesInsteadOfEnums) { 120 | rows.push(`export type ${enum_.Identifier} =`); 121 | 122 | entries.forEach(([key], i) => { 123 | const delimiter = (i === entries.length - 1) ? lastValueSemicolon : ' |'; 124 | rows.push(` '${getEnumStringValue(key)}'${delimiter}`); 125 | }); 126 | 127 | rows.push(''); 128 | } else { 129 | rows.push(`export enum ${enum_.Identifier} {`); 130 | 131 | entries.forEach(([key, entry]) => { 132 | let classCommentRows = formatComment(entry.ExtraInfo, ' ') 133 | if (classCommentRows) { 134 | rows.push(classCommentRows); 135 | } 136 | if (config.numericEnums) { 137 | if (entry.Value == null) { 138 | rows.push(` ${key},`); 139 | } else { 140 | rows.push(` ${key} = ${entry.Value},`); 141 | } 142 | } else { 143 | rows.push(` ${key} = '${getEnumStringValue(key)}',`); 144 | } 145 | }); 146 | 147 | rows.push(`}\n`); 148 | } 149 | 150 | return rows; 151 | }; 152 | 153 | const formatComment = (extraInfo, indentation) => { 154 | if (!config.includeComments || !extraInfo || (!extraInfo.Obsolete && !extraInfo.Summary)) { 155 | return undefined; 156 | } 157 | 158 | let comment = ''; 159 | comment += `${indentation}/**\n`; 160 | 161 | if (extraInfo.Summary) { 162 | let commentLines = extraInfo.Summary.split(/\r?\n/); 163 | commentLines = commentLines.map((e) => { 164 | return `${indentation} * ${replaceCommentTags(e)}\n`; 165 | }) 166 | comment += commentLines.join(''); 167 | } 168 | if (extraInfo.Remarks) { 169 | comment += `${indentation} *\n`; 170 | comment += `${indentation} * @remarks\n`; 171 | let commentLines = extraInfo.Remarks.split(/\r?\n/); 172 | commentLines = commentLines.map((e) => { 173 | return `${indentation} * ${replaceCommentTags(e)}\n`; 174 | }) 175 | comment += commentLines.join(''); 176 | } 177 | 178 | if (extraInfo.Obsolete) { 179 | if (extraInfo.Summary) { 180 | comment += `${indentation} *\n`; 181 | } 182 | 183 | let obsoleteMessage = ''; 184 | if (extraInfo.ObsoleteMessage) { 185 | obsoleteMessage = ' ' + replaceCommentTags(extraInfo.ObsoleteMessage); 186 | } 187 | comment += `${indentation} * @deprecated${obsoleteMessage}\n`; 188 | } 189 | 190 | comment += `${indentation} */`; 191 | 192 | return comment; 193 | } 194 | 195 | const replaceCommentTags = comment => { 196 | return comment 197 | .replace(//gi, '{@link $1}') 198 | .replace(/(.+)<\/see>/gi, '{@link $1 | $2}') 199 | .replace('', '@inheritDoc'); 200 | } 201 | 202 | const convertProperty = property => { 203 | const optional = property.Type.endsWith('?') || (config.validateEmitDefaultValue && 204 | property.ExtraInfo != null && 205 | !property.ExtraInfo.EmitDefaultValue); 206 | const identifier = convertIdentifier(optional ? `${property.Identifier.split(' ')[0]}?` : property.Identifier.split(' ')[0]); 207 | 208 | const type = parseType(property.Type); 209 | 210 | return `${identifier}: ${type}`; 211 | }; 212 | 213 | const convertIndexType = indexType => { 214 | const dictionary = indexType.match(dictionaryRegex); 215 | const simpleDictionary = indexType.match(simpleDictionaryRegex); 216 | 217 | propType = simpleDictionary ? dictionary[2] : parseType(dictionary[2]); 218 | 219 | return `[key: ${convertType(dictionary[1])}]: ${convertType(propType)}`; 220 | }; 221 | 222 | const convertRecord = indexType => { 223 | const dictionary = indexType.match(dictionaryRegex); 224 | const simpleDictionary = indexType.match(simpleDictionaryRegex); 225 | 226 | propType = simpleDictionary ? dictionary[2] : parseType(dictionary[2]); 227 | 228 | return `Record<${convertType(dictionary[1])}, ${convertType(propType)}>`; 229 | }; 230 | 231 | const parseType = propType => { 232 | const array = propType.match(arrayRegex); 233 | if (array) { 234 | propType = array[1]; 235 | } 236 | 237 | const collection = propType.match(collectionRegex); 238 | const dictionary = propType.match(dictionaryRegex); 239 | 240 | let type; 241 | 242 | if (collection) { 243 | const simpleCollection = propType.match(simpleCollectionRegex); 244 | propType = simpleCollection ? collection[1] : parseType(collection[1]); 245 | type = `${convertType(propType)}[]`; 246 | } else if (dictionary) { 247 | type = `${convertRecord(propType)}`; 248 | } else { 249 | const optional = propType.endsWith('?'); 250 | type = convertType(optional ? propType.slice(0, propType.length - 1) : propType); 251 | } 252 | 253 | return array ? `${type}[]` : type; 254 | }; 255 | 256 | const convertIdentifier = identifier => config.camelCase ? camelcase(identifier, config.camelCaseOptions) : identifier; 257 | const convertType = type => type in typeTranslations ? typeTranslations[type] : type; 258 | 259 | return convert; 260 | }; 261 | 262 | module.exports = createConverter; 263 | -------------------------------------------------------------------------------- /lib/csharp-models-to-json_test/ModelCollector_test.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using NUnit.Framework; 5 | 6 | namespace CSharpModelsToJson.Tests 7 | { 8 | [TestFixture] 9 | public class ModelCollectorTest 10 | { 11 | [Test] 12 | public void BasicInheritance_ReturnsInheritedClass() 13 | { 14 | var tree = CSharpSyntaxTree.ParseText(@" 15 | public class A : B, C, D 16 | { 17 | public void AMember() 18 | { 19 | } 20 | }" 21 | ); 22 | 23 | var root = (CompilationUnitSyntax)tree.GetRoot(); 24 | 25 | var modelCollector = new ModelCollector(); 26 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First()); 27 | 28 | Assert.That(modelCollector.Models, Is.Not.Null); 29 | Assert.That(modelCollector.Models.First().BaseClasses, Is.EqualTo(new[] { "B", "C", "D" })); 30 | } 31 | 32 | [Test] 33 | public void InterfaceImport_ReturnsSyntaxClassFromInterface() 34 | { 35 | var tree = CSharpSyntaxTree.ParseText(@" 36 | public interface IPhoneNumber { 37 | string Label { get; set; } 38 | string Number { get; set; } 39 | int MyProperty { get; set; } 40 | } 41 | 42 | public interface IPoint 43 | { 44 | // Property signatures: 45 | int x 46 | { 47 | get; 48 | set; 49 | } 50 | 51 | int y 52 | { 53 | get; 54 | set; 55 | } 56 | } 57 | 58 | 59 | public class X { 60 | public IPhoneNumber test { get; set; } 61 | public IPoint test2 { get; set; } 62 | }" 63 | ); 64 | 65 | var root = (CompilationUnitSyntax)tree.GetRoot(); 66 | 67 | var modelCollector = new ModelCollector(); 68 | modelCollector.Visit(root); 69 | 70 | Assert.That(modelCollector.Models, Is.Not.Null); 71 | Assert.That(modelCollector.Models.Count, Is.EqualTo(3)); 72 | Assert.That(modelCollector.Models.First().Properties.Count(), Is.EqualTo(3)); 73 | } 74 | 75 | 76 | [Test] 77 | public void TypedInheritance_ReturnsInheritance() 78 | { 79 | var tree = CSharpSyntaxTree.ParseText(@" 80 | public class A : IController 81 | { 82 | public void AMember() 83 | { 84 | } 85 | }" 86 | ); 87 | 88 | var root = (CompilationUnitSyntax)tree.GetRoot(); 89 | 90 | var modelCollector = new ModelCollector(); 91 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First()); 92 | 93 | Assert.That(modelCollector.Models, Is.Not.Null); 94 | Assert.That(modelCollector.Models.First().BaseClasses, Is.EqualTo(new[] { "IController" })); 95 | } 96 | 97 | [Test] 98 | public void AccessibilityRespected_ReturnsPublicOnly() 99 | { 100 | var tree = CSharpSyntaxTree.ParseText(@" 101 | public class A : IController 102 | { 103 | const int A_Constant = 0; 104 | 105 | private string B { get; set } 106 | 107 | static string C { get; set } 108 | 109 | public string Included { get; set } 110 | 111 | public void AMember() 112 | { 113 | } 114 | }" 115 | ); 116 | 117 | var root = (CompilationUnitSyntax)tree.GetRoot(); 118 | 119 | var modelCollector = new ModelCollector(); 120 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First()); 121 | 122 | Assert.That(modelCollector.Models, Is.Not.Null); 123 | Assert.That(modelCollector.Models.First().Properties, Is.Not.Null); 124 | Assert.That(modelCollector.Models.First().Properties.Count(), Is.EqualTo(1)); 125 | } 126 | 127 | [Test] 128 | public void IgnoresJsonIgnored_ReturnsOnlyNotIgnored() 129 | { 130 | var tree = CSharpSyntaxTree.ParseText(@" 131 | public class A : IController 132 | { 133 | const int A_Constant = 0; 134 | 135 | private string B { get; set } 136 | 137 | static string C { get; set } 138 | 139 | public string Included { get; set } 140 | 141 | [JsonIgnore] 142 | public string Ignored { get; set; } 143 | 144 | [IgnoreDataMember] 145 | public string Ignored2 { get; set; } 146 | 147 | public void AMember() 148 | { 149 | } 150 | }" 151 | ); 152 | 153 | var root = (CompilationUnitSyntax)tree.GetRoot(); 154 | 155 | var modelCollector = new ModelCollector(); 156 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First()); 157 | 158 | Assert.That(modelCollector.Models, Is.Not.Null); 159 | Assert.That(modelCollector.Models.First().Properties, Is.Not.Null); 160 | Assert.That(modelCollector.Models.First().Properties.Count(), Is.EqualTo(1)); 161 | 162 | } 163 | 164 | [Test] 165 | public void DictionaryInheritance_ReturnsIndexAccessor() 166 | { 167 | var tree = CSharpSyntaxTree.ParseText(@"public class A : Dictionary { }"); 168 | 169 | var root = (CompilationUnitSyntax)tree.GetRoot(); 170 | 171 | var modelCollector = new ModelCollector(); 172 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First()); 173 | 174 | Assert.That(modelCollector.Models, Is.Not.Null); 175 | Assert.That(modelCollector.Models.First().BaseClasses, Is.Not.Null); 176 | Assert.That(modelCollector.Models.First().BaseClasses, Is.EqualTo(new[] { "Dictionary" })); 177 | } 178 | 179 | [Test] 180 | public void ReturnObsoleteClassInfo() 181 | { 182 | var tree = CSharpSyntaxTree.ParseText(@" 183 | [Obsolete(@""test"")] 184 | public class A 185 | { 186 | [Obsolete(@""test prop"")] 187 | public string A { get; set } 188 | 189 | public string B { get; set } 190 | }" 191 | ); 192 | 193 | var root = (CompilationUnitSyntax)tree.GetRoot(); 194 | 195 | var modelCollector = new ModelCollector(); 196 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First()); 197 | 198 | var model = modelCollector.Models.First(); 199 | 200 | Assert.That(model, Is.Not.Null); 201 | Assert.That(model.Properties, Is.Not.Null); 202 | 203 | Assert.That(model.ExtraInfo.Obsolete, Is.True); 204 | Assert.That(model.ExtraInfo.ObsoleteMessage, Is.EqualTo("test")); 205 | 206 | Assert.That(model.Properties.First(x => x.Identifier.Equals("A")).ExtraInfo.Obsolete, Is.True); 207 | Assert.That(model.Properties.First(x => x.Identifier.Equals("A")).ExtraInfo.ObsoleteMessage, Is.EqualTo("test prop")); 208 | 209 | Assert.That(model.Properties.First(x => x.Identifier.Equals("B")).ExtraInfo.Obsolete, Is.False); 210 | Assert.That(model.Properties.First(x => x.Identifier.Equals("B")).ExtraInfo.ObsoleteMessage, Is.Null); 211 | } 212 | 213 | [Test] 214 | public void ReturnObsoleteEnumInfo() 215 | { 216 | var tree = CSharpSyntaxTree.ParseText(@" 217 | [Obsolete(@""test"")] 218 | public enum A 219 | { 220 | A = 0, 221 | B = 1, 222 | }" 223 | ); 224 | 225 | var root = (CompilationUnitSyntax)tree.GetRoot(); 226 | 227 | var enumCollector = new EnumCollector(); 228 | enumCollector.VisitEnumDeclaration(root.DescendantNodes().OfType().First()); 229 | 230 | var model = enumCollector.Enums.First(); 231 | 232 | Assert.That(model, Is.Not.Null) ; 233 | Assert.That(model.Values, Is.Not.Null); 234 | 235 | Assert.That(model.ExtraInfo.Obsolete, Is.True); 236 | Assert.That(model.ExtraInfo.ObsoleteMessage, Is.EqualTo("test")); 237 | } 238 | 239 | [Test] 240 | public void EnumBinaryValue() 241 | { 242 | var tree = CSharpSyntaxTree.ParseText(@" 243 | public enum A { 244 | A = 1, // decimal: 1 245 | B = 1_002, // decimal: 1002 246 | C = 0b011, // binary: 3 in decimal 247 | D = 0b_0000_0100, // binary: 4 in decimal 248 | E = 0x005, // hexadecimal: 5 in decimal 249 | F = 0x000_01a, // hexadecimal: 26 in decimal 250 | }" 251 | ); 252 | 253 | var root = (CompilationUnitSyntax)tree.GetRoot(); 254 | 255 | var enumCollector = new EnumCollector(); 256 | enumCollector.VisitEnumDeclaration(root.DescendantNodes().OfType().First()); 257 | 258 | var model = enumCollector.Enums.First(); 259 | 260 | Assert.That(model, Is.Not.Null); 261 | Assert.That(model.Values, Is.Not.Null); 262 | 263 | Assert.That(model.Values["A"].Value, Is.EqualTo("1")); 264 | Assert.That(model.Values["B"].Value, Is.EqualTo("1002")); 265 | Assert.That(model.Values["C"].Value, Is.EqualTo("0b011")); 266 | Assert.That(model.Values["D"].Value, Is.EqualTo("0b00000100")); 267 | Assert.That(model.Values["E"].Value, Is.EqualTo("0x005")); 268 | Assert.That(model.Values["F"].Value, Is.EqualTo("0x00001a")); 269 | } 270 | 271 | [Test] 272 | public void ReturnEmmitDefaultValueInfo() 273 | { 274 | var tree = CSharpSyntaxTree.ParseText(@" 275 | public class A 276 | { 277 | [DataMember(EmitDefaultValue = false)] 278 | public bool Prop1 { get; set; } 279 | 280 | [DataMember(EmitDefaultValue = true)] 281 | public bool Prop2 { get; set; } 282 | 283 | [DataMember( EmitDefaultValue = false )] 284 | public bool Prop3 { get; set; } 285 | 286 | [DataMember] 287 | public bool Prop4 { get; set; } 288 | 289 | public bool Prop5 { get; set; } 290 | }" 291 | ); 292 | 293 | var root = (CompilationUnitSyntax)tree.GetRoot(); 294 | 295 | var modelCollector = new ModelCollector(); 296 | modelCollector.Visit(root); 297 | 298 | Assert.That(modelCollector.Models, Is.Not.Null); 299 | Assert.That(modelCollector.Models.Count, Is.EqualTo(1)); 300 | 301 | var properties = modelCollector.Models.First().Properties; 302 | 303 | Assert.That(properties.First(x => x.Identifier == "Prop1").ExtraInfo.EmitDefaultValue, Is.False); 304 | Assert.That(properties.First(x => x.Identifier == "Prop2").ExtraInfo.EmitDefaultValue, Is.True); 305 | Assert.That(properties.First(x => x.Identifier == "Prop3").ExtraInfo.EmitDefaultValue, Is.False); 306 | Assert.That(properties.First(x => x.Identifier == "Prop4").ExtraInfo.EmitDefaultValue, Is.True); 307 | Assert.That(properties.First(x => x.Identifier == "Prop5").ExtraInfo.EmitDefaultValue, Is.True); 308 | } 309 | 310 | } 311 | } --------------------------------------------------------------------------------