├── .config └── dotnet-tools.json ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── settings.vscode.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── .paket └── Paket.Restore.targets ├── .travis.yml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── BinaryDefense.FSharp.Analyzers.sln ├── Directory.Build.props ├── LICENSE.md ├── README.md ├── RELEASE_NOTES.md ├── appveyor.yml ├── build.cmd ├── build.fsx ├── build.sh ├── docsSrc ├── Explanations │ └── Background.md ├── How_Tos │ ├── Doing_A_Thing.md │ └── Doing_Another_Thing.md ├── Tutorials │ └── Getting_Started.md ├── content │ ├── cleanups.js │ ├── hotload.js │ ├── style.css │ ├── submenu.js │ ├── themes.js │ ├── tips.js │ ├── toggle-bootstrap-dark.min.css │ └── toggle-bootstrap.min.css ├── files │ └── placeholder.md └── index.md ├── docsTool ├── CLI.fs ├── Prelude.fs ├── Program.fs ├── README.md ├── docsTool.fsproj ├── paket.references └── templates │ ├── helpers.fs │ ├── master.fs │ ├── modules.fs │ ├── namespaces.fs │ ├── nav.fs │ ├── partMembers.fs │ ├── partNested.fs │ └── types.fs ├── global.json ├── paket.dependencies ├── paket.lock ├── src ├── BinaryDefense.FSharp.Analyzers.Hashing │ ├── AssemblyInfo.fs │ ├── BinaryDefense.FSharp.Analyzers.Hashing.fsproj │ ├── Library.fs │ └── paket.references └── Directory.Build.props └── tests ├── BinaryDefense.FSharp.Analyzers.Tests ├── Analyzer.fs ├── AssemblyInfo.fs ├── BinaryDefense.FSharp.Analyzers.Tests.fsproj ├── Main.fs ├── Tests.fs └── paket.references ├── Directory.Build.props └── examples ├── Directory.Build.props └── hashing ├── AssemblyInfo.fs ├── hashing.fsproj ├── md5CryptoServicebinding.fs ├── md5create.fs ├── sha1CryptoServicebinding.fs ├── sha1CryptoServicector.fs └── sha1create.fs /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "sourcelink": { 6 | "version": "3.1.1", 7 | "commands": [ 8 | "sourcelink" 9 | ] 10 | }, 11 | "dotnet-reportgenerator-globaltool": { 12 | "version": "4.2.15", 13 | "commands": [ 14 | "reportgenerator" 15 | ] 16 | }, 17 | "fake-cli": { 18 | "version": "5.20.4-alpha.1642", 19 | "commands": [ 20 | "fake" 21 | ] 22 | }, 23 | "paket": { 24 | "version": "6.0.0-beta8", 25 | "commands": [ 26 | "paket" 27 | ] 28 | }, 29 | "fcswatch-cli": { 30 | "version": "0.7.14", 31 | "commands": [ 32 | "fcswatch" 33 | ] 34 | }, 35 | "fsharp-analyzers": { 36 | "version": "0.4.0", 37 | "commands": [ 38 | "fsharp-analyzers" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fsharp:10.2.3-netcore 2 | 3 | # Copy endpoint specific user settings into container to specify 4 | # .NET Core should be used as the runtime. 5 | COPY settings.vscode.json /root/.vscode-remote/data/Machine/settings.json 6 | 7 | # Install git, process tools 8 | RUN apt-get update && apt-get -y install git procps 9 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MiniScaffold", 3 | "dockerFile": "Dockerfile", 4 | "appPort": [8080], 5 | "extensions": [ 6 | "ionide.ionide-fsharp", 7 | "ms-vscode.csharp", 8 | "editorconfig.editorconfig", 9 | "ionide.ionide-paket", 10 | "ionide.ionide-fake" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.devcontainer/settings.vscode.json: -------------------------------------------------------------------------------- 1 | { 2 | "FSharp.fsacRuntime":"netcore" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: 2 | http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Default settings: 8 | # A newline ending every file 9 | # Use 4 spaces as indentation 10 | [*] 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{fs,fsi,fsx,config}] 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | 19 | [paket.*] 20 | trim_trailing_whitespace = true 21 | indent_size = 2 22 | 23 | [*.paket.references] 24 | trim_trailing_whitespace = true 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.vb diff=csharp text=auto eol=lf 7 | *.fs diff=csharp text=auto eol=lf 8 | *.fsi diff=csharp text=auto eol=lf 9 | *.fsx diff=csharp text=auto eol=lf 10 | *.sln text eol=crlf merge=union 11 | *.csproj merge=union 12 | *.vbproj merge=union 13 | *.fsproj merge=union 14 | *.dbproj merge=union 15 | *.sh text eol=lf 16 | 17 | # Standard to msysgit 18 | *.doc diff=astextplain 19 | *.DOC diff=astextplain 20 | *.docx diff=astextplain 21 | *.DOCX diff=astextplain 22 | *.dot diff=astextplain 23 | *.DOT diff=astextplain 24 | *.pdf diff=astextplain 25 | *.PDF diff=astextplain 26 | *.rtf diff=astextplain 27 | *.RTF diff=astextplain 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please insert a description of your problem or question. 4 | 5 | ## Error messages, screenshots 6 | 7 | Please add any error logs or screenshots if available. 8 | 9 | ## Failing test, failing GitHub repo, or reproduction steps 10 | 11 | Please add either a failing test, a GitHub repo of the problem or detailed reproduction steps. 12 | 13 | ## Expected Behavior 14 | 15 | Please define what you would expect the behavior to be like. 16 | 17 | ## Known workarounds 18 | 19 | Please provide a description of any known workarounds. 20 | 21 | ## Other information 22 | 23 | * Operating System: 24 | - [ ] windows [insert version here] 25 | - [ ] macOs [insert version] 26 | - [ ] linux [insert flavor/version here] 27 | * Platform 28 | - [ ] dotnet core 29 | - [ ] dotnet full 30 | - [ ] mono 31 | * Branch or release version: 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to BinaryDefense.FSharp.Analyzers? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] Build and tests pass locally 20 | - [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) 21 | - [ ] I have added necessary documentation (if appropriate) 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build master 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, windows-latest, macOS-latest] 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use .NET Core 2.1 SDK 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: '2.1.x' 18 | - name: Use .NET Core 3.1 SDK 19 | uses: actions/setup-dotnet@v1 20 | with: 21 | dotnet-version: '3.1.x' 22 | - name: Use .NET Core 5.0.200 SDK 23 | uses: actions/setup-dotnet@v1 24 | with: 25 | dotnet-version: '5.0.200' 26 | # Not specifying a version will attempt to install via global.json 27 | - name: Use .NET Core global.json 28 | uses: actions/setup-dotnet@v1 29 | 30 | - name: Build 31 | if: runner.os != 'Windows' 32 | run: | 33 | chmod +x ./build.sh 34 | ./build.sh 35 | env: 36 | # Work around https://github.com/actions/setup-dotnet/issues/29 37 | DOTNET_ROOT: ${{ runner.tool_cache }}/dncs/${{ matrix.dotnet }}/x64 38 | CI: true 39 | - name: Build 40 | if: runner.os == 'Windows' 41 | run: ./build.cmd 42 | env: 43 | # Work around https://github.com/actions/setup-dotnet/issues/29 44 | DOTNET_ROOT: ${{ runner.tool_cache }}/dncs/${{ matrix.dotnet }}/x64 45 | CI: true 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | packages/ 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | TestResults.xml 255 | 256 | # NuGet packages distributables 257 | dist/ 258 | 259 | # Ionide cache 260 | .ionide/ 261 | 262 | # Test coverage files 263 | coverage.xml 264 | coverage.*.xml 265 | 266 | # Paket tool store 267 | .paket/.store 268 | .paket/paket 269 | 270 | .fake 271 | .ionide -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | 241 | 242 | %(PaketReferencesFileLinesInfo.PackageVersion) 243 | All 244 | runtime 245 | runtime 246 | true 247 | true 248 | 249 | 250 | 251 | 252 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 262 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 263 | 264 | 265 | %(PaketCliToolFileLinesInfo.PackageVersion) 266 | 267 | 268 | 269 | 273 | 274 | 275 | 276 | 277 | 278 | false 279 | 280 | 281 | 282 | 283 | 284 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 285 | 286 | 287 | 288 | 289 | 290 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 291 | true 292 | false 293 | true 294 | false 295 | true 296 | false 297 | true 298 | false 299 | true 300 | $(PaketIntermediateOutputPath)\$(Configuration) 301 | $(PaketIntermediateOutputPath) 302 | 303 | 304 | 305 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 363 | 364 | 407 | 408 | 450 | 451 | 492 | 493 | 494 | 495 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: required 3 | dist: xenial 4 | 5 | dotnet: 3.0.200 6 | mono: 7 | - latest # => "stable release" 8 | - alpha 9 | - beta 10 | - weekly # => "latest commits" 11 | os: 12 | - linux 13 | 14 | script: 15 | - ./build.sh 16 | 17 | matrix: 18 | fast_finish: true 19 | allow_failures: 20 | - mono: alpha 21 | - mono: beta 22 | - mono: weekly 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ionide.ionide-paket", 4 | "ionide.ionide-fsharp", 5 | "ionide.ionide-fake", 6 | "ms-vscode.csharp", 7 | "editorConfig.editorConfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "WARNING01": "*********************************************************************************", 12 | "WARNING02": "The C# extension was unable to automatically decode projects in the current", 13 | "WARNING03": "workspace to create a runnable launch.json file. A template launch.json file has", 14 | "WARNING04": "been created as a placeholder.", 15 | "WARNING05": "", 16 | "WARNING06": "If OmniSharp is currently unable to load your project, you can attempt to resolve", 17 | "WARNING07": "this by restoring any missing project dependencies (example: run 'dotnet restore')", 18 | "WARNING08": "and by fixing any reported errors from building the projects in your workspace.", 19 | "WARNING09": "If this allows OmniSharp to now load your project then --", 20 | "WARNING10": " * Delete this file", 21 | "WARNING11": " * Open the Visual Studio Code command palette (View->Command Palette)", 22 | "WARNING12": " * run the command: '.NET: Generate Assets for Build and Debug'.", 23 | "WARNING13": "", 24 | "WARNING14": "If your project requires a more complex launch configuration, you may wish to delete", 25 | "WARNING15": "this configuration and pick a different template using the 'Add Configuration...'", 26 | "WARNING16": "button at the bottom of this file.", 27 | "WARNING17": "*********************************************************************************", 28 | "preLaunchTask": "build", 29 | "program": "${workspaceFolder}/bin/Debug//.dll", 30 | "args": [], 31 | "cwd": "${workspaceFolder}", 32 | "console": "internalConsole", 33 | "stopAtEntry": false 34 | }, 35 | { 36 | "name": ".NET Core Attach", 37 | "type": "coreclr", 38 | "request": "attach", 39 | "processId": "${command:pickProcess}" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "tab.unfocusedActiveBorder": "#fff0" 4 | } 5 | } -------------------------------------------------------------------------------- /BinaryDefense.FSharp.Analyzers.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C397A34C-84F1-49E7-AEBC-2F9F2B196216}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ACBEE43C-7A88-4FB1-9B06-DB064D22B29F}" 9 | EndProject 10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BinaryDefense.FSharp.Analyzers.Tests", "tests\BinaryDefense.FSharp.Analyzers.Tests\BinaryDefense.FSharp.Analyzers.Tests.fsproj", "{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "docsTool", "docsTool\docsTool.fsproj", "{8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BinaryDefense.FSharp.Analyzers.Hashing", "src\BinaryDefense.FSharp.Analyzers.Hashing\BinaryDefense.FSharp.Analyzers.Hashing.fsproj", "{AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{4CA3F686-DFAB-4AAF-8004-4158E18EF24E}" 17 | EndProject 18 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "hashing", "tests\examples\hashing\hashing.fsproj", "{8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Debug|x64 = Debug|x64 24 | Debug|x86 = Debug|x86 25 | Release|Any CPU = Release|Any CPU 26 | Release|x64 = Release|x64 27 | Release|x86 = Release|x86 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.ActiveCfg = Debug|x64 36 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.Build.0 = Debug|x64 37 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.ActiveCfg = Debug|x86 38 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.Build.0 = Debug|x86 39 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.ActiveCfg = Release|x64 42 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.Build.0 = Release|x64 43 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.ActiveCfg = Release|x86 44 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.Build.0 = Release|x86 45 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x64.ActiveCfg = Debug|Any CPU 48 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x64.Build.0 = Debug|Any CPU 49 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Debug|x86.Build.0 = Debug|Any CPU 51 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x64.ActiveCfg = Release|Any CPU 54 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x64.Build.0 = Release|Any CPU 55 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x86.ActiveCfg = Release|Any CPU 56 | {8855EC73-F6A1-43D3-AFBC-04A3E09F9BD9}.Release|x86.Build.0 = Release|Any CPU 57 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Debug|x64.ActiveCfg = Debug|Any CPU 60 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Debug|x64.Build.0 = Debug|Any CPU 61 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Debug|x86.ActiveCfg = Debug|Any CPU 62 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Debug|x86.Build.0 = Debug|Any CPU 63 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Release|x64.ActiveCfg = Release|Any CPU 66 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Release|x64.Build.0 = Release|Any CPU 67 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Release|x86.ActiveCfg = Release|Any CPU 68 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD}.Release|x86.Build.0 = Release|Any CPU 69 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Debug|x64.ActiveCfg = Debug|Any CPU 72 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Debug|x64.Build.0 = Debug|Any CPU 73 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Debug|x86.ActiveCfg = Debug|Any CPU 74 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Debug|x86.Build.0 = Debug|Any CPU 75 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Release|x64.ActiveCfg = Release|Any CPU 78 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Release|x64.Build.0 = Release|Any CPU 79 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Release|x86.ActiveCfg = Release|Any CPU 80 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E}.Release|x86.Build.0 = Release|Any CPU 81 | EndGlobalSection 82 | GlobalSection(NestedProjects) = preSolution 83 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F} 84 | {AAFC4496-746A-44AC-A4B1-CECC4ADED6CD} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} 85 | {4CA3F686-DFAB-4AAF-8004-4158E18EF24E} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F} 86 | {8FC9B600-2A16-4E0C-89D7-F61F5F315F3E} = {4CA3F686-DFAB-4AAF-8004-4158E18EF24E} 87 | EndGlobalSection 88 | EndGlobal 89 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | f#, fsharp 5 | https://github.com/TheAngryByrd/BinaryDefense.FSharp.Analyzers 6 | https://github.com/TheAngryByrd/BinaryDefense.FSharp.Analyzers/blob/master/LICENSE.md 7 | false 8 | git 9 | TheAngryByrd 10 | https://github.com/TheAngryByrd/BinaryDefense.FSharp.Analyzers 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BinaryDefense.FSharp.Analyzers 2 | 3 | ## What? 4 | 5 | This is a set of [security analyzers](https://owasp.org/www-community/Source_Code_Analysis_Tools) for the [FSharp Language](https://fsharp.org/) using the [FSharp Analyzers SDK](https://github.com/ionide/FSharp.Analyzers.SDK). 6 | 7 | ### Currently supported analyzers 8 | 9 | * Hashing 10 | * Looks for MD5 creation 11 | * Looks for SHA1 creation 12 | 13 | 14 | ## Why? 15 | 16 | Detecting security issues early in your codebase can save your company from embarrassment or financial repercussions. 17 | 18 | Also, there's growing need for security based tools in the FSharp ecosystem. Many tools cover CSharp projects but not FSharp. This project seeks to remedy that. 19 | 20 | ## How? 21 | 22 | ### 1 - Install the analyzer using paket 23 | 24 | Use paket to install the analyzer into a specialized Analyzers dependency group like this: 25 | 26 | ```sh 27 | paket add BinaryDefense.FSharp.Analyzers.Hashing --group Analyzers 28 | ``` 29 | 30 | DO NOT use storage:none because we want the analyzer package to be downloaded physically into packages/analyzers directory. 31 | 32 | ### 2.a - Enable analyzers in Ionide 33 | 34 | Make sure you have these settings in Ionide for FSharp 35 | 36 | ```json 37 | { 38 | "FSharp.enableAnalyzers": true, 39 | "FSharp.analyzersPath": [ 40 | "./packages/analyzers" 41 | ] 42 | } 43 | ``` 44 | 45 | ### 2.b - Install the fsharp-analyzers tool 46 | 47 | ```sh 48 | dotnet tool add fsharp-analyzers 49 | dotnet tool restore 50 | ``` 51 | 52 | Then run it against your project 53 | 54 | ```sh 55 | dotnet fsharp-analyzers --project ./src/MyLibrary/MyLibrary.fsproj 56 | ``` 57 | 58 | --- 59 | 60 | 66 | 67 | ## NuGet 68 | 69 | Package | Stable | Prerelease 70 | --- | --- | --- 71 | BinaryDefense.FSharp.Analyzers.Hashing | [![NuGet Badge](https://buildstats.info/nuget/BinaryDefense.FSharp.Analyzers.Hashing)](https://www.nuget.org/packages/BinaryDefense.FSharp.Analyzers.Hashing/) | [![NuGet Badge](https://buildstats.info/nuget/BinaryDefense.FSharp.Analyzers.Hashing?includePreReleases=true)](https://www.nuget.org/packages/BinaryDefense.FSharp.Analyzers.Hashing/) 72 | 73 | --- 74 | 75 | ### Developing 76 | 77 | Make sure the following **requirements** are installed on your system: 78 | 79 | - [dotnet SDK](https://www.microsoft.com/net/download/core) 3.0 or higher 80 | - [Mono](http://www.mono-project.com/) if you're on Linux or macOS. 81 | 82 | or 83 | 84 | - [VSCode Dev Container](https://code.visualstudio.com/docs/remote/containers) 85 | 86 | 87 | --- 88 | 89 | ### Environment Variables 90 | 91 | - `CONFIGURATION` will set the [configuration](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-build?tabs=netcore2x#options) of the dotnet commands. If not set, it will default to Release. 92 | - `CONFIGURATION=Debug ./build.sh` will result in `-c` additions to commands such as in `dotnet build -c Debug` 93 | - `GITHUB_TOKEN` will be used to upload release notes and Nuget packages to GitHub. 94 | - Be sure to set this before releasing 95 | - `DISABLE_COVERAGE` Will disable running code coverage metrics. AltCover can have [severe performance degradation](https://github.com/SteveGilham/altcover/issues/57) so it's worth disabling when looking to do a quicker feedback loop. 96 | - `DISABLE_COVERAGE=1 ./build.sh` 97 | 98 | 99 | --- 100 | 101 | ### Building 102 | 103 | 104 | ```sh 105 | > build.cmd // on windows 106 | $ ./build.sh // on unix 107 | ``` 108 | 109 | The bin of your library should look similar to: 110 | 111 | ``` 112 | $ tree src/MyCoolNewLib/bin/ 113 | src/MyCoolNewLib/bin/ 114 | └── Debug 115 | ├── net461 116 | │   ├── FSharp.Core.dll 117 | │   ├── MyCoolNewLib.dll 118 | │   ├── MyCoolNewLib.pdb 119 | │   ├── MyCoolNewLib.xml 120 | └── netstandard2.1 121 | ├── MyCoolNewLib.deps.json 122 | ├── MyCoolNewLib.dll 123 | ├── MyCoolNewLib.pdb 124 | └── MyCoolNewLib.xml 125 | 126 | ``` 127 | 128 | --- 129 | 130 | ### Build Targets 131 | 132 | - `Clean` - Cleans artifact and temp directories. 133 | - `DotnetRestore` - Runs [dotnet restore](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-restore?tabs=netcore2x) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 134 | - [`DotnetBuild`](#Building) - Runs [dotnet build](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-build?tabs=netcore2x) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 135 | - `DotnetTest` - Runs [dotnet test](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=netcore21) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 136 | - `GenerateCoverageReport` - Code coverage is run during `DotnetTest` and this generates a report via [ReportGenerator](https://github.com/danielpalme/ReportGenerator). 137 | - `WatchTests` - Runs [dotnet watch](https://docs.microsoft.com/en-us/aspnet/core/tutorials/dotnet-watch?view=aspnetcore-3.0) with the test projects. Useful for rapid feedback loops. 138 | - `GenerateAssemblyInfo` - Generates [AssemblyInfo](https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualbasic.applicationservices.assemblyinfo?view=netframework-4.8) for libraries. 139 | - `DotnetPack` - Runs [dotnet pack](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-pack). This includes running [Source Link](https://github.com/dotnet/sourcelink). 140 | - `SourceLinkTest` - Runs a Source Link test tool to verify Source Links were properly generated. 141 | - `PublishToNuGet` - Publishes the NuGet packages generated in `DotnetPack` to NuGet via [paket push](https://fsprojects.github.io/Paket/paket-push.html). 142 | - `GitRelease` - Creates a commit message with the [Release Notes](https://fake.build/apidocs/v5/fake-core-releasenotes.html) and a git tag via the version in the `Release Notes`. 143 | - `GitHubRelease` - Publishes a [GitHub Release](https://help.github.com/en/articles/creating-releases) with the Release Notes and any NuGet packages. 144 | - `FormatCode` - Runs [Fantomas](https://github.com/fsprojects/fantomas) on the solution file. 145 | - `BuildDocs` - Generates Documentation from `docsSrc` and the [XML Documentation Comments](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/) from your libraries in `src`. 146 | - `WatchDocs` - Generates documentation and starts a webserver locally. It will rebuild and hot reload if it detects any changes made to `docsSrc` files, libraries in `src`, or the `docsTool` itself. 147 | - `ReleaseDocs` - Will stage, commit, and push docs generated in the `BuildDocs` target. 148 | - [`Release`](#Releasing) - Task that runs all release type tasks such as `PublishToNuGet`, `GitRelease`, `ReleaseDocs`, and `GitHubRelease`. Make sure to read [Releasing](#Releasing) to setup your environment correctly for releases. 149 | --- 150 | 151 | 152 | ### Releasing 153 | 154 | - [Start a git repo with a remote](https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/) 155 | 156 | ```sh 157 | git add . 158 | git commit -m "Scaffold" 159 | git remote add origin https://github.com/user/MyCoolNewLib.git 160 | git push -u origin master 161 | ``` 162 | 163 | - [Add your NuGet API key to paket](https://fsprojects.github.io/Paket/paket-config.html#Adding-a-NuGet-API-key) 164 | 165 | ```sh 166 | paket config add-token "https://www.nuget.org" 4003d786-cc37-4004-bfdf-c4f3e8ef9b3a 167 | ``` 168 | 169 | - [Create a GitHub OAuth Token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) 170 | - You can then set the `GITHUB_TOKEN` to upload release notes and artifacts to github 171 | - Otherwise it will fallback to username/password 172 | 173 | - Then update the `RELEASE_NOTES.md` with a new version, date, and release notes [ReleaseNotesHelper](https://fsharp.github.io/FAKE/apidocs/fake-releasenoteshelper.html) 174 | 175 | ```markdown 176 | #### 0.2.0 - 2017-04-20 177 | - FEATURE: Does cool stuff! 178 | - BUGFIX: Fixes that silly oversight 179 | ``` 180 | 181 | - You can then use the `Release` target. This will: 182 | - make a commit bumping the version: `Bump version to 0.2.0` and add the release notes to the commit 183 | - publish the package to NuGet 184 | - push a git tag 185 | 186 | ```sh 187 | ./build.sh Release 188 | ``` 189 | 190 | 191 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | #### 0.2.2 - 2021-05-23 2 | 3 | * CHANGES: [update to Analyzer SDK 0.8.0, FSharp.Core 5.0.1, FCS 39.0.0](https://github.com/BinaryDefense/BinaryDefense.FSharp.Analyzers/pull/7) 4 | 5 | #### 0.2.1 - 2021-01-21 6 | 7 | * CHANGES: [Update to FSharp.Analyzers.SDK 0.7.0](https://github.com/BinaryDefense/BinaryDefense.FSharp.Analyzers/pull/6) 8 | 9 | #### 0.2.0-beta002 - 2020-03-30 10 | 11 | * CHANGES: [Update to FSharp.Analyzers.SDK 0.4.1](https://github.com/BinaryDefense/BinaryDefense.FSharp.Analyzers/pull/4) 12 | 13 | #### 0.1.1 - 2020-03-30 14 | 15 | * MINOR: [Using type definitions FullName instead of hard coded strings]( List.exists((=)envvar) 40 | with 41 | | _ -> defaultValue 42 | 43 | //----------------------------------------------------------------------------- 44 | // Metadata and Configuration 45 | //----------------------------------------------------------------------------- 46 | 47 | let productName = "BinaryDefense.FSharp.Analyzers" 48 | let sln = "BinaryDefense.FSharp.Analyzers.sln" 49 | 50 | 51 | let srcCodeGlob = 52 | !! (__SOURCE_DIRECTORY__ @@ "src/**/*.fs") 53 | ++ (__SOURCE_DIRECTORY__ @@ "src/**/*.fsx") 54 | 55 | let testsCodeGlob = 56 | !! (__SOURCE_DIRECTORY__ @@ "tests/**/*.fs") 57 | ++ (__SOURCE_DIRECTORY__ @@ "tests/**/*.fsx") 58 | 59 | let srcGlob =__SOURCE_DIRECTORY__ @@ "src/**/*.??proj" 60 | let testsGlob = __SOURCE_DIRECTORY__ @@ "tests/**/*.??proj" 61 | 62 | let srcAndTest = 63 | !! srcGlob 64 | ++ testsGlob 65 | 66 | let distDir = __SOURCE_DIRECTORY__ @@ "dist" 67 | let distGlob = distDir @@ "*.nupkg" 68 | 69 | let coverageThresholdPercent = 1 70 | let coverageReportDir = __SOURCE_DIRECTORY__ @@ "docs" @@ "coverage" 71 | 72 | 73 | let docsDir = __SOURCE_DIRECTORY__ @@ "docs" 74 | let docsSrcDir = __SOURCE_DIRECTORY__ @@ "docsSrc" 75 | let docsToolDir = __SOURCE_DIRECTORY__ @@ "docsTool" 76 | 77 | let gitOwner = "BinaryDefense" 78 | let gitRepoName = "BinaryDefense.FSharp.Analyzers" 79 | 80 | let gitHubRepoUrl = sprintf "https://github.com/%s/%s" gitOwner gitRepoName 81 | 82 | let releaseBranch = "master" 83 | let releaseNotes = Fake.Core.ReleaseNotes.load "RELEASE_NOTES.md" 84 | 85 | let publishUrl = "https://www.nuget.org" 86 | 87 | let docsSiteBaseUrl = sprintf "https://%s.github.io/%s" gitOwner gitRepoName 88 | 89 | let disableCodeCoverage = environVarAsBoolOrDefault "DISABLE_COVERAGE" true 90 | 91 | let ``BD_NUGET_TOKEN`` = "BD_NUGET_TOKEN" 92 | let nugetApiKey = Environment.environVarOrNone ``BD_NUGET_TOKEN`` 93 | nugetApiKey |> Option.iter (fun n -> TraceSecrets.register ``BD_NUGET_TOKEN`` n) 94 | 95 | //----------------------------------------------------------------------------- 96 | // Helpers 97 | //----------------------------------------------------------------------------- 98 | 99 | let isRelease (targets : Target list) = 100 | targets 101 | |> Seq.map(fun t -> t.Name) 102 | |> Seq.exists ((=)"Release") 103 | 104 | let invokeAsync f = async { f () } 105 | 106 | let configuration (targets : Target list) = 107 | let defaultVal = if isRelease targets then "Release" else "Debug" 108 | match Environment.environVarOrDefault "CONFIGURATION" defaultVal with 109 | | "Debug" -> DotNet.BuildConfiguration.Debug 110 | | "Release" -> DotNet.BuildConfiguration.Release 111 | | config -> DotNet.BuildConfiguration.Custom config 112 | 113 | let failOnBadExitAndPrint (p : ProcessResult) = 114 | if p.ExitCode <> 0 then 115 | p.Errors |> Seq.iter Trace.traceError 116 | failwithf "failed with exitcode %d" p.ExitCode 117 | 118 | // CI Servers can have bizzare failures that have nothing to do with your code 119 | let rec retryIfInCI times fn = 120 | match Environment.environVarOrNone "CI" with 121 | | Some _ -> 122 | if times > 1 then 123 | try 124 | fn() 125 | with 126 | | _ -> retryIfInCI (times - 1) fn 127 | else 128 | fn() 129 | | _ -> fn() 130 | 131 | let isReleaseBranchCheck () = 132 | if Git.Information.getBranchName "" <> releaseBranch then failwithf "Not on %s. If you want to release please switch to this branch." releaseBranch 133 | 134 | 135 | module dotnet = 136 | let watch cmdParam program args = 137 | DotNet.exec cmdParam (sprintf "watch %s" program) args 138 | 139 | let run cmdParam args = 140 | DotNet.exec cmdParam "run" args 141 | 142 | let tool optionConfig command args = 143 | DotNet.exec optionConfig (sprintf "%s" command) args 144 | |> failOnBadExitAndPrint 145 | 146 | let reportgenerator optionConfig args = 147 | tool optionConfig "reportgenerator" args 148 | 149 | let sourcelink optionConfig args = 150 | tool optionConfig "sourcelink" args 151 | 152 | let fcswatch optionConfig args = 153 | tool optionConfig "fcswatch" args 154 | 155 | [] 156 | type private DisposableDirectory (directory : string) = 157 | static member Create() = 158 | let tempPath = IO.Path.Combine(IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")) 159 | Trace.tracefn "Creating disposable directory %s" tempPath 160 | IO.Directory.CreateDirectory tempPath |> ignore 161 | new DisposableDirectory(tempPath) 162 | member x.DirectoryInfo = IO.DirectoryInfo(directory) 163 | interface IDisposable with 164 | member x.Dispose() = 165 | Trace.tracefn "Deleting disposable directory %s" x.DirectoryInfo.FullName 166 | IO.Directory.Delete(x.DirectoryInfo.FullName,true) 167 | 168 | open DocsTool.CLIArgs 169 | module DocsTool = 170 | open Argu 171 | let buildparser = ArgumentParser.Create(programName = "docstool") 172 | let buildCLI = 173 | [ 174 | BuildArgs.SiteBaseUrl docsSiteBaseUrl 175 | BuildArgs.ProjectGlob srcGlob 176 | BuildArgs.DocsOutputDirectory docsDir 177 | BuildArgs.DocsSourceDirectory docsSrcDir 178 | BuildArgs.GitHubRepoUrl gitHubRepoUrl 179 | BuildArgs.ProjectName gitRepoName 180 | BuildArgs.ReleaseVersion releaseNotes.NugetVersion 181 | ] 182 | |> buildparser.PrintCommandLineArgumentsFlat 183 | 184 | let build () = 185 | dotnet.run (fun args -> 186 | { args with WorkingDirectory = docsToolDir } 187 | ) (sprintf " -- build %s" (buildCLI)) 188 | |> failOnBadExitAndPrint 189 | 190 | let watchparser = ArgumentParser.Create(programName = "docstool") 191 | let watchCLI = 192 | [ 193 | WatchArgs.ProjectGlob srcGlob 194 | WatchArgs.DocsSourceDirectory docsSrcDir 195 | WatchArgs.GitHubRepoUrl gitHubRepoUrl 196 | WatchArgs.ProjectName gitRepoName 197 | WatchArgs.ReleaseVersion releaseNotes.NugetVersion 198 | ] 199 | |> watchparser.PrintCommandLineArgumentsFlat 200 | 201 | let watch projectpath = 202 | dotnet.watch (fun args -> 203 | { args with WorkingDirectory = docsToolDir } 204 | ) "run" (sprintf "-- watch %s" (watchCLI)) 205 | |> failOnBadExitAndPrint 206 | 207 | //----------------------------------------------------------------------------- 208 | // Target Implementations 209 | //----------------------------------------------------------------------------- 210 | 211 | let clean _ = 212 | ["bin"; "temp" ; distDir; coverageReportDir] 213 | |> Shell.cleanDirs 214 | 215 | !! srcGlob 216 | ++ testsGlob 217 | |> Seq.collect(fun p -> 218 | ["bin";"obj"] 219 | |> Seq.map(fun sp -> IO.Path.GetDirectoryName p @@ sp )) 220 | |> Shell.cleanDirs 221 | 222 | [ 223 | "paket-files/paket.restore.cached" 224 | ] 225 | |> Seq.iter Shell.rm 226 | 227 | let dotnetRestore _ = 228 | [sln] 229 | |> Seq.map(fun dir -> fun () -> 230 | let args = 231 | [ 232 | sprintf "/p:PackageVersion=%s" releaseNotes.NugetVersion 233 | ] |> String.concat " " 234 | DotNet.restore(fun c -> 235 | { c with 236 | Common = 237 | c.Common 238 | |> DotNet.Options.withCustomParams 239 | (Some(args)) 240 | }) dir) 241 | |> Seq.iter(retryIfInCI 10) 242 | 243 | let replacements = 244 | [ "FsLibLog\\n", "BinaryDefense.Logging\n" 245 | "FsLibLog\\.", "BinaryDefense.Logging." ] 246 | 247 | let fslibLogGlobs = !! "paket-files/TheAngryByrd/FsLibLog/**/FsLibLog*.fs" 248 | 249 | let replaceTemplateFiles _ = 250 | replacements 251 | |> List.iter (fun (``match``, replace) -> 252 | Shell.regexReplaceInFilesWithEncoding 253 | ``match`` 254 | replace 255 | Text.Encoding.UTF8 (fslibLogGlobs)) 256 | 257 | 258 | let dotnetBuild ctx = 259 | let args = 260 | [ 261 | sprintf "/p:PackageVersion=%s" releaseNotes.NugetVersion 262 | "--no-restore" 263 | ] 264 | DotNet.build(fun c -> 265 | { c with 266 | Configuration = configuration (ctx.Context.AllExecutingTargets) 267 | Common = 268 | c.Common 269 | |> DotNet.Options.withAdditionalArgs args 270 | 271 | }) sln 272 | 273 | let dotnetTest ctx = 274 | let excludeCoverage = 275 | !! testsGlob 276 | |> Seq.map IO.Path.GetFileNameWithoutExtension 277 | |> String.concat "|" 278 | let args = 279 | [ 280 | "--no-build" 281 | sprintf "/p:AltCover=%b" (not disableCodeCoverage) 282 | sprintf "/p:AltCoverThreshold=%d" coverageThresholdPercent 283 | sprintf "/p:AltCoverAssemblyExcludeFilter=%s" excludeCoverage 284 | ] 285 | DotNet.test(fun c -> 286 | 287 | { c with 288 | Configuration = configuration (ctx.Context.AllExecutingTargets) 289 | Common = 290 | c.Common 291 | |> DotNet.Options.withAdditionalArgs args 292 | }) sln 293 | 294 | let generateCoverageReport _ = 295 | let coverageReports = 296 | !!"tests/**/coverage.*.xml" 297 | |> String.concat ";" 298 | let sourceDirs = 299 | !! srcGlob 300 | |> Seq.map Path.getDirectory 301 | |> String.concat ";" 302 | let independentArgs = 303 | [ 304 | sprintf "-reports:%s" coverageReports 305 | sprintf "-targetdir:%s" coverageReportDir 306 | // Add source dir 307 | sprintf "-sourcedirs:%s" sourceDirs 308 | // Ignore Tests and if AltCover.Recorder.g sneaks in 309 | sprintf "-assemblyfilters:\"%s\"" "-*.Tests;-AltCover.Recorder.g" 310 | sprintf "-Reporttypes:%s" "Html" 311 | ] 312 | let args = 313 | independentArgs 314 | |> String.concat " " 315 | dotnet.reportgenerator id args 316 | 317 | let watchTests _ = 318 | !! testsGlob 319 | |> Seq.map(fun proj -> fun () -> 320 | dotnet.watch 321 | (fun opt -> 322 | opt |> DotNet.Options.withWorkingDirectory (IO.Path.GetDirectoryName proj)) 323 | "test" 324 | "" 325 | |> ignore 326 | ) 327 | |> Seq.iter (invokeAsync >> Async.Catch >> Async.Ignore >> Async.Start) 328 | 329 | printfn "Press Ctrl+C (or Ctrl+Break) to stop..." 330 | let cancelEvent = Console.CancelKeyPress |> Async.AwaitEvent |> Async.RunSynchronously 331 | cancelEvent.Cancel <- true 332 | 333 | let generateAssemblyInfo _ = 334 | 335 | let (|Fsproj|Csproj|Vbproj|) (projFileName:string) = 336 | match projFileName with 337 | | f when f.EndsWith("fsproj") -> Fsproj 338 | | f when f.EndsWith("csproj") -> Csproj 339 | | f when f.EndsWith("vbproj") -> Vbproj 340 | | _ -> failwith (sprintf "Project file %s not supported. Unknown project type." projFileName) 341 | 342 | let releaseChannel = 343 | match releaseNotes.SemVer.PreRelease with 344 | | Some pr -> pr.Name 345 | | _ -> "release" 346 | let getAssemblyInfoAttributes projectName = 347 | [ 348 | AssemblyInfo.Title (projectName) 349 | AssemblyInfo.Product productName 350 | AssemblyInfo.Version releaseNotes.AssemblyVersion 351 | AssemblyInfo.Metadata("ReleaseDate", releaseNotes.Date.Value.ToString("o")) 352 | AssemblyInfo.FileVersion releaseNotes.AssemblyVersion 353 | AssemblyInfo.InformationalVersion releaseNotes.AssemblyVersion 354 | AssemblyInfo.Metadata("ReleaseChannel", releaseChannel) 355 | AssemblyInfo.Metadata("GitHash", Git.Information.getCurrentSHA1(null)) 356 | ] 357 | 358 | let getProjectDetails (projectPath : string) = 359 | let projectName = IO.Path.GetFileNameWithoutExtension(projectPath) 360 | ( 361 | projectPath, 362 | projectName, 363 | IO.Path.GetDirectoryName(projectPath), 364 | (getAssemblyInfoAttributes projectName) 365 | ) 366 | 367 | srcAndTest 368 | |> Seq.map getProjectDetails 369 | |> Seq.iter (fun (projFileName, _, folderName, attributes) -> 370 | match projFileName with 371 | | Fsproj -> AssemblyInfoFile.createFSharp (folderName @@ "AssemblyInfo.fs") attributes 372 | | Csproj -> AssemblyInfoFile.createCSharp ((folderName @@ "Properties") @@ "AssemblyInfo.cs") attributes 373 | | Vbproj -> AssemblyInfoFile.createVisualBasic ((folderName @@ "My Project") @@ "AssemblyInfo.vb") attributes 374 | ) 375 | 376 | let dotnetPack ctx = 377 | 378 | // Analyzers need some additional work to bundle 3rd party dependencies: see https://github.com/ionide/FSharp.Analyzers.SDK#packaging-and-distribution 379 | let publishFramework = "net5.0" 380 | let args = 381 | [ 382 | sprintf "/p:PackageVersion=%s" releaseNotes.NugetVersion 383 | sprintf "/p:PackageReleaseNotes=\"%s\"" (releaseNotes.Notes |> String.concat "\n") 384 | ] 385 | !! srcGlob 386 | |> Seq.iter(fun proj -> 387 | let configuration = configuration (ctx.Context.AllExecutingTargets) 388 | DotNet.pack (fun c -> 389 | { c with 390 | Configuration = configuration 391 | OutputPath = Some distDir 392 | Common = 393 | c.Common 394 | |> DotNet.Options.withAdditionalArgs args 395 | }) proj 396 | 397 | DotNet.publish (fun c -> 398 | { c with 399 | Configuration = configuration 400 | Framework = Some publishFramework 401 | }) proj 402 | 403 | let nupkg = 404 | let projectName = IO.Path.GetFileNameWithoutExtension proj 405 | IO.Directory.GetFiles distDir 406 | |> Seq.filter(fun path -> path.Contains projectName) 407 | |> Seq.tryExactlyOne 408 | |> Option.defaultWith(fun () -> failwithf "Could not find corresponsiding nuget package with name containing %s" projectName ) 409 | |> IO.FileInfo 410 | 411 | let publishPath = IO.FileInfo(proj).Directory.FullName "bin" (string configuration) publishFramework "publish" 412 | 413 | use dd = DisposableDirectory.Create() 414 | // Unzip the nuget 415 | ZipFile.ExtractToDirectory(nupkg.FullName, dd.DirectoryInfo.FullName) 416 | // delete the initial nuget package 417 | nupkg.Delete() 418 | // remove stuff from ./lib/netcoreapp2.0 419 | Shell.deleteDir (dd.DirectoryInfo.FullName "lib" publishFramework) 420 | // move the output of publish folder into the ./lib/netcoreapp2.0 directory 421 | Shell.copyDir (dd.DirectoryInfo.FullName "lib" publishFramework) publishPath (fun _ -> true) 422 | // re-create the nuget package 423 | ZipFile.CreateFromDirectory(dd.DirectoryInfo.FullName, nupkg.FullName) 424 | ) 425 | 426 | 427 | let publishToNuget _ = 428 | isReleaseBranchCheck () 429 | Paket.push(fun c -> 430 | { c with 431 | ApiKey = Option.defaultValue c.ApiKey nugetApiKey 432 | ToolType = ToolType.CreateLocalTool() 433 | PublishUrl = publishUrl 434 | WorkingDir = "dist" 435 | } 436 | ) 437 | 438 | let gitRelease _ = 439 | isReleaseBranchCheck () 440 | 441 | let releaseNotesGitCommitFormat = releaseNotes.Notes |> Seq.map(sprintf "* %s\n") |> String.concat "" 442 | 443 | Git.Staging.stageAll "" 444 | Git.Commit.exec "" (sprintf "Bump version to %s \n%s" releaseNotes.NugetVersion releaseNotesGitCommitFormat) 445 | Git.Branches.push "" 446 | 447 | Git.Branches.tag "" releaseNotes.NugetVersion 448 | Git.Branches.pushTag "" "origin" releaseNotes.NugetVersion 449 | 450 | let githubRelease _ = 451 | let token = 452 | match Environment.environVarOrDefault "GITHUB_TOKEN" "" with 453 | | s when not (String.IsNullOrWhiteSpace s) -> s 454 | | _ -> failwith "please set the github_token environment variable to a github personal access token with repro access." 455 | 456 | let files = !! distGlob 457 | 458 | GitHub.createClientWithToken token 459 | |> GitHub.draftNewRelease gitOwner gitRepoName releaseNotes.NugetVersion (releaseNotes.SemVer.PreRelease <> None) releaseNotes.Notes 460 | |> GitHub.uploadFiles files 461 | |> GitHub.publishDraft 462 | |> Async.RunSynchronously 463 | 464 | let formatCode _ = 465 | [ 466 | srcCodeGlob 467 | testsCodeGlob 468 | ] 469 | |> Seq.collect id 470 | // Ignore AssemblyInfo 471 | |> Seq.filter(fun f -> f.EndsWith("AssemblyInfo.fs") |> not) 472 | |> formatFilesAsync 473 | |> Async.RunSynchronously 474 | |> Seq.iter(fun result -> 475 | match result with 476 | | Formatted(original, tempfile) -> 477 | tempfile |> Shell.copyFile original 478 | Trace.logfn "Formatted %s" original 479 | | _ -> () 480 | ) 481 | 482 | 483 | let buildDocs _ = 484 | DocsTool.build () 485 | 486 | let watchDocs _ = 487 | let watchBuild () = 488 | !! srcGlob 489 | |> Seq.map(fun proj -> fun () -> 490 | dotnet.watch 491 | (fun opt -> 492 | opt |> DotNet.Options.withWorkingDirectory (IO.Path.GetDirectoryName proj)) 493 | "build" 494 | "" 495 | |> ignore 496 | ) 497 | |> Seq.iter (invokeAsync >> Async.Catch >> Async.Ignore >> Async.Start) 498 | watchBuild () 499 | DocsTool.watch () 500 | 501 | let releaseDocs ctx = 502 | isReleaseBranchCheck () 503 | 504 | Git.Staging.stageAll docsDir 505 | Git.Commit.exec "" (sprintf "Documentation release of version %s" releaseNotes.NugetVersion) 506 | if isRelease (ctx.Context.AllExecutingTargets) |> not then 507 | // We only want to push if we're only calling "ReleaseDocs" target 508 | // If we're calling "Release" target, we'll let the "GitRelease" target do the git push 509 | Git.Branches.push "" 510 | 511 | 512 | //----------------------------------------------------------------------------- 513 | // Target Declaration 514 | //----------------------------------------------------------------------------- 515 | 516 | Target.create "Clean" clean 517 | Target.create "DotnetRestore" dotnetRestore 518 | Target.create "ReplaceTemplateFilesNamespace" replaceTemplateFiles 519 | Target.create "DotnetBuild" dotnetBuild 520 | Target.create "DotnetTest" dotnetTest 521 | Target.create "GenerateCoverageReport" generateCoverageReport 522 | Target.create "WatchTests" watchTests 523 | Target.create "GenerateAssemblyInfo" generateAssemblyInfo 524 | Target.create "DotnetPack" dotnetPack 525 | Target.create "PublishToNuGet" publishToNuget 526 | Target.create "GitRelease" gitRelease 527 | Target.create "GitHubRelease" githubRelease 528 | Target.create "FormatCode" formatCode 529 | Target.create "Release" ignore 530 | Target.create "BuildDocs" buildDocs 531 | Target.create "WatchDocs" watchDocs 532 | Target.create "ReleaseDocs" releaseDocs 533 | 534 | //----------------------------------------------------------------------------- 535 | // Target Dependencies 536 | //----------------------------------------------------------------------------- 537 | 538 | 539 | // Only call Clean if DotnetPack was in the call chain 540 | // Ensure Clean is called before DotnetRestore 541 | "Clean" ?=> "DotnetRestore" 542 | "Clean" ==> "DotnetPack" 543 | 544 | // Only call AssemblyInfo if Publish was in the call chain 545 | // Ensure AssemblyInfo is called after DotnetRestore and before DotnetBuild 546 | "DotnetRestore" ?=> "GenerateAssemblyInfo" 547 | "GenerateAssemblyInfo" ?=> "DotnetBuild" 548 | "GenerateAssemblyInfo" ==> "PublishToNuGet" 549 | "DotnetRestore" ==> "ReplaceTemplateFilesNamespace" 550 | "ReplaceTemplateFilesNamespace" ==> "DotnetBuild" 551 | "DotnetBuild" ==> "BuildDocs" 552 | "BuildDocs" ==> "ReleaseDocs" 553 | "BuildDocs" ?=> "PublishToNuget" 554 | "DotnetPack" ?=> "BuildDocs" 555 | "GenerateCoverageReport" ?=> "ReleaseDocs" 556 | 557 | "DotnetBuild" ==> "WatchDocs" 558 | 559 | "DotnetRestore" 560 | ==> "DotnetBuild" 561 | ==> "DotnetTest" 562 | =?> ("GenerateCoverageReport", not disableCodeCoverage) 563 | ==> "DotnetPack" 564 | ==> "PublishToNuGet" 565 | ==> "GitRelease" 566 | ==> "GitHubRelease" 567 | ==> "Release" 568 | 569 | "DotnetRestore" 570 | ==> "WatchTests" 571 | 572 | //----------------------------------------------------------------------------- 573 | // Target Start 574 | //----------------------------------------------------------------------------- 575 | 576 | Target.runOrDefaultWithArguments "DotnetPack" 577 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | echo "Restoring dotnet tools..." 7 | dotnet tool restore 8 | 9 | PAKET_SKIP_RESTORE_TARGETS=true FAKE_DETAILED_ERRORS=true dotnet fake build -t "$@" 10 | -------------------------------------------------------------------------------- /docsSrc/Explanations/Background.md: -------------------------------------------------------------------------------- 1 | # Background 2 | 3 | Here's a core concept and reasons why this exists at a deeper level. 4 | -------------------------------------------------------------------------------- /docsSrc/How_Tos/Doing_A_Thing.md: -------------------------------------------------------------------------------- 1 | # How To do this specific thing 2 | 3 | -------------------------------------------------------------------------------- /docsSrc/How_Tos/Doing_Another_Thing.md: -------------------------------------------------------------------------------- 1 | # How To do this specific thing 2 | 3 | -------------------------------------------------------------------------------- /docsSrc/Tutorials/Getting_Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ```fsharp 4 | let foo = () 5 | let myAge = 21 6 | ``` 7 | 8 | ## Here is the path to downloading 9 | 10 | [lang=bash] 11 | paket install BinaryDefense.FSharp.Analyzers 12 | 13 | 14 | -------------------------------------------------------------------------------- /docsSrc/content/cleanups.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // Makes code snippets responsive 3 | $("table").addClass("table-responsive"); 4 | }) 5 | 6 | -------------------------------------------------------------------------------- /docsSrc/content/hotload.js: -------------------------------------------------------------------------------- 1 | var refreshSocket = new WebSocket('ws://' + window.location.host) 2 | .onmessage = () => { 3 | location.reload(); 4 | } 5 | -------------------------------------------------------------------------------- /docsSrc/content/style.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Droid+Sans|Droid+Sans+Mono|Open+Sans:400,600,700); 2 | 3 | /*-------------------------------------------------------------------------- 4 | Formatting for F# code snippets 5 | /*--------------------------------------------------------------------------*/ 6 | 7 | /* strings --- and styles for other string related formats */ 8 | span.s { color:#E0E268; } 9 | /* printf formatters */ 10 | span.pf { color:#E0C57F; } 11 | /* escaped chars */ 12 | span.e { color:#EA8675; } 13 | 14 | /* identifiers --- and styles for more specific identifier types */ 15 | span.i { color:#d1d1d1; } 16 | /* type or module */ 17 | span.t { color:#43AEC6; } 18 | /* function */ 19 | span.f { color:#e1e1e1; } 20 | /* DU case or active pattern */ 21 | span.p { color:#4ec9b0; } 22 | 23 | /* keywords */ 24 | span.k { color:#FAB11D; } 25 | /* comment */ 26 | span.c { color:#808080; } 27 | /* operators */ 28 | span.o { color:#af75c1; } 29 | /* numbers */ 30 | span.n { color:#96C71D; } 31 | /* line number */ 32 | span.l { color:#80b0b0; } 33 | /* mutable var or ref cell */ 34 | span.v { color:#d1d1d1; font-weight: bold; } 35 | /* inactive code */ 36 | span.inactive { color:#808080; } 37 | /* preprocessor */ 38 | span.prep { color:#af75c1; } 39 | /* fsi output */ 40 | span.fsi { color:#808080; } 41 | 42 | /* omitted */ 43 | span.omitted { 44 | background:#3c4e52; 45 | border-radius:5px; 46 | color:#808080; 47 | padding:0px 0px 1px 0px; 48 | } 49 | /* tool tip */ 50 | div.tip { 51 | background:#475b5f; 52 | border-radius:4px; 53 | font:11pt 'Droid Sans', arial, sans-serif; 54 | padding:6px 8px 6px 8px; 55 | display:none; 56 | color:#d1d1d1; 57 | pointer-events:none; 58 | } 59 | table.pre pre { 60 | padding:0px; 61 | margin:0px; 62 | border:none; 63 | } 64 | table.pre, pre.fssnip, pre { 65 | line-height:13pt; 66 | /*border:1px solid #d8d8d8;*/ 67 | border:1px solid #000; 68 | /* border: none; */ 69 | border-collapse:separate; 70 | white-space:pre-wrap; 71 | font: 9pt 'Droid Sans Mono',consolas,monospace; 72 | width:90%; 73 | margin:10px 20px 20px 20px; 74 | background-color:#212d30; 75 | padding:10px; 76 | /*border-radius:5px;*/ 77 | color:#d1d1d1; 78 | max-width: none; 79 | } 80 | pre.fssnip code { 81 | font: 9pt 'Droid Sans Mono',consolas,monospace; 82 | } 83 | table.pre pre { 84 | padding:0px; 85 | margin:0px; 86 | border-radius:0px; 87 | width: 100%; 88 | } 89 | table.pre td { 90 | padding:0px; 91 | white-space:normal; 92 | margin:0px; 93 | } 94 | table.pre td.lines { 95 | width:30px; 96 | } 97 | 98 | .table thead td.fit, 99 | .table th.fit { 100 | white-space: nowrap; 101 | width: 1%; 102 | } 103 | /*-------------------------------------------------------------------------- 104 | Formatting for page & standard document content 105 | /*--------------------------------------------------------------------------*/ 106 | 107 | body { 108 | font-family: 'Open Sans', serif; 109 | } 110 | 111 | pre { 112 | word-wrap: inherit; 113 | } 114 | 115 | /* Format the heading - nicer spacing etc. */ 116 | .masthead { 117 | overflow: hidden; 118 | } 119 | .masthead .muted a { 120 | text-decoration:none; 121 | color:#999999; 122 | } 123 | .masthead ul, .masthead li { 124 | margin-bottom:0px; 125 | } 126 | .masthead .nav li { 127 | margin-top: 15px; 128 | font-size:110%; 129 | } 130 | .masthead h3 { 131 | margin-bottom:5px; 132 | font-size:170%; 133 | } 134 | hr { 135 | margin:0px 0px 20px 0px; 136 | } 137 | 138 | /* Make table headings and td.title bold */ 139 | td.title, thead { 140 | font-weight:bold; 141 | } 142 | 143 | /* Format the right-side menu */ 144 | #menu { 145 | margin-top:50px; 146 | font-size:11pt; 147 | padding-left:20px; 148 | } 149 | 150 | #menu .nav-header { 151 | font-size:12pt; 152 | color:#606060; 153 | margin-top:20px; 154 | } 155 | 156 | #menu li { 157 | line-height:25px; 158 | } 159 | 160 | .wrapper { 161 | margin-top: -56px; 162 | padding-top: 56px; 163 | } 164 | 165 | /* Change font sizes for headings etc. */ 166 | #main h1 { font-size: 26pt; margin:10px 0px 15px 0px; font-weight:400; } 167 | #main h2 { font-size: 20pt; margin:20px 0px 0px 0px; font-weight:400; } 168 | #main h3 { font-size: 14pt; margin:15px 0px 0px 0px; font-weight:600; } 169 | #main p { font-size: 11pt; margin:5px 0px 15px 0px; } 170 | #main ul { font-size: 11pt; margin-top:10px; } 171 | #main li { font-size: 11pt; margin: 5px 0px 5px 0px; } 172 | #main strong { font-weight:700; } 173 | 174 | /*-------------------------------------------------------------------------- 175 | Formatting for API reference 176 | /*--------------------------------------------------------------------------*/ 177 | 178 | .type-list .type-name, .module-list .module-name { 179 | width:25%; 180 | font-weight:bold; 181 | } 182 | .member-list .member-name { 183 | width:35%; 184 | } 185 | #main .xmldoc h2 { 186 | font-size:14pt; 187 | margin:10px 0px 0px 0px; 188 | } 189 | #main .xmldoc h3 { 190 | font-size:12pt; 191 | margin:10px 0px 0px 0px; 192 | } 193 | .github-link { 194 | float:right; 195 | text-decoration:none; 196 | } 197 | .github-link img { 198 | border-style:none; 199 | margin-left:10px; 200 | } 201 | .github-link .hover { display:none; } 202 | .github-link:hover .hover { display:block; } 203 | .github-link .normal { display: block; } 204 | .github-link:hover .normal { display: none; } 205 | 206 | /*-------------------------------------------------------------------------- 207 | Links 208 | /*--------------------------------------------------------------------------*/ 209 | 210 | .bootstrap h1 a, .bootstrap h1 a:hover, .bootstrap h1 a:focus, 211 | .bootstrap h2 a, .bootstrap h2 a:hover, .bootstrap h2 a:focus, 212 | .bootstrap h3 a, .bootstrap h3 a:hover, .bootstrap h3 a:focus, 213 | .bootstrap h4 a, .bootstrap h4 a:hover, .bootstrap h4 a:focus, 214 | .bootstrap h5 a, .bootstrap h5 a:hover, .bootstrap h5 a:focus, 215 | .bootstrap h6 a, .bootstrap h6 a:hover, .bootstrap h6 a:focus { color : inherit; text-decoration : inherit; outline:none } 216 | 217 | /*-------------------------------------------------------------------------- 218 | Additional formatting for the homepage 219 | /*--------------------------------------------------------------------------*/ 220 | 221 | #nuget { 222 | margin-top:20px; 223 | font-size: 11pt; 224 | padding:20px; 225 | } 226 | 227 | #nuget pre { 228 | font-size:11pt; 229 | -moz-border-radius: 0px; 230 | -webkit-border-radius: 0px; 231 | border-radius: 0px; 232 | background: #404040; 233 | border-style:none; 234 | color: #e0e0e0; 235 | margin-top:15px; 236 | } 237 | 238 | .date { 239 | font-style: italic; 240 | margin-bottom: 15px; 241 | } 242 | 243 | h1.header { 244 | color: green; 245 | } 246 | 247 | h1.header:hover { 248 | color: green; 249 | } 250 | 251 | h1.header:visited { 252 | color: green; 253 | } 254 | 255 | .categories, .category, .recent-posts { 256 | font-family: 'Droid Sans', arial, sans-serif; 257 | } 258 | 259 | .categories ul, 260 | .recent-posts ul { 261 | margin-left: 0; 262 | } 263 | .categories li, 264 | .category li, 265 | .recent-posts li 266 | { 267 | list-style-type: none; 268 | white-space: nowrap; 269 | } 270 | 271 | .links { 272 | text-align: center; 273 | margin-bottom: 8px; 274 | } 275 | 276 | .copyright { 277 | text-align: center; 278 | color: lightslategray; 279 | margin-bottom: 25px; 280 | } 281 | 282 | .social { 283 | margin-bottom: 30px; 284 | } 285 | 286 | /* Fixes page anchors with bootstrap navbar */ 287 | :target::before { 288 | display: block; 289 | height: 59px; 290 | margin-top: -59px; 291 | content: ""; 292 | } 293 | 294 | /* Hides first br from FSharp.Literate xml-doc rendering */ 295 | .comment-block > br:first-child, 296 | .xmldoc > br:first-child { 297 | display: none; 298 | } 299 | 300 | .main h1 { 301 | padding: .5em 0em 302 | } 303 | 304 | .main h2 { 305 | padding: .5em 0em 306 | } 307 | 308 | .dropdown-submenu { 309 | position: relative; 310 | } 311 | 312 | .dropdown-submenu>a:after { 313 | content: "\f0da"; 314 | padding-left: 5px; 315 | vertical-align: middle; 316 | border: none; 317 | font-weight: 900; 318 | font-family: 'Font Awesome 5 Free'; 319 | } 320 | 321 | .dropdown-submenu>.dropdown-menu { 322 | top: 0; 323 | left: 100%; 324 | margin-top: 0px; 325 | margin-left: 0px; 326 | } 327 | 328 | .fsharp-footer-logo { 329 | width: 20px; 330 | margin-top: -2px; 331 | -webkit-filter: grayscale(100%) brightness(0) invert(1); /* Safari 6.0 - 9.0 */ 332 | filter: grayscale(100%) brightness(0) invert(1); 333 | } 334 | -------------------------------------------------------------------------------- /docsSrc/content/submenu.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // ------------------------------------------------------- // 3 | // Multi Level dropdowns 4 | // ------------------------------------------------------ // 5 | $("ul.dropdown-menu [data-toggle='dropdown']").on("click", function(event) { 6 | event.preventDefault(); 7 | event.stopPropagation(); 8 | 9 | $(this).siblings().toggleClass("show"); 10 | 11 | 12 | if (!$(this).next().hasClass('show')) { 13 | $(this).parents('.dropdown-menu').first().find('.show').removeClass("show"); 14 | } 15 | $(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function(e) { 16 | $('.dropdown-submenu .show').removeClass("show"); 17 | }); 18 | 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /docsSrc/content/themes.js: -------------------------------------------------------------------------------- 1 | 2 | var themes = { 3 | "light" : { 4 | "button-text" : "Swap to Dark", 5 | "button-classes" : "btn btn-dark border-light", 6 | "next-theme" : "dark", 7 | "body-class" : "bootstrap" 8 | }, 9 | "dark" : { 10 | "button-text" : "Swap to Light", 11 | "button-classes" : "btn btn-light", 12 | "next-theme" : "light", 13 | "body-class" : "bootstrap-dark" 14 | } 15 | }; 16 | 17 | var themeStorageKey = 'theme'; 18 | 19 | function swapThemeInDom(theme) { 20 | var newTheme = themes[theme]; 21 | var bootstrapCSS = document.getElementsByTagName('body')[0]; 22 | bootstrapCSS.setAttribute('class', newTheme['body-class']) 23 | } 24 | 25 | function persistNewTheme(theme) { 26 | window.localStorage.setItem(themeStorageKey, theme); 27 | } 28 | 29 | function setToggleButton(theme) { 30 | var newTheme = themes[theme]; 31 | var themeToggleButton = document.getElementById('theme-toggle'); 32 | themeToggleButton.textContent = newTheme['button-text']; 33 | themeToggleButton.className = newTheme['button-classes']; 34 | themeToggleButton.onclick = function() { 35 | setTheme(newTheme['next-theme']); 36 | } 37 | } 38 | 39 | function setTheme(theme) { 40 | try { 41 | swapThemeInDom(theme); 42 | } 43 | catch(e){ 44 | } 45 | try { 46 | persistNewTheme(theme); 47 | } 48 | catch(e) { 49 | } 50 | try { 51 | setToggleButton(theme); 52 | } 53 | catch (e) { 54 | } 55 | } 56 | 57 | function getThemeFromStorage() { 58 | return window.localStorage.getItem(themeStorageKey); 59 | } 60 | 61 | function getThemeFromScheme() { 62 | try { 63 | if (window.matchMedia("(prefers-color-scheme: dark)").matches){ 64 | return 'dark'; 65 | } 66 | else { 67 | return 'light'; 68 | } 69 | } 70 | catch(e) { 71 | return null; 72 | } 73 | } 74 | 75 | function loadTheme() { 76 | var theme = getThemeFromStorage() || getThemeFromScheme() || 'light'; 77 | setTheme(theme); 78 | } 79 | 80 | document.addEventListener('readystatechange', (event) => { 81 | loadTheme() 82 | }); 83 | -------------------------------------------------------------------------------- /docsSrc/content/tips.js: -------------------------------------------------------------------------------- 1 | var currentTip = null; 2 | var currentTipElement = null; 3 | 4 | function hideTip(evt, name, unique) { 5 | var el = document.getElementById(name); 6 | el.style.display = "none"; 7 | currentTip = null; 8 | } 9 | 10 | function findPos(obj) { 11 | // no idea why, but it behaves differently in webbrowser component 12 | if (window.location.search == "?inapp") 13 | return [obj.offsetLeft + 10, obj.offsetTop + 30]; 14 | 15 | var curleft = 0; 16 | var curtop = obj.offsetHeight; 17 | while (obj) { 18 | curleft += obj.offsetLeft; 19 | curtop += obj.offsetTop; 20 | obj = obj.offsetParent; 21 | }; 22 | return [curleft, curtop]; 23 | } 24 | 25 | function hideUsingEsc(e) { 26 | if (!e) { e = event; } 27 | hideTip(e, currentTipElement, currentTip); 28 | } 29 | 30 | function showTip(evt, name, unique, owner) { 31 | document.onkeydown = hideUsingEsc; 32 | if (currentTip == unique) return; 33 | currentTip = unique; 34 | currentTipElement = name; 35 | 36 | var pos = findPos(owner ? owner : (evt.srcElement ? evt.srcElement : evt.target)); 37 | var posx = pos[0]; 38 | var posy = pos[1]; 39 | 40 | var el = document.getElementById(name); 41 | var parent = (document.documentElement == null) ? document.body : document.documentElement; 42 | el.style.position = "absolute"; 43 | el.style.left = posx + "px"; 44 | el.style.top = posy + "px"; 45 | el.style.display = "block"; 46 | } 47 | -------------------------------------------------------------------------------- /docsSrc/files/placeholder.md: -------------------------------------------------------------------------------- 1 | place images or other files here 2 | -------------------------------------------------------------------------------- /docsSrc/index.md: -------------------------------------------------------------------------------- 1 | # BinaryDefense.FSharp.Analyzers 2 | 3 | --- 4 | 5 | ## What is BinaryDefense.FSharp.Analyzers? 6 | 7 | BinaryDefense.FSharp.Analyzers is a library that does this specific thing. 8 | 9 | ## Why use BinaryDefense.FSharp.Analyzers? 10 | 11 | I created it because I had to solve an issue with this other thing. 12 | 13 | --- 14 | 15 |
16 |
17 |
18 |
19 |
Tutorials
20 |

Takes you by the hand through a series of steps to create your first thing.

21 |
22 | 25 |
26 |
27 |
28 |
29 |
30 |
How-To Guides
31 |

Guides you through the steps involved in addressing key problems and use-cases.

32 |
33 | 36 |
37 |
38 |
39 |
40 |
41 |
Explanations
42 |

Discusses key topics and concepts at a fairly high level and provide useful background information and explanation..

43 |
44 | 47 |
48 |
49 |
50 |
51 |
52 |
Api Reference
53 |

Contain technical reference for APIs.

54 |
55 | 58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /docsTool/CLI.fs: -------------------------------------------------------------------------------- 1 | namespace DocsTool 2 | 3 | module CLIArgs = 4 | open Argu 5 | open Fake.IO.Globbing.Operators 6 | 7 | type WatchArgs = 8 | | ProjectGlob of string 9 | | DocsSourceDirectory of string 10 | | GitHubRepoUrl of string 11 | | ProjectName of string 12 | | ReleaseVersion of string 13 | with 14 | interface IArgParserTemplate with 15 | member this.Usage = 16 | match this with 17 | | ProjectGlob _ -> "The glob for the dlls to generate API documentation." 18 | | DocsSourceDirectory _ -> "The docs source directory." 19 | | GitHubRepoUrl _ -> "The GitHub repository url." 20 | | ProjectName _ -> "The project name." 21 | | ReleaseVersion _ -> "The project's Release Version name." 22 | 23 | type BuildArgs = 24 | | SiteBaseUrl of string 25 | | ProjectGlob of string 26 | | DocsOutputDirectory of string 27 | | DocsSourceDirectory of string 28 | | GitHubRepoUrl of string 29 | | ProjectName of string 30 | | ReleaseVersion of string 31 | with 32 | interface IArgParserTemplate with 33 | member this.Usage = 34 | match this with 35 | | SiteBaseUrl _ -> "The public site's base url." 36 | | ProjectGlob _ -> "The glob for the dlls to generate API documentation" 37 | | DocsOutputDirectory _ -> "The docs output directory." 38 | | DocsSourceDirectory _ -> "The docs source directory." 39 | | GitHubRepoUrl _ -> "The GitHub repository url." 40 | | ProjectName _ -> "The project name." 41 | | ReleaseVersion _ -> "The project's Release Version name." 42 | 43 | type CLIArguments = 44 | | [] Watch of ParseResults 45 | | [] Build of ParseResults 46 | with 47 | interface IArgParserTemplate with 48 | member this.Usage = 49 | match this with 50 | | Watch _ -> "Builds the docs, serves the content, and watches for changes to the content." 51 | | Build _ -> "Builds the docs" 52 | -------------------------------------------------------------------------------- /docsTool/Prelude.fs: -------------------------------------------------------------------------------- 1 | namespace DocsTool 2 | 3 | module Uri = 4 | open System 5 | let simpleCombine (slug : string) (baseUri : Uri) = 6 | sprintf "%s/%s" (baseUri.AbsoluteUri.TrimEnd('/')) (slug.TrimStart('/')) 7 | 8 | let create (url : string) = 9 | match Uri.TryCreate(url, UriKind.Absolute) with 10 | | (true, v) -> v 11 | | _ -> failwithf "Bad url %s" url 12 | -------------------------------------------------------------------------------- /docsTool/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org 2 | 3 | 4 | open System 5 | open Fake.IO.FileSystemOperators 6 | open Fake.IO 7 | open Fake.Core 8 | 9 | let dispose (d : #IDisposable) = d.Dispose() 10 | type DisposableDirectory (directory : string) = 11 | do 12 | Trace.tracefn "Created disposable directory %s" directory 13 | static member Create() = 14 | let tempPath = IO.Path.Combine(IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")) 15 | IO.Directory.CreateDirectory tempPath |> ignore 16 | 17 | new DisposableDirectory(tempPath) 18 | member x.Directory = directory 19 | member x.DirectoryInfo = IO.DirectoryInfo(directory) 20 | 21 | interface IDisposable with 22 | member x.Dispose() = 23 | Trace.tracefn "Deleting directory %s" directory 24 | IO.Directory.Delete(x.Directory,true) 25 | 26 | 27 | let refreshWebpageEvent = new Event() 28 | 29 | type Configuration = { 30 | SiteBaseUrl : Uri 31 | GitHubRepoUrl : Uri 32 | RepositoryRoot : IO.DirectoryInfo 33 | DocsOutputDirectory : IO.DirectoryInfo 34 | DocsSourceDirectory : IO.DirectoryInfo 35 | ProjectName : string 36 | ProjectFilesGlob : IGlobbingPattern 37 | ReleaseVersion : string 38 | } 39 | 40 | let docsApiDir docsDir = docsDir @@ "Api_Reference" 41 | 42 | type DisposableList = 43 | { 44 | disposables : IDisposable list 45 | } interface IDisposable with 46 | member x.Dispose () = 47 | x.disposables |> List.iter(fun s -> s.Dispose()) 48 | 49 | 50 | module ProjInfo = 51 | open System.IO 52 | 53 | type References = FileInfo [] 54 | type TargetPath = FileInfo 55 | 56 | type ProjInfo = { 57 | References : References 58 | TargetPath : TargetPath 59 | } 60 | 61 | open Dotnet.ProjInfo.Workspace 62 | open Dotnet.ProjInfo.Workspace.FCS 63 | let createFCS () = 64 | let checker = 65 | FCS_Checker.Create( 66 | projectCacheSize = 200, 67 | keepAllBackgroundResolutions = true, 68 | keepAssemblyContents = true) 69 | checker.ImplicitlyStartBackgroundWork <- true 70 | checker 71 | 72 | let createLoader () = 73 | let msbuildLocator = MSBuildLocator() 74 | let config = LoaderConfig.Default msbuildLocator 75 | let loader = Loader.Create(config) 76 | let netFwconfig = NetFWInfoConfig.Default msbuildLocator 77 | let netFwInfo = NetFWInfo.Create(netFwconfig) 78 | 79 | loader, netFwInfo 80 | 81 | let [] RefPrefix = "-r:" 82 | 83 | let findTargetPath targetPath = 84 | if File.exists targetPath then 85 | FileInfo targetPath 86 | else 87 | //HACK: Need to get dotnet-proj-info to handle configurations when extracting data 88 | let debugFolder = sprintf "%cDebug%c" Path.DirectorySeparatorChar Path.DirectorySeparatorChar 89 | let releaseFolder = sprintf "%cRelease%c" Path.DirectorySeparatorChar Path.DirectorySeparatorChar 90 | let debugFolderAlt = sprintf "%cDebug%c" Path.DirectorySeparatorChar Path.AltDirectorySeparatorChar 91 | let releaseFolderAlt = sprintf "%cRelease%c" Path.DirectorySeparatorChar Path.AltDirectorySeparatorChar 92 | 93 | let releasePath = targetPath.Replace(debugFolder, releaseFolder).Replace(debugFolderAlt, releaseFolderAlt) 94 | if releasePath |> File.exists then 95 | releasePath |> FileInfo 96 | else 97 | failwithf "Couldn't find a dll to generate documentationfrom %s or %s" targetPath releasePath 98 | 99 | let findReferences projPath : ProjInfo= 100 | let fcs = createFCS () 101 | let loader, netFwInfo = createLoader () 102 | loader.LoadProjects [ projPath ] 103 | let fcsBinder = FCSBinder(netFwInfo, loader, fcs) 104 | match fcsBinder.GetProjectOptions(projPath) with 105 | | Some options -> 106 | let references = 107 | options.OtherOptions 108 | |> Array.filter(fun s -> 109 | s.StartsWith(RefPrefix) 110 | ) 111 | |> Array.map(fun s -> 112 | // removes "-r:" from beginning of reference path 113 | s.Remove(0,RefPrefix.Length) 114 | |> FileInfo 115 | ) 116 | 117 | let dpwPo = 118 | match options.ExtraProjectInfo with 119 | | Some (:? ProjectOptions as dpwPo) -> dpwPo 120 | | x -> failwithf "invalid project info %A" x 121 | let targetPath = findTargetPath dpwPo.ExtraProjectInfo.TargetPath 122 | { References = references ; TargetPath = targetPath} 123 | 124 | | None -> 125 | failwithf "Couldn't read project %s" projPath 126 | 127 | 128 | module GenerateDocs = 129 | open DocsTool 130 | open Fake.Core 131 | open Fake.IO.Globbing.Operators 132 | open Fake.IO 133 | open Fable.React 134 | open Fable.React.Helpers 135 | open FSharp.Literate 136 | open System.IO 137 | open FSharp.MetadataFormat 138 | 139 | 140 | type GeneratedDoc = { 141 | SourcePath : FileInfo option 142 | OutputPath : FileInfo 143 | Content : ReactElement list 144 | Title : string 145 | } 146 | 147 | 148 | let docsFileGlob docsSrcDir = 149 | !! (docsSrcDir @@ "**/*.fsx") 150 | ++ (docsSrcDir @@ "**/*.md") 151 | 152 | let render html = 153 | fragment [] [ 154 | RawText "" 155 | RawText "\n" 156 | html ] 157 | |> Fable.ReactServer.renderToString 158 | 159 | let renderWithMasterTemplate masterCfg navBar titletext bodytext pageSource = 160 | Master.masterTemplate masterCfg navBar titletext bodytext pageSource 161 | |> render 162 | 163 | let renderWithMasterAndWrite masterCfg (outPath : FileInfo) navBar titletext bodytext pageSource = 164 | let contents = renderWithMasterTemplate masterCfg navBar titletext bodytext pageSource 165 | IO.Directory.CreateDirectory(outPath.DirectoryName) |> ignore 166 | 167 | IO.File.WriteAllText(outPath.FullName, contents) 168 | Fake.Core.Trace.tracefn "Rendered to %s" outPath.FullName 169 | 170 | let generateNav (cfg : Configuration) (generatedDocs : GeneratedDoc list) = 171 | let docsDir = cfg.DocsOutputDirectory.FullName 172 | let pages = 173 | generatedDocs 174 | |> List.map(fun gd -> gd.OutputPath) 175 | |> List.filter(fun f -> f.FullName.StartsWith(docsDir "content") |> not) 176 | |> List.filter(fun f -> f.FullName.StartsWith(docsDir "files") |> not) 177 | |> List.filter(fun f -> f.FullName.StartsWith(docsDir "index.html") |> not) 178 | 179 | let topLevelNavs : Nav.TopLevelNav = { 180 | DocsRoot = IO.DirectoryInfo docsDir 181 | DocsPages = pages 182 | } 183 | 184 | let navCfg : Nav.NavConfig = { 185 | SiteBaseUrl = cfg.SiteBaseUrl 186 | GitHubRepoUrl = cfg.GitHubRepoUrl 187 | ProjectName = cfg.ProjectName 188 | TopLevelNav = topLevelNavs 189 | } 190 | 191 | Nav.generateNav navCfg 192 | 193 | let renderGeneratedDocs isWatchMode (cfg : Configuration) (generatedDocs : GeneratedDoc list) = 194 | let nav = generateNav cfg generatedDocs 195 | let masterCfg : Master.MasterTemplateConfig = { 196 | SiteBaseUrl = cfg.SiteBaseUrl 197 | GitHubRepoUrl = cfg.GitHubRepoUrl 198 | ProjectName = cfg.ProjectName 199 | ReleaseVersion = cfg.ReleaseVersion 200 | ReleaseDate = DateTimeOffset.Now 201 | RepositoryRoot = cfg.RepositoryRoot 202 | IsWatchMode = isWatchMode 203 | } 204 | generatedDocs 205 | |> Seq.iter(fun gd -> 206 | let pageSource = 207 | gd.SourcePath 208 | |> Option.map(fun sp -> 209 | sp.FullName.Replace(cfg.RepositoryRoot.FullName, "").Replace("\\", "/") 210 | ) 211 | renderWithMasterAndWrite masterCfg gd.OutputPath nav gd.Title gd.Content pageSource 212 | ) 213 | 214 | 215 | let copyAssets (cfg : Configuration) = 216 | Shell.copyDir (cfg.DocsOutputDirectory.FullName "content") ( cfg.DocsSourceDirectory.FullName "content") (fun _ -> true) 217 | Shell.copyDir (cfg.DocsOutputDirectory.FullName "files") ( cfg.DocsSourceDirectory.FullName "files") (fun _ -> true) 218 | 219 | 220 | let regexReplace (cfg : Configuration) source = 221 | let replacements = 222 | [ 223 | "{{siteBaseUrl}}", (cfg.SiteBaseUrl.ToString().TrimEnd('/')) 224 | ] 225 | (source, replacements) 226 | ||> List.fold(fun state (pattern, replacement) -> 227 | Text.RegularExpressions.Regex.Replace(state, pattern, replacement) 228 | ) 229 | 230 | let generateDocs (libDirs : ProjInfo.References) (docSourcePaths : IGlobbingPattern) (cfg : Configuration) = 231 | let parse (fileName : string) source = 232 | let doc = 233 | let references = 234 | libDirs 235 | |> Array.map(fun fi -> fi.DirectoryName) 236 | |> Array.distinct 237 | |> Array.map(sprintf "-I:%s") 238 | let runtimeDeps = 239 | [| 240 | "-r:System.Runtime" 241 | "-r:System.Net.WebClient" 242 | |] 243 | let compilerOptions = String.Join(' ', Array.concat [runtimeDeps; references]) 244 | let fsiEvaluator = FSharp.Literate.FsiEvaluator(references) 245 | match Path.GetExtension fileName with 246 | | ".fsx" -> 247 | Literate.ParseScriptString( 248 | source, 249 | path = fileName, 250 | compilerOptions = compilerOptions, 251 | fsiEvaluator = fsiEvaluator) 252 | | ".md" -> 253 | let source = regexReplace cfg source 254 | Literate.ParseMarkdownString( 255 | source, 256 | path = fileName, 257 | compilerOptions = compilerOptions, 258 | fsiEvaluator = fsiEvaluator 259 | ) 260 | | others -> failwithf "FSharp.Literal does not support %s file extensions" others 261 | FSharp.Literate.Literate.FormatLiterateNodes(doc, OutputKind.Html, "", true, true) 262 | 263 | let format (doc: LiterateDocument) = 264 | if not <| Seq.isEmpty doc.Errors 265 | then 266 | failwithf "error while formatting file %s. Errors are:\n%A" doc.SourceFile doc.Errors 267 | else 268 | Formatting.format doc.MarkdownDocument true OutputKind.Html 269 | + doc.FormattedTips 270 | 271 | 272 | 273 | docSourcePaths 274 | |> Array.ofSeq 275 | |> Seq.map(fun filePath -> 276 | 277 | Fake.Core.Trace.tracefn "Rendering %s" filePath 278 | let file = IO.File.ReadAllText filePath 279 | let outPath = 280 | let changeExtension ext path = IO.Path.ChangeExtension(path,ext) 281 | filePath.Replace(cfg.DocsSourceDirectory.FullName, cfg.DocsOutputDirectory.FullName) 282 | |> changeExtension ".html" 283 | |> FileInfo 284 | let fs = 285 | file 286 | |> parse filePath 287 | |> format 288 | let contents = 289 | [div [] [ 290 | fs 291 | |> RawText 292 | ]] 293 | 294 | { 295 | SourcePath = FileInfo filePath |> Some 296 | OutputPath = outPath 297 | Content = contents 298 | Title = sprintf "%s-%s" outPath.Name cfg.ProjectName 299 | } 300 | ) 301 | |> Seq.toList 302 | 303 | 304 | let generateAPI (projInfos : ProjInfo.ProjInfo array) (cfg : Configuration) = 305 | let generate (projInfo : ProjInfo.ProjInfo) = 306 | Trace.tracefn "Generating API Docs for %s" projInfo.TargetPath.FullName 307 | let mscorlibDir = 308 | (Uri(typedefof.GetType().Assembly.CodeBase)) //Find runtime dll 309 | .AbsolutePath // removes file protocol from path 310 | |> Path.GetDirectoryName 311 | let references = 312 | projInfo.References 313 | |> Array.toList 314 | |> List.map(fun fi -> fi.DirectoryName) 315 | |> List.distinct 316 | let libDirs = mscorlibDir :: references 317 | let targetApiDir = docsApiDir cfg.DocsOutputDirectory.FullName @@ IO.Path.GetFileNameWithoutExtension(projInfo.TargetPath.Name) 318 | 319 | let generatorOutput = 320 | MetadataFormat.Generate( 321 | projInfo.TargetPath.FullName, 322 | libDirs = libDirs, 323 | sourceFolder = cfg.RepositoryRoot.FullName, 324 | sourceRepo = (cfg.GitHubRepoUrl |> Uri.simpleCombine "tree/master" |> string), 325 | markDownComments = false 326 | ) 327 | 328 | let fi = FileInfo <| targetApiDir @@ (sprintf "%s.html" generatorOutput.AssemblyGroup.Name) 329 | let indexDoc = { 330 | SourcePath = None 331 | OutputPath = fi 332 | Content = [Namespaces.generateNamespaceDocs generatorOutput.AssemblyGroup generatorOutput.Properties] 333 | Title = sprintf "%s-%s" fi.Name cfg.ProjectName 334 | } 335 | 336 | let moduleDocs = 337 | generatorOutput.ModuleInfos 338 | |> List.map (fun m -> 339 | let fi = FileInfo <| targetApiDir @@ (sprintf "%s.html" m.Module.UrlName) 340 | let content = Modules.generateModuleDocs m generatorOutput.Properties 341 | { 342 | SourcePath = None 343 | OutputPath = fi 344 | Content = content 345 | Title = sprintf "%s-%s" m.Module.Name cfg.ProjectName 346 | } 347 | ) 348 | let typeDocs = 349 | generatorOutput.TypesInfos 350 | |> List.map (fun m -> 351 | let fi = FileInfo <| targetApiDir @@ (sprintf "%s.html" m.Type.UrlName) 352 | let content = Types.generateTypeDocs m generatorOutput.Properties 353 | { 354 | SourcePath = None 355 | OutputPath = fi 356 | Content = content 357 | Title = sprintf "%s-%s" m.Type.Name cfg.ProjectName 358 | } 359 | ) 360 | [ indexDoc ] @ moduleDocs @ typeDocs 361 | projInfos 362 | |> Seq.collect(generate) 363 | |> Seq.toList 364 | 365 | let buildDocs (projInfos : ProjInfo.ProjInfo array) (cfg : Configuration) = 366 | let refs = projInfos |> Seq.collect (fun p -> p.References) |> Seq.distinct |> Seq.toArray 367 | copyAssets cfg 368 | let generateDocs = 369 | async { 370 | return generateDocs refs (docsFileGlob cfg.DocsSourceDirectory.FullName) cfg 371 | } 372 | let generateAPI = 373 | async { 374 | return (generateAPI projInfos cfg) 375 | } 376 | Async.Parallel [generateDocs; generateAPI] 377 | |> Async.RunSynchronously 378 | |> Array.toList 379 | |> List.collect id 380 | 381 | let renderDocs (cfg : Configuration) = 382 | let projInfos = cfg.ProjectFilesGlob |> Seq.map(ProjInfo.findReferences) |> Seq.toArray 383 | buildDocs projInfos cfg 384 | |> renderGeneratedDocs false cfg 385 | 386 | let watchDocs (cfg : Configuration) = 387 | let projInfos = cfg.ProjectFilesGlob |> Seq.map(ProjInfo.findReferences) |> Seq.toArray 388 | let initialDocs = buildDocs projInfos cfg 389 | let renderGeneratedDocs = renderGeneratedDocs true 390 | initialDocs |> renderGeneratedDocs cfg 391 | 392 | let refs = projInfos |> Seq.collect (fun p -> p.References) |> Seq.distinct |> Seq.toArray 393 | let d1 = 394 | docsFileGlob cfg.DocsSourceDirectory.FullName 395 | |> ChangeWatcher.run (fun changes -> 396 | printfn "changes %A" changes 397 | changes 398 | |> Seq.iter (fun m -> 399 | printfn "watching %s" m.FullPath 400 | let generated = generateDocs refs (!! m.FullPath) cfg 401 | initialDocs 402 | |> List.filter(fun x -> generated |> List.exists(fun y -> y.OutputPath = x.OutputPath) |> not ) 403 | |> List.append generated 404 | |> List.distinctBy(fun gd -> gd.OutputPath.FullName) 405 | |> renderGeneratedDocs cfg 406 | ) 407 | refreshWebpageEvent.Trigger "m.FullPath" 408 | ) 409 | let d2 = 410 | !! (cfg.DocsSourceDirectory.FullName "content" "**/*") 411 | ++ (cfg.DocsSourceDirectory.FullName "files" "**/*") 412 | |> ChangeWatcher.run(fun changes -> 413 | printfn "changes %A" changes 414 | copyAssets cfg 415 | refreshWebpageEvent.Trigger "Assets" 416 | ) 417 | 418 | 419 | let d3 = 420 | projInfos 421 | |> Seq.map(fun p -> p.TargetPath.FullName) 422 | |> Seq.fold ((++)) (!! "") 423 | 424 | |> ChangeWatcher.run(fun changes -> 425 | changes 426 | |> Seq.iter(fun c -> Trace.logf "Regenerating API docs due to %s" c.FullPath ) 427 | let generated = generateAPI projInfos cfg 428 | initialDocs 429 | |> List.filter(fun x -> generated |> List.exists(fun y -> y.OutputPath = x.OutputPath) |> not ) 430 | |> List.append generated 431 | |> List.distinctBy(fun gd -> gd.OutputPath.FullName) 432 | |> renderGeneratedDocs cfg 433 | refreshWebpageEvent.Trigger "Api" 434 | ) 435 | { disposables = [d1; d2; d3] } :> IDisposable 436 | 437 | 438 | module WebServer = 439 | open Microsoft.AspNetCore.Hosting 440 | open Microsoft.AspNetCore.Builder 441 | open Microsoft.Extensions.FileProviders 442 | open Microsoft.AspNetCore.Http 443 | open System.Net.WebSockets 444 | open System.Diagnostics 445 | 446 | let hostname = "localhost" 447 | let port = 5000 448 | 449 | /// Helper to determine if port is in use 450 | let waitForPortInUse (hostname : string) port = 451 | let mutable portInUse = false 452 | while not portInUse do 453 | Async.Sleep(10) |> Async.RunSynchronously 454 | use client = new Net.Sockets.TcpClient() 455 | try 456 | client.Connect(hostname,port) 457 | portInUse <- client.Connected 458 | client.Close() 459 | with e -> 460 | client.Close() 461 | 462 | /// Async version of IApplicationBuilder.Use 463 | let useAsync (middlware : HttpContext -> (unit -> Async) -> Async) (app:IApplicationBuilder) = 464 | app.Use(fun env next -> 465 | middlware env (next.Invoke >> Async.AwaitTask) 466 | |> Async.StartAsTask 467 | :> System.Threading.Tasks.Task 468 | ) 469 | 470 | let createWebsocketForLiveReload (httpContext : HttpContext) (next : unit -> Async) = async { 471 | if httpContext.WebSockets.IsWebSocketRequest then 472 | let! websocket = httpContext.WebSockets.AcceptWebSocketAsync() |> Async.AwaitTask 473 | use d = 474 | refreshWebpageEvent.Publish 475 | |> Observable.subscribe (fun m -> 476 | let segment = ArraySegment(m |> Text.Encoding.UTF8.GetBytes) 477 | websocket.SendAsync(segment, WebSocketMessageType.Text, true, httpContext.RequestAborted) 478 | |> Async.AwaitTask 479 | |> Async.Start 480 | 481 | ) 482 | while websocket.State <> WebSocketState.Closed do 483 | do! Async.Sleep(1000) 484 | else 485 | do! next () 486 | } 487 | 488 | let configureWebsocket (appBuilder : IApplicationBuilder) = 489 | appBuilder.UseWebSockets() 490 | |> useAsync (createWebsocketForLiveReload) 491 | |> ignore 492 | 493 | let startWebserver docsDir (url : string) = 494 | WebHostBuilder() 495 | .UseKestrel() 496 | .UseUrls(url) 497 | .Configure(fun app -> 498 | let opts = 499 | StaticFileOptions( 500 | FileProvider = new PhysicalFileProvider(docsDir) 501 | ) 502 | app.UseStaticFiles(opts) |> ignore 503 | configureWebsocket app 504 | ) 505 | .Build() 506 | .Run() 507 | 508 | let openBrowser url = 509 | //https://github.com/dotnet/corefx/issues/10361 510 | let psi = ProcessStartInfo(FileName = url, UseShellExecute = true) 511 | let proc = Process.Start psi 512 | proc.WaitForExit() 513 | if proc.ExitCode <> 0 then failwithf "opening browser failed" 514 | 515 | let serveDocs docsDir = 516 | async { 517 | waitForPortInUse hostname port 518 | sprintf "http://%s:%d/index.html" hostname port |> openBrowser 519 | } |> Async.Start 520 | startWebserver docsDir (sprintf "http://%s:%d" hostname port) 521 | 522 | 523 | open Argu 524 | open Fake.IO.Globbing.Operators 525 | open DocsTool.CLIArgs 526 | [] 527 | let main argv = 528 | use tempDocsOutDir = DisposableDirectory.Create() 529 | use __ = AppDomain.CurrentDomain.ProcessExit.Subscribe(fun _ -> 530 | dispose tempDocsOutDir 531 | ) 532 | use __ = Console.CancelKeyPress.Subscribe(fun _ -> 533 | dispose tempDocsOutDir 534 | ) 535 | let defaultConfig = { 536 | SiteBaseUrl = Uri(sprintf "http://%s:%d/" WebServer.hostname WebServer.port ) 537 | GitHubRepoUrl = Uri "https://github.com" 538 | RepositoryRoot = IO.DirectoryInfo (__SOURCE_DIRECTORY__ @@ "..") 539 | DocsOutputDirectory = tempDocsOutDir.DirectoryInfo 540 | DocsSourceDirectory = IO.DirectoryInfo "docsSrc" 541 | ProjectName = "" 542 | ProjectFilesGlob = !! "" 543 | ReleaseVersion = "0.1.0" 544 | } 545 | 546 | let errorHandler = ProcessExiter(colorizer = function ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red) 547 | let programName = 548 | let name = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name 549 | if Fake.Core.Environment.isWindows then 550 | sprintf "%s.exe" name 551 | else 552 | name 553 | 554 | 555 | 556 | let parser = ArgumentParser.Create(programName = programName, errorHandler = errorHandler) 557 | let parsedArgs = parser.Parse argv 558 | match parsedArgs.GetSubCommand() with 559 | | Build args -> 560 | let config = 561 | (defaultConfig, args.GetAllResults()) 562 | ||> List.fold(fun state next -> 563 | match next with 564 | | BuildArgs.SiteBaseUrl url -> { state with SiteBaseUrl = Uri url } 565 | | BuildArgs.ProjectGlob glob -> { state with ProjectFilesGlob = !! glob} 566 | | BuildArgs.DocsOutputDirectory outdir -> { state with DocsOutputDirectory = IO.DirectoryInfo outdir} 567 | | BuildArgs.DocsSourceDirectory srcdir -> { state with DocsSourceDirectory = IO.DirectoryInfo srcdir} 568 | | BuildArgs.GitHubRepoUrl url -> { state with GitHubRepoUrl = Uri url} 569 | | BuildArgs.ProjectName repo -> { state with ProjectName = repo} 570 | | BuildArgs.ReleaseVersion version -> { state with ReleaseVersion = version} 571 | ) 572 | GenerateDocs.renderDocs config 573 | | Watch args -> 574 | let config = 575 | (defaultConfig, args.GetAllResults()) 576 | ||> List.fold(fun state next -> 577 | match next with 578 | | WatchArgs.ProjectGlob glob -> {state with ProjectFilesGlob = !! glob} 579 | | WatchArgs.DocsSourceDirectory srcdir -> { state with DocsSourceDirectory = IO.DirectoryInfo srcdir} 580 | | WatchArgs.GitHubRepoUrl url -> { state with GitHubRepoUrl = Uri url} 581 | | WatchArgs.ProjectName repo -> { state with ProjectName = repo} 582 | | WatchArgs.ReleaseVersion version -> { state with ReleaseVersion = version} 583 | ) 584 | use ds = GenerateDocs.watchDocs config 585 | WebServer.serveDocs config.DocsOutputDirectory.FullName 586 | 0 // return an integer exit code 587 | -------------------------------------------------------------------------------- /docsTool/README.md: -------------------------------------------------------------------------------- 1 | # Docs Tool 2 | 3 | ## Example 4 | [MiniScaffold docs example](https://www.jimmybyrd.me/miniscaffold-docs-test/) 5 | 6 | ## Docs High Level Design 7 | 8 | This template is based heavily on [What nobody tells you about documentation](https://www.divio.com/blog/documentation/). In `docsSrc` folder you'll see a similar structure to what is described below: 9 | 10 | - **Tutorials** 11 | - is learning-oriented 12 | - allows the newcomer to get started 13 | - is a lesson 14 | - Analogy: teaching a small child how to cook 15 | - **How-To Guides** 16 | - is goal-oriented 17 | - shows how to solve a specific problem 18 | - is a series of steps 19 | - Analogy: a recipe in a cookery book 20 | - **Explanation** 21 | - is understanding-oriented 22 | - explains 23 | - provides background and context 24 | - Analogy: an article on culinary social history 25 | - **Reference** 26 | - is information-oriented 27 | - describes the machinery 28 | - is accurate and complete 29 | - Analogy: a reference encyclopedia article 30 | 31 | 32 | The folders in `docsSrc` are: 33 | 34 | - `content` - custom css, javascript, and similar go here. 35 | - `Explanations` - A content section as defined above. 36 | - `files` - extra files like screenshots, images, videos. 37 | - `How_Tos` - A content section as defined above. 38 | - `Tutorials` - A content section as defined above. 39 | - `index.md` - The entry page to your documentation 40 | 41 | The navbar is generated by the folders in docsSrc, excluding `content` and `files` folders. Looking at the [example](https://www.jimmybyrd.me/miniscaffold-docs-test/) we can the navbar containing: 42 | 43 | - `Api References` 44 | - `Explanations` 45 | - `How Tos` 46 | - `Tutorials` 47 | 48 | The odd one not generated from the convention of your folders in docsSrc is Api References. This is generated by the [XML Doc Comments](https://docs.microsoft.com/en-us/dotnet/csharp/codedoc) in your libraries under the `src` folder. 49 | 50 | 51 | ## Running docs tool 52 | 53 | ``` 54 | USAGE: docsTool [--help] [ []] 55 | 56 | SUBCOMMANDS: 57 | 58 | watch Builds the docs, serves the content, and watches for changes to the content. 59 | build Builds the docs 60 | 61 | Use 'docsTool --help' for additional information. 62 | ``` 63 | 64 | ### build 65 | 66 | Builds the docs 67 | 68 | ``` 69 | USAGE: docsTool build [--help] [--sitebaseurl ] [--projectglob ] [--docsoutputdirectory ] [--docssourcedirectory ] [--githubrepourl ] [--projectname ] 70 | [--releaseversion ] 71 | 72 | OPTIONS: 73 | 74 | --sitebaseurl 75 | The public site's base url. 76 | --projectglob 77 | The glob for the dlls to generate API documentation 78 | --docsoutputdirectory 79 | The docs output directory. 80 | --docssourcedirectory 81 | The docs source directory. 82 | --githubrepourl 83 | The GitHub repository url. 84 | --projectname 85 | The project name. 86 | --releaseversion 87 | The project's Release Version name. 88 | --help display this list of options. 89 | 90 | ``` 91 | 92 | 93 | ### watch 94 | 95 | Builds the docs, serves the content, and watches for changes to the content. 96 | 97 | ``` 98 | 99 | USAGE: docsTool watch [--help] [--projectglob ] [--docsoutputdirectory ] [--docssourcedirectory ] [--githubrepourl ] [--projectname ] [--releaseversion ] 100 | 101 | OPTIONS: 102 | 103 | --projectglob 104 | The glob for the dlls to generate API documentation. 105 | --docsoutputdirectory 106 | The docs output directory. 107 | --docssourcedirectory 108 | The docs source directory. 109 | --githubrepourl 110 | The GitHub repository url. 111 | --projectname 112 | The project name. 113 | --releaseversion 114 | The project's Release Version name. 115 | --help display this list of options. 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /docsTool/docsTool.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net5.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docsTool/paket.references: -------------------------------------------------------------------------------- 1 | group Docs 2 | Argu 3 | FSharp.Core 4 | Fake.IO.FileSystem 5 | Fake.DotNet.Cli 6 | FSharp.Literate 7 | Fable.React 8 | Microsoft.AspNetCore.StaticFiles 9 | Microsoft.AspNetCore.Hosting 10 | Microsoft.AspNetCore.Server.Kestrel 11 | Microsoft.AspNetCore.WebSockets 12 | Dotnet.ProjInfo.Workspace.FCS 13 | -------------------------------------------------------------------------------- /docsTool/templates/helpers.fs: -------------------------------------------------------------------------------- 1 | module Helpers 2 | open System 3 | open Fable.React 4 | open Fable.React.Props 5 | open FSharp.MetadataFormat 6 | 7 | 8 | let createAnchorIcon name = 9 | let normalized = name 10 | let href = sprintf "#%s" normalized 11 | a [Href href; Id normalized] [ 12 | str "#" 13 | ] 14 | 15 | let createAnchor fullName name = 16 | let fullNameNormalize = fullName 17 | a [ 18 | Name fullNameNormalize 19 | Href (sprintf "#%s" fullNameNormalize) 20 | Class "anchor" 21 | ] [ 22 | str name 23 | ] 24 | 25 | let renderNamespace (ns: Namespace) = [ 26 | h3 [] [ str "Namespace" ] 27 | str ns.Name 28 | ] 29 | 30 | let inline isObsolete< ^t when ^t : (member IsObsolete: bool)> t = 31 | (^t : (member IsObsolete: bool) (t)) 32 | 33 | let inline obsoleteMessage< ^t when ^t : (member ObsoleteMessage: string)> t = 34 | (^t : (member ObsoleteMessage:string) (t)) 35 | 36 | let inline renderObsoleteMessage item = 37 | if isObsolete item 38 | then 39 | let text = match obsoleteMessage item with | "" | null -> "This member is obsolete" | s -> s 40 | [ 41 | div [Class "alert alert-warning"] [ 42 | strong [] [ str "OBSOLETE: "] 43 | str text 44 | ] 45 | ] 46 | else 47 | [] 48 | -------------------------------------------------------------------------------- /docsTool/templates/master.fs: -------------------------------------------------------------------------------- 1 | module Master 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open DocsTool 7 | 8 | type MasterTemplateConfig = { 9 | SiteBaseUrl : Uri 10 | GitHubRepoUrl : Uri 11 | ProjectName : string 12 | ReleaseVersion : string 13 | ReleaseDate : DateTimeOffset 14 | RepositoryRoot: IO.DirectoryInfo 15 | IsWatchMode : bool 16 | } 17 | 18 | type FAIcon = 19 | | Solid of name: string 20 | | Brand of name: string 21 | 22 | let footerLink uri image linkText = 23 | let faClass, img = 24 | match image with 25 | | Solid name -> "fas", name 26 | | Brand name -> "fab", name 27 | a [Href uri; Class "text-white"] [ 28 | i [Class (sprintf "%s fa-%s fa-fw mr-2" faClass img)] [] 29 | str linkText 30 | ] 31 | 32 | let repoFileLink repoUrl filePathFromRepoRoot = 33 | let link = repoUrl |> Uri.simpleCombine (sprintf "blob/master/%s" filePathFromRepoRoot) 34 | footerLink link 35 | 36 | let linkColumn headerTitle items = 37 | div [Class "col-12 col-md-4 mb-4 mb-md-0"] [ 38 | div [Class "text-light"] [ 39 | h2 [Class "h5"] [ str headerTitle ] 40 | ul [Class "list-group list-group-flush"] 41 | (items |> List.choose (function | [] -> None 42 | | items -> Some(li [Class "list-group-item bg-dark ml-0 pl-0"] items))) 43 | ] 44 | ] 45 | 46 | let renderFooter (cfg : MasterTemplateConfig) (pageSource : string option) = 47 | let hasFile relPath = 48 | match cfg.RepositoryRoot.GetFiles(relPath) with 49 | | [||] -> false 50 | | [|file|] -> true 51 | | files -> false 52 | 53 | let repoFileLink relPath image title = 54 | if hasFile relPath 55 | then [ repoFileLink cfg.GitHubRepoUrl relPath image title ] 56 | else [] 57 | 58 | footer [Class "footer font-small m-0 py-4 bg-dark"] [ 59 | div [Class "container"] [ 60 | div [Class "row"] [ 61 | linkColumn "Project Resources" [ 62 | repoFileLink "README.md" (Solid "book-reader") "README" 63 | repoFileLink "RELEASE_NOTES.md" (Solid "sticky-note") "Release Notes / Changelog" 64 | repoFileLink "LICENSE.md" (Solid "id-card") "License" 65 | repoFileLink "CONTRIBUTING.md" (Solid "directions") "Contributing" 66 | repoFileLink "CODE_OF_CONDUCT.md" (Solid "users") "Code of Conduct" 67 | ] 68 | linkColumn "Other Links" [ 69 | [footerLink "https://docs.microsoft.com/en-us/dotnet/fsharp/" (Brand "microsoft") "F# Documentation"] 70 | [footerLink "https://fsharp.org/guides/slack/" (Brand "slack") "F# Slack"] 71 | [a [Href "http://foundation.fsharp.org/"; Class "text-white"] [ 72 | img [Class "fsharp-footer-logo mr-2"; Src "https://fsharp.org/img/logo/fsharp.svg"; Alt "FSharp Logo"] 73 | str "F# Software Foundation" 74 | ]] 75 | ] 76 | linkColumn "Metadata" [ 77 | [str "Generated for version " 78 | a [Class "text-white"; Href (cfg.GitHubRepoUrl |> Uri.simpleCombine (sprintf "releases/tag/%s" cfg.ReleaseVersion))] [str cfg.ReleaseVersion] 79 | str (sprintf " on %s" (cfg.ReleaseDate.ToString("yyyy/MM/dd")))] 80 | match pageSource with 81 | | Some p -> 82 | let page = cfg.GitHubRepoUrl |> Uri.simpleCombine "edit/master" |> Uri |> Uri.simpleCombine p 83 | [ str "Found an issue? " 84 | a [Class "text-white"; Href (page |> string)] [ str "Edit this page." ] ] 85 | | None -> 86 | () 87 | ] 88 | ] 89 | div [Class "row"] [ 90 | div [Class "col text-center"] [ 91 | small [Class "text-light"] [ 92 | i [Class "fas fa-copyright mr-1"] [] 93 | str (sprintf "%s BinaryDefense.FSharp.Analyzers, All rights reserved" (DateTimeOffset.UtcNow.ToString("yyyy"))) 94 | ] 95 | ] 96 | ] 97 | ] 98 | ] 99 | 100 | let masterTemplate (cfg : MasterTemplateConfig) navBar titletext bodyText pageSource = 101 | html [Lang "en"] [ 102 | head [] [ 103 | title [] [ str (sprintf "%s docs / %s" cfg.ProjectName titletext) ] 104 | meta [Name "viewport"; HTMLAttr.Content "width=device-width, initial-scale=1" ] 105 | link [ 106 | Href (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/toggle-bootstrap.min.css?version=%i" cfg.ReleaseDate.Ticks) ) 107 | Type "text/css" 108 | Rel "stylesheet" 109 | ] 110 | link [ 111 | Href (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/toggle-bootstrap-dark.min.css?version=%i" cfg.ReleaseDate.Ticks) ) 112 | Type "text/css" 113 | Rel "stylesheet" 114 | ] 115 | link [ 116 | Href "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css" 117 | Rel "stylesheet" 118 | Integrity "sha384-KA6wR/X5RY4zFAHpv/CnoG2UW1uogYfdnP67Uv7eULvTveboZJg0qUpmJZb5VqzN" 119 | CrossOrigin "anonymous" 120 | ] 121 | link [ 122 | Href (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/style.css?version=%i" cfg.ReleaseDate.Ticks) ) 123 | Type "text/css" 124 | Rel "stylesheet" 125 | ] 126 | 127 | ] 128 | body [] [ 129 | yield navBar 130 | yield div [Class "wrapper d-flex flex-column justify-content-between min-vh-100"] [ 131 | main [Class "container main mb-4"] bodyText 132 | renderFooter cfg pageSource 133 | ] 134 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/themes.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 135 | yield script [ 136 | Src "https://code.jquery.com/jquery-3.4.1.slim.min.js" 137 | Integrity "sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" 138 | CrossOrigin "anonymous" 139 | ] [] 140 | yield script [ 141 | Src "https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" 142 | Integrity "sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" 143 | CrossOrigin "anonymous" 144 | ] [] 145 | yield script [ 146 | Src "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" 147 | Integrity "sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" 148 | CrossOrigin "anonymous" 149 | ] [] 150 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/tips.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 151 | if cfg.IsWatchMode then 152 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/hotload.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 153 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/submenu.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 154 | yield script [Src (cfg.SiteBaseUrl |> Uri.simpleCombine (sprintf "/content/cleanups.js?version=%i" cfg.ReleaseDate.Ticks)) ] [] 155 | ] 156 | ] 157 | -------------------------------------------------------------------------------- /docsTool/templates/modules.fs: -------------------------------------------------------------------------------- 1 | module Modules 2 | open System 3 | open Fable.React 4 | open Fable.React.Props 5 | open FSharp.MetadataFormat 6 | open PartNested 7 | open PartMembers 8 | open Helpers 9 | 10 | 11 | let generateModuleDocs (moduleInfo : ModuleInfo) (props) = 12 | let members = moduleInfo.Module.AllMembers 13 | let comment = moduleInfo.Module.Comment 14 | 15 | let byCategory = 16 | members 17 | |> List.groupBy(fun m -> m.Category) 18 | |> List.sortBy(fun (g,v) -> if String.IsNullOrEmpty g then "ZZZ" else g) 19 | |> List.mapi(fun i (key, value) -> { 20 | Index = i 21 | GroupKey = key 22 | Members = value |> List.sortBy(fun m -> m.Name) 23 | Name = if String.IsNullOrEmpty key then "Other module members" else key 24 | }) 25 | let nestModules = moduleInfo.Module.NestedModules 26 | let nestTypes = moduleInfo.Module.NestedTypes 27 | [ 28 | yield div [ Class "container-fluid py-3" ] [ 29 | yield div [ Class "row" ] [ 30 | yield div [ Class "col-12" ] [ 31 | yield h1 [] [ 32 | str moduleInfo.Module.Name 33 | ] 34 | yield! renderObsoleteMessage moduleInfo.Module 35 | yield! renderNamespace moduleInfo.Namespace 36 | yield dl [] [ 37 | if moduleInfo.ParentModule.IsSome then 38 | yield dt [] [ 39 | str "Parent Module" 40 | ] 41 | yield dd [] [ 42 | a [ 43 | Href (sprintf "%s.html" moduleInfo.ParentModule.Value.UrlName) 44 | ] [ 45 | str moduleInfo.ParentModule.Value.Name 46 | ] 47 | ] 48 | if moduleInfo.Module.Attributes |> Seq.isEmpty |> not then 49 | yield dt [] [ 50 | str "Attributes" 51 | ] 52 | yield dd [] [ 53 | for attr in moduleInfo.Module.Attributes do 54 | yield str (attr.Format()) 55 | yield br [] 56 | ] 57 | ] 58 | 59 | yield div [ 60 | Class "xmldoc" 61 | ] [ 62 | for sec in comment.Sections do 63 | if byCategory |> Seq.exists (fun g -> g.GroupKey = sec.Key) |> not then 64 | if sec.Key <> "" then 65 | yield h2 [] [ 66 | RawText sec.Key 67 | ] 68 | yield RawText sec.Value 69 | ] 70 | 71 | 72 | if byCategory |> Seq.length > 1 then 73 | yield h2 [] [ 74 | str "Table of contents" 75 | ] 76 | 77 | yield ul [] [ 78 | for g in byCategory do 79 | yield li [] [ 80 | a [ 81 | Href (g.Index.ToString() |> sprintf "#section%s") 82 | ] [ 83 | str g.Name 84 | ] 85 | ] 86 | ] 87 | 88 | if (nestTypes |> Seq.length) + (nestModules |> Seq.length) > 0 then 89 | yield h2 [] [ 90 | str "Nested types and modules" 91 | ] 92 | 93 | yield! (partNested (nestTypes |> Seq.toArray) (nestModules |> Seq.toArray)) 94 | 95 | for g in byCategory do 96 | if byCategory |> Seq.length > 1 then 97 | yield h2 [] [ 98 | str g.Name 99 | a [ 100 | Name (sprintf "section%d" g.Index) 101 | ] [ 102 | str " " 103 | ] 104 | ] 105 | 106 | let info = comment.Sections |> Seq.tryFind(fun kvp -> kvp.Key = g.GroupKey) 107 | 108 | match info with 109 | | Some info -> 110 | yield div [ 111 | Class "xmldoc" 112 | ] [ 113 | str info.Value 114 | ] 115 | | None -> 116 | yield nothing 117 | 118 | yield! partMembers "Functions and values" "Function or value" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.ValueOrFunction)) 119 | 120 | yield! partMembers "Type extensions" "Type extension" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.TypeExtension)) 121 | 122 | yield! partMembers "Active patterns" "Active pattern" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.ActivePattern)) 123 | ] 124 | ] 125 | ] 126 | ] 127 | -------------------------------------------------------------------------------- /docsTool/templates/namespaces.fs: -------------------------------------------------------------------------------- 1 | module Namespaces 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | 8 | 9 | type ByCategory = { 10 | Name : string 11 | Index : string 12 | Types : Type array 13 | Modules : Module array 14 | } 15 | 16 | let generateNamespaceDocs (asm : AssemblyGroup) (props) = 17 | let parts = 18 | asm.Namespaces 19 | |> Seq.mapi(fun nsi ns -> 20 | let allByCategories = 21 | ns.Types 22 | |> Seq.map(fun t -> t.Category) 23 | |> Seq.append (ns.Modules |> Seq.map(fun m -> m.Category)) 24 | |> Seq.distinct 25 | |> Seq.sortBy(fun s -> 26 | if String.IsNullOrEmpty(s) then "ZZZ" 27 | else s) 28 | |> Seq.mapi(fun ci c -> 29 | { 30 | Name = if String.IsNullOrEmpty(c) then "Other namespace members" else c 31 | Index = sprintf "%d_%d" nsi ci 32 | Types = ns.Types |> Seq.filter(fun t -> t.Category = c) |> Seq.toArray 33 | Modules = ns.Modules |> Seq.filter(fun m -> m.Category = c) |> Seq.toArray 34 | }) 35 | |> Seq.filter(fun c -> c.Types.Length + c.Modules.Length > 0) 36 | |> Seq.toArray 37 | [ 38 | yield h2 [] [ 39 | Helpers.createAnchor ns.Name ns.Name 40 | ] 41 | if allByCategories.Length > 1 then 42 | yield ul [] [ 43 | for c in allByCategories do 44 | yield 45 | li [] [ 46 | a [Href (sprintf "#section%s" c.Index)] [ 47 | str c.Name 48 | ] 49 | ] 50 | ] 51 | 52 | 53 | for c in allByCategories do 54 | if allByCategories.Length > 1 then 55 | yield h3 [] [ 56 | a [Class "anchor"; Name (sprintf "section%s" c.Index); Href (sprintf "#section%s" c.Index)] [ 57 | str c.Name 58 | ] 59 | ] 60 | yield! PartNested.partNested c.Types c.Modules 61 | ] 62 | ) 63 | |> Seq.collect id 64 | div [ Class "container-fluid py-3" ] [ 65 | div [ Class "row" ] [ 66 | div [ Class "col-12" ] [ 67 | yield h1 [] [ 68 | Helpers.createAnchor asm.Name asm.Name 69 | ] 70 | yield! parts 71 | ] 72 | ] 73 | ] 74 | -------------------------------------------------------------------------------- /docsTool/templates/nav.fs: -------------------------------------------------------------------------------- 1 | module Nav 2 | 3 | open System 4 | open DocsTool 5 | open Fable.React 6 | open Fable.React.Props 7 | 8 | type NameOfArticle = string 9 | type UrlPath = string 10 | 11 | type TopLevelNav = { 12 | DocsRoot : IO.DirectoryInfo 13 | DocsPages : IO.FileInfo list 14 | } 15 | 16 | type NavConfig = { 17 | SiteBaseUrl : Uri 18 | GitHubRepoUrl : Uri 19 | ProjectName : string 20 | TopLevelNav : TopLevelNav 21 | } 22 | 23 | let normalizeText text = 24 | System.Text.RegularExpressions.Regex.Replace(text, @"[^0-9a-zA-Z\.]+", " ") 25 | 26 | let normalizeStr = normalizeText >> str 27 | 28 | let navItem link inner = 29 | li [ 30 | Class "nav-item" 31 | ] [ 32 | a [ 33 | Class "nav-link" 34 | Href link 35 | ] inner 36 | ] 37 | 38 | let navItemText text link = 39 | navItem link [ normalizeStr text ] 40 | 41 | let navItemIconOnly link ariaLabel inner = 42 | li [Class "nav-item"] [ 43 | a [ 44 | Class "nav-link" 45 | HTMLAttr.Custom("aria-label", ariaLabel) 46 | Href link 47 | ] inner 48 | ] 49 | 50 | let dropDownNavMenu text items = 51 | li [ Class "nav-item dropdown" ][ 52 | a [ 53 | Id (sprintf "navbarDropdown-%s" text) 54 | Href "#" 55 | DataToggle "dropdown" 56 | AriaHasPopup true 57 | AriaExpanded false 58 | Class "nav-link dropdown-toggle" ] 59 | [ normalizeStr text ] 60 | ul [ HTMLAttr.Custom ("aria-labelledby", "dropdownMenu1") 61 | Class "dropdown-menu border-0 shadow" ] items ] 62 | 63 | let dropDownNavItem text link = 64 | li [ 65 | Class "nav-item" 66 | ] [ 67 | a [ 68 | Class "dropdown-item" 69 | Href link 70 | ] [ 71 | normalizeStr text 72 | ] 73 | ] 74 | let dropdownSubMenu text items = 75 | li [ Class "dropdown-submenu" ] [ 76 | a [ Id (sprintf "navbarDropdown-%s" text) 77 | Href "#" 78 | Role "button" 79 | DataToggle "dropdown" 80 | AriaHasPopup true 81 | AriaExpanded false 82 | Class "dropdown-item dropdown-toggle" ] [ 83 | normalizeStr text ] 84 | ul [ 85 | HTMLAttr.Custom ("aria-labelledby", "dropdownMenu2") 86 | Class "dropdown-menu border-0 shadow" ] items 87 | ] 88 | 89 | type NavTree = 90 | | File of title:string * link:string 91 | | Folder of title: string * NavTree list 92 | 93 | let rec sortNavTree (navtree : NavTree list) = 94 | navtree 95 | |> List.map(fun navTree -> 96 | match navTree with 97 | | File (t,l) -> File (t,l) 98 | | Folder(title, nodes) -> Folder(title, sortNavTree nodes) 99 | ) 100 | |> List.sortBy(fun navtree -> 101 | match navtree with 102 | | File(title,_) -> title 103 | | Folder(title, _) -> title 104 | ) 105 | 106 | let navTreeFromPaths (rootPath : IO.DirectoryInfo) (files : IO.FileInfo list) = 107 | let rec addPath subFilePath parts nodes = 108 | match parts with 109 | | [] -> nodes 110 | | hp :: tp -> 111 | addHeadPath subFilePath hp tp nodes 112 | and addHeadPath subFilePath (part : string) remainingParts (nodes : NavTree list)= 113 | match nodes with 114 | | [] -> 115 | if part.EndsWith("html") then 116 | File(IO.Path.GetFileNameWithoutExtension part, subFilePath) 117 | else 118 | Folder(part, addPath subFilePath remainingParts []) 119 | |> List.singleton 120 | | Folder(title, subnodes) :: nodes when title = part -> Folder(title, addPath subFilePath remainingParts subnodes ) :: nodes 121 | | hn :: tn -> hn :: addHeadPath subFilePath part remainingParts tn 122 | 123 | ([], files) 124 | ||> List.fold(fun state file -> 125 | let subFilePath = file.FullName.Replace(rootPath.FullName, "") 126 | let pathParts = subFilePath.Split(IO.Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries) |> Array.toList 127 | addPath subFilePath pathParts state 128 | ) 129 | 130 | 131 | 132 | let generateNavMenus siteBaseUrl (navTree : NavTree list) = 133 | let rec innerDo depth (navTree : NavTree list) = 134 | navTree 135 | |> List.map(fun nav -> 136 | match nav with 137 | | File (title, link) when depth = 0 -> navItemText title (siteBaseUrl |> Uri.simpleCombine link) 138 | | File (title, link) -> dropDownNavItem title (siteBaseUrl |> Uri.simpleCombine link) 139 | | Folder (title, subtree) when depth = 0 -> 140 | innerDo (depth + 1) subtree 141 | |> dropDownNavMenu title 142 | | Folder (title, subtree) -> 143 | innerDo (depth + 1) subtree 144 | |> dropdownSubMenu title 145 | ) 146 | innerDo 0 navTree 147 | 148 | 149 | 150 | let generateNav (navCfg : NavConfig) = 151 | nav [ 152 | Class "navbar navbar-expand-md sticky-top navbar-dark bg-dark" 153 | ] [ 154 | a [ 155 | Class "navbar-brand" 156 | Href (navCfg.SiteBaseUrl |> Uri.simpleCombine "/index.html") 157 | ] [ 158 | i [ Class "fa fa-car text-white mr-2"] [] 159 | str (navCfg.ProjectName) 160 | ] 161 | button [ 162 | Class "navbar-toggler" 163 | Type "button" 164 | DataToggle "collapse" 165 | HTMLAttr.Custom("data-target","#navbarNav" ) 166 | HTMLAttr.Custom("aria-controls","navbarNav" ) 167 | HTMLAttr.Custom("aria-expanded","false" ) 168 | HTMLAttr.Custom("aria-label","Toggle navigation" ) 169 | ] [ 170 | span [Class "navbar-toggler-icon"] [] 171 | ] 172 | div [ Class "collapse navbar-collapse" 173 | Id "navbarNav" ] [ 174 | ul [ Class "navbar-nav mr-auto" ] [ 175 | yield! navTreeFromPaths navCfg.TopLevelNav.DocsRoot navCfg.TopLevelNav.DocsPages |> sortNavTree |> generateNavMenus navCfg.SiteBaseUrl 176 | ] 177 | ul [ Class "navbar-nav"] [ 178 | button [Id "theme-toggle"; Class ""] [ 179 | str "" 180 | ] 181 | navItemIconOnly (string navCfg.GitHubRepoUrl) (sprintf "%s Repository on Github" navCfg.ProjectName) [ 182 | i [ Class "fab fa-github fa-lg fa-fw text-light"] [] 183 | ] 184 | ] 185 | ] 186 | ] 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /docsTool/templates/partMembers.fs: -------------------------------------------------------------------------------- 1 | module PartMembers 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open System.Collections.Generic 8 | open Helpers 9 | 10 | type ModuleByCategory = { 11 | Index : int 12 | GroupKey : string 13 | Members : list 14 | Name : string 15 | } 16 | 17 | 18 | let signature (m : Member) = seq { 19 | if m.Details.Signature |> String.IsNullOrEmpty |> not then 20 | yield 21 | code [ Class "function-or-value"] [ 22 | str m.Details.Signature 23 | ] 24 | } 25 | 26 | let repoSourceLink (m: Member) = seq { 27 | if m.Details.FormatSourceLocation |> String.IsNullOrEmpty |> not then 28 | yield a [ 29 | Href m.Details.FormatSourceLocation 30 | Class "float-right" 31 | HTMLAttr.Custom("aria-label", "View source on GitHub") 32 | ] [ 33 | yield i [ 34 | Class "fab fa-github text-dark" 35 | ] [] 36 | ] 37 | } 38 | 39 | let replaceh2withh5 (content : string) = 40 | content.Replace("

", "

") 41 | 42 | 43 | let normalize (content : string) = 44 | content 45 | |> replaceh2withh5 46 | 47 | 48 | 49 | let commentBlock (c: Comment) = 50 | let (|EmptyDefaultBlock|NonEmptyDefaultBlock|Section|) (KeyValue(section, content)) = 51 | match section, content with 52 | | "", c when String.IsNullOrEmpty c -> EmptyDefaultBlock 53 | | "", c -> NonEmptyDefaultBlock c 54 | | section, content -> Section (section, content) 55 | 56 | let renderSection (s : KeyValuePair): Fable.React.ReactElement list = 57 | match s with 58 | | EmptyDefaultBlock -> [] 59 | | NonEmptyDefaultBlock content -> [ div [ Class "comment-block" ] [ RawText (normalize content) ] ] 60 | | Section(name, content) -> [ h5 [] [ str name ] // h2 is obnoxiously large for this context, go with the smaller h5 61 | RawText (normalize content) ] 62 | c.Sections 63 | |> List.collect renderSection 64 | 65 | let compiledName (m: Member) = seq { 66 | if m.Details.FormatCompiledName |> String.IsNullOrEmpty |> not then 67 | yield p [] [ 68 | strong [] [ str "CompiledName:" ] 69 | code [] [ str m.Details.FormatCompiledName ] 70 | ] 71 | } 72 | 73 | let partMembers (header : string) (tableHeader : string) (members : #seq) = [ 74 | if members |> Seq.length > 0 then 75 | yield h3 [] [ 76 | str header 77 | ] 78 | 79 | yield table [ 80 | Class "table" 81 | ] [ 82 | thead [] [ 83 | 84 | tr [] [ 85 | th [Class "fit"] [ 86 | 87 | ] 88 | th [] [ 89 | str tableHeader 90 | ] 91 | 92 | th [] [ 93 | str "Signature" 94 | ] 95 | 96 | th [] [ 97 | str "Description" 98 | ] 99 | ] 100 | ] 101 | tbody [] [ 102 | for it in members do 103 | let id = Guid.NewGuid().ToString() 104 | yield tr [] [ 105 | td [] [ 106 | Helpers.createAnchorIcon (it.Details.FormatUsage(40)) 107 | ] 108 | td [ 109 | Class "member-name" 110 | ] [ 111 | code [ 112 | Class "function-or-value" 113 | HTMLAttr.Custom("data-guid", id) 114 | ] [ 115 | str (it.Details.FormatUsage(40)) 116 | ] 117 | ] 118 | td [ 119 | Class "member-name" 120 | ] [ 121 | yield! signature it 122 | ] 123 | 124 | td [ 125 | Class "xmldoc" 126 | ] [ 127 | yield! renderObsoleteMessage it 128 | yield! repoSourceLink it 129 | yield! commentBlock it.Comment 130 | yield! compiledName it 131 | ] 132 | ] 133 | ] 134 | ] 135 | ] 136 | -------------------------------------------------------------------------------- /docsTool/templates/partNested.fs: -------------------------------------------------------------------------------- 1 | module PartNested 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open Helpers 8 | 9 | let partNested (types : Type array) (modules : Module array) = 10 | [ 11 | if types.Length > 0 then 12 | yield table [ Class "table" ] [ 13 | thead [] [ 14 | tr [] [ 15 | th [Class "fit"] [ 16 | 17 | ] 18 | th [] [ 19 | str "Type" 20 | ] 21 | th [] [ 22 | str "Description" 23 | ] 24 | ] 25 | ] 26 | tbody [] [ 27 | for t in types do 28 | yield tr [] [ 29 | td [] [ 30 | Helpers.createAnchorIcon t.Name 31 | ] 32 | td [Class "type-name"] [ 33 | a [Href (sprintf "%s.html" t.UrlName)] [ 34 | str t.Name 35 | ] 36 | ] 37 | td [Class "xmldoc"] [ 38 | yield! renderObsoleteMessage t 39 | yield RawText t.Comment.Blurb 40 | ] 41 | ] 42 | ] 43 | ] 44 | if modules.Length > 0 then 45 | yield table [ Class "table" ] [ 46 | thead [] [ 47 | tr [] [ 48 | th [Class "fit"] [ 49 | 50 | ] 51 | th [] [ 52 | str "Module" 53 | ] 54 | th [] [ 55 | str "Description" 56 | ] 57 | ] 58 | ] 59 | tbody [] [ 60 | for t in modules do 61 | yield tr [] [ 62 | td [] [ 63 | Helpers.createAnchorIcon t.Name 64 | ] 65 | td [Class "Modules-name"] [ 66 | a [Href (sprintf "%s.html" t.UrlName)] [ 67 | str t.Name 68 | ] 69 | ] 70 | td [Class "xmldoc"] [ 71 | yield! renderObsoleteMessage t 72 | yield RawText t.Comment.Blurb 73 | ] 74 | ] 75 | ] 76 | ] 77 | ] 78 | -------------------------------------------------------------------------------- /docsTool/templates/types.fs: -------------------------------------------------------------------------------- 1 | module Types 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open PartMembers 8 | open Helpers 9 | 10 | let generateTypeDocs (model : TypeInfo) (props) = 11 | let members = model.Type.AllMembers 12 | let comment = model.Type.Comment 13 | let ``type`` = model.Type 14 | let byCategory = 15 | members 16 | |> List.groupBy (fun m -> m.Category) 17 | |> List.sortBy (fun (k,v) -> if String.IsNullOrEmpty(k) then "ZZZ" else k ) 18 | |> List.mapi (fun i (k,v) -> { 19 | Index = i 20 | GroupKey = k 21 | Members = v |> List.sortBy (fun m -> if m.Kind = MemberKind.StaticParameter then "" else m.Name) 22 | Name = if String.IsNullOrEmpty(k) then "Other type members" else k 23 | }) 24 | [ 25 | yield h1 [] [ 26 | str model.Type.Name 27 | ] 28 | 29 | yield p [] [ 30 | yield! renderObsoleteMessage model.Type 31 | yield! renderNamespace model.Namespace 32 | if model.HasParentModule then 33 | yield br [] 34 | yield span [] [ 35 | str "Parent Module: " 36 | 37 | a [ 38 | Href (sprintf "%s.html" model.ParentModule.Value.UrlName) 39 | ] [ 40 | str model.ParentModule.Value.Name 41 | ] 42 | ] 43 | 44 | 45 | if ``type``.Attributes |> Seq.isEmpty |> not then 46 | yield br [] 47 | yield span [] [ 48 | yield str "Attributes: " 49 | 50 | yield br [] 51 | 52 | for attr in ``type``.Attributes do 53 | yield str (attr.Format()) 54 | yield br [] 55 | ] 56 | ] 57 | 58 | yield div [ 59 | Class "xmldoc" 60 | ] [ 61 | for sec in comment.Sections do 62 | if byCategory |> Seq.exists (fun m -> m.GroupKey = sec.Key) |> not then 63 | if sec.Key <> "" then 64 | yield h2 [] [ 65 | str sec.Key 66 | ] 67 | yield RawText sec.Value 68 | ] 69 | 70 | if byCategory |> Seq.length > 1 then 71 | yield h2 [] [ 72 | str "Table of contents" 73 | ] 74 | 75 | yield ul [] [ 76 | for g in byCategory do 77 | yield li [] [ 78 | a [ 79 | Href (sprintf "#section%d" g.Index) 80 | ] [ 81 | str g.Name 82 | ] 83 | ] 84 | ] 85 | 86 | for g in byCategory do 87 | if byCategory |> Seq.length > 1 then 88 | yield h2 [] [ 89 | str g.Name 90 | 91 | a [ 92 | Name (sprintf "section%d" g.Index) 93 | ] [ 94 | str " " 95 | ] 96 | ] 97 | 98 | match comment.Sections |> Seq.tryFind (fun kvp -> kvp.Key = g.GroupKey) with 99 | | Some info -> 100 | yield div [ 101 | Class "xmldoc" 102 | ] [ 103 | str info.Value 104 | ] 105 | | None -> yield nothing 106 | 107 | yield! partMembers "Union Cases" "Union Case" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.UnionCase)) 108 | yield! partMembers "Record Fields" "Record Field" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.RecordField)) 109 | yield! partMembers "Static parameters" "Static parameters" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.StaticParameter)) 110 | yield! partMembers "Contructors" "Constructor" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.Constructor)) 111 | yield! partMembers "Instance members" "Instance member" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.InstanceMember)) 112 | yield! partMembers "Static members" "Static member" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.StaticMember)) 113 | ] 114 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0.100" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://www.nuget.org/api/v2 2 | source https://api.nuget.org/v3/index.json 3 | source paket-files/github.com/fsharp/FsAutoComplete/lib 4 | storage: none 5 | clitool dotnet-mono 0.5.2 6 | 7 | git https://github.com/fsharp/FsAutoComplete.git master build:"build.cmd", Packages: /bin/pkgs/, OS: win 8 | git https://github.com/fsharp/FsAutoComplete.git master build:"build.sh", Packages: /bin/pkgs/, OS: osx 9 | git https://github.com/fsharp/FsAutoComplete.git master build:"build.sh", Packages: /bin/pkgs/, OS: linux 10 | 11 | nuget FSharp.Analyzers.SDK 0.8.0 12 | nuget FSharp.Compiler.Service 39.0.0 13 | nuget Ionide.ProjInfo 14 | nuget Ionide.ProjInfo.FCS 15 | // Have to install this exact version, this will change with the .net SDK version :( 16 | nuget NuGet.Frameworks 5.8.0 17 | nuget NuGet.ProjectModel 5.8.0 strategy: min 18 | 19 | github TheAngryByrd/FsLibLog:0dc37e8471dcece8f89d3f8cc9191be206d6fb01 src/FsLibLog/FsLibLog.fs 20 | nuget FSharp.Core 5.0.1 21 | nuget Microsoft.SourceLink.GitHub 1.0.0 copy_local: true 22 | nuget Expecto 9.0.2 23 | nuget YoloDev.Expecto.TestSdk 0.11.1 24 | nuget Microsoft.NET.Test.Sdk 16.8.0 25 | nuget altcover ~> 7 26 | 27 | // [ FAKE GROUP ] 28 | group Build 29 | storage: none 30 | source https://www.nuget.org/api/v2 31 | source https://api.nuget.org/v3/index.json 32 | nuget Fake.IO.FileSystem 5.20.4-alpha.1642 33 | nuget Fake.Core.Target 5.20.4-alpha.1642 34 | nuget Fake.Core.ReleaseNotes 5.20.4-alpha.1642 35 | nuget FAKE.Core.Environment 5.20.4-alpha.1642 36 | nuget Fake.DotNet.Cli 5.20.4-alpha.1642 37 | nuget FAKE.Core.Process 5.20.4-alpha.1642 38 | nuget Fake.DotNet.AssemblyInfoFile 5.20.4-alpha.1642 39 | nuget Fake.Tools.Git 5.20.4-alpha.1642 40 | nuget Fake.DotNet.Paket 5.20.4-alpha.1642 41 | nuget Fake.Api.GitHub 5.20.4-alpha.1642 42 | nuget Fake.BuildServer.AppVeyor 5.20.4-alpha.1642 43 | nuget Fake.BuildServer.Travis 5.20.4-alpha.1642 44 | nuget Fantomas 45 | nuget Fantomas.Extras 46 | nuget Argu 47 | nuget Octokit 0.48.0 48 | 49 | group Docs 50 | storage: none 51 | source https://www.nuget.org/api/v2 52 | source https://api.nuget.org/v3/index.json 53 | nuget Argu 54 | nuget FSharp.Core 55 | nuget Fake.IO.FileSystem 56 | nuget FAKE.Core.Environment 57 | nuget Fake.DotNet.Cli 58 | nuget FSharp.Literate 3.1.0 59 | nuget Fable.React 60 | nuget Microsoft.AspNetCore.StaticFiles 61 | nuget Microsoft.AspNetCore.Hosting 62 | nuget Microsoft.AspNetCore.Server.Kestrel 63 | nuget Microsoft.AspNetCore.WebSockets 64 | nuget Dotnet.ProjInfo.Workspace.FCS 65 | -------------------------------------------------------------------------------- /src/BinaryDefense.FSharp.Analyzers.Hashing/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "BinaryDefense.FSharp.Analyzers.Hashing" 17 | let [] AssemblyProduct = "BinaryDefense.FSharp.Analyzers" 18 | let [] AssemblyVersion = "0.2.2" 19 | let [] AssemblyMetadata_ReleaseDate = "2021-05-23T00:00:00.0000000" 20 | let [] AssemblyFileVersion = "0.2.2" 21 | let [] AssemblyInformationalVersion = "0.2.2" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "ce6714e40c39d4a88cba9b4cbe0b5f93e2c1b285" 24 | -------------------------------------------------------------------------------- /src/BinaryDefense.FSharp.Analyzers.Hashing/BinaryDefense.FSharp.Analyzers.Hashing.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | 7 | 8 | BinaryDefense.FSharp.Analyzers.Hashing 9 | Looks for insecure hashing functions 10 | 11 | 12 | true 13 | true 14 | 15 | 16 | 17 | True 18 | paket-files/FsLibLog.fs 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/BinaryDefense.FSharp.Analyzers.Hashing/Library.fs: -------------------------------------------------------------------------------- 1 | namespace BinaryDefense.FSharp.Analyzers 2 | open BinaryDefense.Logging 3 | open BinaryDefense.Logging.Types 4 | open System.Security.Cryptography 5 | 6 | module Hashing = 7 | open System 8 | open FSharp.Analyzers.SDK 9 | open FSharp.Compiler.SourceCodeServices 10 | open FSharp.Compiler.Text 11 | 12 | let rec visitExpr memberCallHandler (e:FSharpExpr) = 13 | match e with 14 | | BasicPatterns.AddressOf(lvalueExpr) -> 15 | visitExpr memberCallHandler lvalueExpr 16 | | BasicPatterns.AddressSet(lvalueExpr, rvalueExpr) -> 17 | visitExpr memberCallHandler lvalueExpr; visitExpr memberCallHandler rvalueExpr 18 | | BasicPatterns.Application(funcExpr, typeArgs, argExprs) -> 19 | visitExpr memberCallHandler funcExpr; visitExprs memberCallHandler argExprs 20 | | BasicPatterns.Call(objExprOpt, memberOrFunc, typeArgs1, typeArgs2, argExprs) -> 21 | memberCallHandler e.Range memberOrFunc 22 | visitObjArg memberCallHandler objExprOpt; visitExprs memberCallHandler argExprs 23 | | BasicPatterns.Coerce(targetType, inpExpr) -> 24 | visitExpr memberCallHandler inpExpr 25 | | BasicPatterns .FastIntegerForLoop(startExpr, limitExpr, consumeExpr, isUp) -> 26 | visitExpr memberCallHandler startExpr; visitExpr memberCallHandler limitExpr; visitExpr memberCallHandler consumeExpr 27 | | BasicPatterns.ILAsm(asmCode, typeArgs, argExprs) -> 28 | visitExprs memberCallHandler argExprs 29 | | BasicPatterns.ILFieldGet (objExprOpt, fieldType, fieldName) -> 30 | visitObjArg memberCallHandler objExprOpt 31 | | BasicPatterns.ILFieldSet (objExprOpt, fieldType, fieldName, valueExpr) -> 32 | visitObjArg memberCallHandler objExprOpt 33 | | BasicPatterns.IfThenElse (guardExpr, thenExpr, elseExpr) -> 34 | visitExpr memberCallHandler guardExpr; visitExpr memberCallHandler thenExpr; visitExpr memberCallHandler elseExpr 35 | | BasicPatterns.Lambda(lambdaVar, bodyExpr) -> 36 | visitExpr memberCallHandler bodyExpr 37 | | BasicPatterns.Let((bindingVar, bindingExpr), bodyExpr) -> 38 | visitExpr memberCallHandler bindingExpr; visitExpr memberCallHandler bodyExpr 39 | | BasicPatterns.LetRec(recursiveBindings, bodyExpr) -> 40 | List.iter (snd >> visitExpr memberCallHandler) recursiveBindings; visitExpr memberCallHandler bodyExpr 41 | | BasicPatterns.NewArray(arrayType, argExprs) -> 42 | visitExprs memberCallHandler argExprs 43 | | BasicPatterns.NewDelegate(delegateType, delegateBodyExpr) -> 44 | visitExpr memberCallHandler delegateBodyExpr 45 | | BasicPatterns.NewObject(objType, typeArgs, argExprs) -> 46 | memberCallHandler e.Range objType 47 | visitExprs memberCallHandler argExprs 48 | | BasicPatterns.NewRecord(recordType, argExprs) -> 49 | visitExprs memberCallHandler argExprs 50 | | BasicPatterns.NewTuple(tupleType, argExprs) -> 51 | visitExprs memberCallHandler argExprs 52 | | BasicPatterns.NewUnionCase(unionType, unionCase, argExprs) -> 53 | visitExprs memberCallHandler argExprs 54 | | BasicPatterns.Quote(quotedExpr) -> 55 | visitExpr memberCallHandler quotedExpr 56 | | BasicPatterns.FSharpFieldGet(objExprOpt, recordOrClassType, fieldInfo) -> 57 | visitObjArg memberCallHandler objExprOpt 58 | | BasicPatterns.FSharpFieldSet(objExprOpt, recordOrClassType, fieldInfo, argExpr) -> 59 | visitObjArg memberCallHandler objExprOpt; visitExpr memberCallHandler argExpr 60 | | BasicPatterns.Sequential(firstExpr, secondExpr) -> 61 | visitExpr memberCallHandler firstExpr; visitExpr memberCallHandler secondExpr 62 | | BasicPatterns.TryFinally(bodyExpr, finalizeExpr) -> 63 | visitExpr memberCallHandler bodyExpr; visitExpr memberCallHandler finalizeExpr 64 | | BasicPatterns.TryWith(bodyExpr, _, _, catchVar, catchExpr) -> 65 | visitExpr memberCallHandler bodyExpr; visitExpr memberCallHandler catchExpr 66 | | BasicPatterns.TupleGet(tupleType, tupleElemIndex, tupleExpr) -> 67 | visitExpr memberCallHandler tupleExpr 68 | | BasicPatterns.DecisionTree(decisionExpr, decisionTargets) -> 69 | visitExpr memberCallHandler decisionExpr; List.iter (snd >> visitExpr memberCallHandler) decisionTargets 70 | | BasicPatterns.DecisionTreeSuccess (decisionTargetIdx, decisionTargetExprs) -> 71 | visitExprs memberCallHandler decisionTargetExprs 72 | | BasicPatterns.TypeLambda(genericParam, bodyExpr) -> 73 | visitExpr memberCallHandler bodyExpr 74 | | BasicPatterns.TypeTest(ty, inpExpr) -> 75 | visitExpr memberCallHandler inpExpr 76 | | BasicPatterns.UnionCaseSet(unionExpr, unionType, unionCase, unionCaseField, valueExpr) -> 77 | visitExpr memberCallHandler unionExpr; visitExpr memberCallHandler valueExpr 78 | | BasicPatterns.UnionCaseGet(unionExpr, unionType, unionCase, unionCaseField) -> 79 | visitExpr memberCallHandler unionExpr 80 | | BasicPatterns.UnionCaseTest(unionExpr, unionType, unionCase) -> 81 | visitExpr memberCallHandler unionExpr 82 | | BasicPatterns.UnionCaseTag(unionExpr, unionType) -> 83 | visitExpr memberCallHandler unionExpr 84 | | BasicPatterns.ObjectExpr(objType, baseCallExpr, overrides, interfaceImplementations) -> 85 | visitExpr memberCallHandler baseCallExpr 86 | List.iter (visitObjMember memberCallHandler) overrides 87 | List.iter (snd >> List.iter (visitObjMember memberCallHandler)) interfaceImplementations 88 | | BasicPatterns.TraitCall(sourceTypes, traitName, typeArgs, typeInstantiation, argTypes, argExprs) -> 89 | visitExprs memberCallHandler argExprs 90 | | BasicPatterns.ValueSet(valToSet, valueExpr) -> 91 | visitExpr memberCallHandler valueExpr 92 | | BasicPatterns.WhileLoop(guardExpr, bodyExpr) -> 93 | visitExpr memberCallHandler guardExpr; visitExpr memberCallHandler bodyExpr 94 | | BasicPatterns.BaseValue baseType -> 95 | () 96 | | BasicPatterns.DefaultValue defaultType -> 97 | () 98 | | BasicPatterns.ThisValue thisType -> 99 | () 100 | | BasicPatterns.Const(constValueObj, constType) -> 101 | () 102 | | BasicPatterns.Value(valueToGet) -> 103 | memberCallHandler e.Range valueToGet 104 | | other -> 105 | () 106 | 107 | and visitExprs f exprs = 108 | List.iter (visitExpr f) exprs 109 | 110 | and visitObjArg f objOpt = 111 | Option.iter (visitExpr f) objOpt 112 | 113 | and visitObjMember f memb = 114 | visitExpr f memb.Body 115 | 116 | let rec visitDeclaration f d = 117 | match d with 118 | | FSharpImplementationFileDeclaration.Entity (e, subDecls) -> 119 | for subDecl in subDecls do 120 | visitDeclaration f subDecl 121 | | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(v, vs, e) -> 122 | visitExpr f e 123 | | FSharpImplementationFileDeclaration.InitAction(e) -> 124 | visitExpr f e 125 | 126 | type WeakHash = 127 | | MD5 128 | | SHA1 129 | with 130 | override x.ToString() = 131 | match x with 132 | | MD5 -> "MD5" 133 | | SHA1 -> "SHA1" 134 | 135 | let toCode = function 136 | | MD5 -> "BDH0001" 137 | | SHA1 -> "BDH0002" 138 | 139 | // let waitForDebuggerAttached (programName) = 140 | // #if DEBUG 141 | // if not(System.Diagnostics.Debugger.IsAttached) then 142 | // printfn "Please attach a debugger for %s, PID: %d" programName (System.Diagnostics.Process.GetCurrentProcess().Id) 143 | // while not(System.Diagnostics.Debugger.IsAttached) do 144 | // System.Threading.Thread.Sleep(100) 145 | // System.Diagnostics.Debugger.Break() 146 | // #else 147 | // () 148 | // #endif 149 | 150 | let matchers = 151 | [ 152 | typeof.FullName, MD5 153 | (sprintf "%s.Create" typeof.FullName), MD5 154 | typeof.FullName, SHA1 155 | (sprintf "%s.Create" typeof.FullName), SHA1 156 | ] |> dict 157 | 158 | [] 159 | let weakHashingAnalyzer : Analyzer = 160 | // waitForDebuggerAttached ("App") 161 | fun ctx -> 162 | if ctx.FileName.EndsWith("AssemblyInfo.fs") then 163 | [] 164 | else 165 | let state = ResizeArray() 166 | 167 | let handler (range: range) (m: FSharpMemberOrFunctionOrValue) = 168 | let logger = LogProvider.getLoggerByName "BinaryDefense.FSharp.Analyzers.Hashing.handler" 169 | // logger.info( 170 | // Log.setMessage "Hello World!" 171 | // ) 172 | // logger.debug( 173 | // Log.setMessage "Handler hit {FullTypeSafe}" 174 | // ) 175 | let name = 176 | if m.DeclaringEntity.IsSome then 177 | String.Join(".", m.DeclaringEntity.Value.FullName, m.DisplayName) 178 | else 179 | m.FullTypeSafe.Value.Format(FSharpDisplayContext.Empty) 180 | 181 | match 182 | matchers 183 | |> Seq.tryFind(fun k -> name.Contains(k.Key)) 184 | with 185 | | Some v -> 186 | state.Add (v.Value, range) 187 | | _ -> () 188 | ctx.TypedTree.Declarations |> List.iter (visitDeclaration handler) 189 | state 190 | |> Seq.map (fun (hash, r) -> 191 | { Type = "Weak hashing analyzer" 192 | Message = sprintf "%A shouldn't be used. Consider changing to SHA256 or SHA512." hash 193 | Code = toCode hash 194 | Severity = Warning 195 | Range = r 196 | Fixes = []} 197 | ) 198 | |> Seq.toList 199 | -------------------------------------------------------------------------------- /src/BinaryDefense.FSharp.Analyzers.Hashing/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Microsoft.SourceLink.GitHub 3 | FSharp.Analyzers.SDK 4 | File:FsLibLog.fs 5 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | false 5 | 6 | true 7 | true 8 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/BinaryDefense.FSharp.Analyzers.Tests/Analyzer.fs: -------------------------------------------------------------------------------- 1 | module AnalyzerBootstrap 2 | 3 | open System 4 | open System.IO 5 | open FSharp.Compiler.SourceCodeServices 6 | open FSharp.Compiler.Text 7 | open FSharp.Analyzers.SDK 8 | open Ionide.ProjInfo 9 | open Ionide.ProjInfo.Types 10 | 11 | 12 | let checker = 13 | FSharpChecker.Create( 14 | projectCacheSize = 200, 15 | keepAllBackgroundResolutions = true, 16 | keepAssemblyContents = true, 17 | ImplicitlyStartBackgroundWork = true) 18 | 19 | // let projectSystem = ProjectController(checker) 20 | 21 | let dumpOpts (opts : FSharpProjectOptions) = 22 | printfn "FSharpProjectOptions.OtherOptions ->" 23 | opts.OtherOptions 24 | |> Array.iter(printfn "%s") 25 | 26 | /// This will fix relative paths and make them absolute 27 | let toAbsolutePath path = 28 | FileInfo(path).FullName 29 | 30 | 31 | let loadProject toolsPath projPath = 32 | async { 33 | let projPath = toAbsolutePath projPath 34 | let loader = WorkspaceLoader.Create(toolsPath) 35 | // Uncomment for debugging 36 | loader.Notifications |> Observable.add(fun e -> printfn "%A" e) 37 | let parsed = loader.LoadProjects [projPath] |> Seq.toList 38 | let opts = FCS.mapToFSharpProjectOptions parsed.Head parsed 39 | return 40 | opts.SourceFiles 41 | |> Array.map(fun file -> 42 | file, opts 43 | ) 44 | } |> Async.RunSynchronously 45 | 46 | let loadFile file = 47 | async { 48 | let! source = IO.File.ReadAllTextAsync file |> Async.AwaitTask 49 | let! (opts, error) = checker.GetProjectOptionsFromScript(file, SourceText.ofString source, assumeDotNetFramework = false, useSdkRefs = true, useFsiAuxLib = true, otherFlags = [|"--targetprofile:netstandard" |]) 50 | //HACK: fixes references on windows. See https://github.com/fsharp/FSharp.Compiler.Service/issues/920#issuecomment-576355140 51 | let newOO = 52 | opts.OtherOptions 53 | |> Array.map(fun i -> 54 | if i.StartsWith("-r:") then 55 | i.Split("-r:", StringSplitOptions.RemoveEmptyEntries) 56 | |> Array.head 57 | |> toAbsolutePath 58 | |> sprintf "-r:%s" 59 | else 60 | i 61 | ) 62 | // dumpOpts opts 63 | return file,{ opts with OtherOptions = newOO} 64 | } |> Async.RunSynchronously 65 | 66 | let typeCheckFile (file,opts) = 67 | let text = File.ReadAllText file 68 | let st = SourceText.ofString text 69 | let (parseRes, checkAnswer) = 70 | checker.ParseAndCheckFileInProject(file, 1, st, opts) 71 | |> Async.RunSynchronously 72 | 73 | match checkAnswer with 74 | | FSharpCheckFileAnswer.Aborted -> 75 | // printError "Checking of file %s aborted" file 76 | None 77 | | FSharpCheckFileAnswer.Succeeded(c) -> 78 | Some (file, text, parseRes, c) 79 | 80 | let entityCache = EntityCache() 81 | 82 | let getAllEntities (checkResults: FSharpCheckFileResults) (publicOnly: bool) : AssemblySymbol list = 83 | try 84 | let res = [ 85 | yield! AssemblyContentProvider.getAssemblySignatureContent AssemblyContentType.Full checkResults.PartialAssemblySignature 86 | let ctx = checkResults.ProjectContext 87 | let assembliesByFileName = 88 | ctx.GetReferencedAssemblies() 89 | |> Seq.groupBy (fun asm -> asm.FileName) 90 | |> Seq.map (fun (fileName, asms) -> fileName, List.ofSeq asms) 91 | |> Seq.toList 92 | |> List.rev // if mscorlib.dll is the first then FSC raises exception when we try to 93 | // get Content.Entities from it. 94 | 95 | for fileName, signatures in assembliesByFileName do 96 | let contentType = if publicOnly then Public else Full 97 | let content = AssemblyContentProvider.getAssemblyContent entityCache.Locking contentType fileName signatures 98 | yield! content 99 | ] 100 | res 101 | with 102 | | _ -> [] 103 | 104 | let createContext (file, text: string, p: FSharpParseFileResults,c: FSharpCheckFileResults) = 105 | match p.ParseTree, c.ImplementationFile with 106 | | Some pt, Some tast -> 107 | let context : Context = { 108 | FileName = file 109 | Content = text.Split([|'\n'|]) 110 | ParseTree = pt 111 | TypedTree = tast 112 | Symbols = c.PartialAssemblySignature.Entities |> Seq.toList 113 | GetAllEntities = getAllEntities c 114 | } 115 | Some context 116 | | _ -> None 117 | 118 | let tee f x = 119 | f x 120 | x 121 | open Microsoft.FSharp.Reflection 122 | open System.Reflection 123 | let nullToolPath () = 124 | let case = 125 | FSharpType.GetUnionCases(typeof, BindingFlags.NonPublic) 126 | |> Seq.head 127 | 128 | 129 | FSharpValue.MakeUnion(case,[| box ""|], BindingFlags.NonPublic) :?> ToolsPath 130 | 131 | type Loader = 132 | | Project of string 133 | | File of string 134 | let toolsPath = lazy ( 135 | try 136 | Init.init () 137 | with e -> nullToolPath () 138 | ) 139 | let runProject (proj : Loader) (analyzers : Analyzer seq) = 140 | // let path = 141 | // Path.Combine(Environment.CurrentDirectory, proj) 142 | // |> Path.GetFullPath 143 | let files = 144 | match proj with 145 | | Project f -> loadProject toolsPath.Value f 146 | | File f -> [| loadFile f |] 147 | |> tee (fun fs -> fs |> Seq.iter(fun (file, opts) -> printfn "%s" file)) 148 | |> Array.choose typeCheckFile 149 | |> Array.choose createContext 150 | 151 | files 152 | |> Array.collect(fun ctx -> 153 | analyzers 154 | |> Seq.collect (fun analyzer -> analyzer ctx) 155 | |> Seq.toArray 156 | ) 157 | -------------------------------------------------------------------------------- /tests/BinaryDefense.FSharp.Analyzers.Tests/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "BinaryDefense.FSharp.Analyzers.Tests" 17 | let [] AssemblyProduct = "BinaryDefense.FSharp.Analyzers" 18 | let [] AssemblyVersion = "0.2.2" 19 | let [] AssemblyMetadata_ReleaseDate = "2021-05-23T00:00:00.0000000" 20 | let [] AssemblyFileVersion = "0.2.2" 21 | let [] AssemblyInformationalVersion = "0.2.2" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "ce6714e40c39d4a88cba9b4cbe0b5f93e2c1b285" 24 | -------------------------------------------------------------------------------- /tests/BinaryDefense.FSharp.Analyzers.Tests/BinaryDefense.FSharp.Analyzers.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | false 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/BinaryDefense.FSharp.Analyzers.Tests/Main.fs: -------------------------------------------------------------------------------- 1 | module ExpectoTemplate 2 | 3 | open Expecto 4 | 5 | [] 6 | let main argv = Tests.runTestsInAssembly defaultConfig argv 7 | -------------------------------------------------------------------------------- /tests/BinaryDefense.FSharp.Analyzers.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | module Tests 2 | 3 | open System 4 | open Expecto 5 | open BinaryDefense.FSharp.Analyzers 6 | open System.Runtime.Loader 7 | 8 | let analyzers = [ Hashing.weakHashingAnalyzer ] 9 | 10 | let testSeq name tests = testList name (List.ofSeq tests) 11 | 12 | 13 | [] 14 | let tests = 15 | testSeq "Hashing Tests" <| 16 | testParam analyzers [ 17 | "Check MD5.Create() binding", fun analyzers () -> 18 | let file = IO.Path.Combine(__SOURCE_DIRECTORY__ ,"../examples/hashing/md5create.fs") 19 | let results = (AnalyzerBootstrap.runProject (AnalyzerBootstrap.File file) analyzers) 20 | Expect.hasLength results 1 "" 21 | results 22 | |> Seq.iter(fun r -> 23 | Expect.stringContains r.Message "MD5" "" 24 | ) 25 | "Check MD5CryptoServiceProvider binding", fun analyzers () -> 26 | let file = IO.Path.Combine(__SOURCE_DIRECTORY__ ,"../examples/hashing/md5CryptoServicebinding.fs") 27 | let results = (AnalyzerBootstrap.runProject (AnalyzerBootstrap.File file) analyzers) 28 | Expect.hasLength results 4 "" 29 | results 30 | |> Seq.iter(fun r -> 31 | Expect.stringContains r.Message "MD5" "" 32 | ) 33 | "Check SHA1.Create() binding", fun analyzers () -> 34 | let file = IO.Path.Combine(__SOURCE_DIRECTORY__ ,"../examples/hashing/sha1create.fs") 35 | let results = (AnalyzerBootstrap.runProject (AnalyzerBootstrap.File file) analyzers) 36 | Expect.hasLength results 1 "" 37 | results 38 | |> Seq.iter(fun r -> 39 | Expect.stringContains r.Message "SHA1" "" 40 | ) 41 | "Check SHACryptoServiceProvider binding", fun analyzers () -> 42 | let file = IO.Path.Combine(__SOURCE_DIRECTORY__ ,"../examples/hashing/sha1CryptoServicebinding.fs") 43 | let results = (AnalyzerBootstrap.runProject (AnalyzerBootstrap.File file) analyzers) 44 | Expect.hasLength results 4 "" 45 | results 46 | |> Seq.iter(fun r -> 47 | Expect.stringContains r.Message "SHA1" "" 48 | ) 49 | "Check SHACryptoServiceProvider ctor", fun analyzers () -> 50 | let file = IO.Path.Combine(__SOURCE_DIRECTORY__ ,"../examples/hashing/sha1CryptoServicector.fs") 51 | let results = (AnalyzerBootstrap.runProject (AnalyzerBootstrap.File file) analyzers) 52 | Expect.hasLength results 1 "" 53 | results 54 | |> Seq.iter(fun r -> 55 | Expect.stringContains r.Message "SHA1" "" 56 | ) 57 | "Check project", fun analyzers () -> 58 | let file = IO.Path.Combine(__SOURCE_DIRECTORY__, "../examples/hashing/hashing.fsproj") 59 | let results = (AnalyzerBootstrap.runProject (AnalyzerBootstrap.Project file) analyzers) 60 | Expect.isGreaterThan results.Length 0 "" 61 | ] 62 | -------------------------------------------------------------------------------- /tests/BinaryDefense.FSharp.Analyzers.Tests/paket.references: -------------------------------------------------------------------------------- 1 | Expecto 2 | FSharp.Core 3 | Microsoft.NET.Test.Sdk 4 | YoloDev.Expecto.TestSdk 5 | altcover 6 | Ionide.ProjInfo 7 | Ionide.ProjInfo.FCS 8 | NuGet.Frameworks 9 | NuGet.ProjectModel 10 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | true 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/examples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/examples/hashing/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "hashing" 17 | let [] AssemblyProduct = "BinaryDefense.FSharp.Analyzers" 18 | let [] AssemblyVersion = "0.2.2" 19 | let [] AssemblyMetadata_ReleaseDate = "2021-05-23T00:00:00.0000000" 20 | let [] AssemblyFileVersion = "0.2.2" 21 | let [] AssemblyInformationalVersion = "0.2.2" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "ce6714e40c39d4a88cba9b4cbe0b5f93e2c1b285" 24 | -------------------------------------------------------------------------------- /tests/examples/hashing/hashing.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/examples/hashing/md5CryptoServicebinding.fs: -------------------------------------------------------------------------------- 1 | module md5CryptoServicebinding 2 | open System.Security.Cryptography 3 | open System.Text 4 | 5 | let foo () = 6 | use md5 = new MD5CryptoServiceProvider() 7 | md5.ComputeHash(UTF8Encoding().GetBytes("foo")) 8 | 9 | -------------------------------------------------------------------------------- /tests/examples/hashing/md5create.fs: -------------------------------------------------------------------------------- 1 | module md5create 2 | open System.Security.Cryptography 3 | open System.Text 4 | 5 | let foo () = 6 | use md5 = MD5.Create() 7 | md5.ComputeHash(UTF8Encoding().GetBytes("foo")) 8 | 9 | -------------------------------------------------------------------------------- /tests/examples/hashing/sha1CryptoServicebinding.fs: -------------------------------------------------------------------------------- 1 | module sha1CryptoServicebinding 2 | open System.Security.Cryptography 3 | open System.Text 4 | 5 | let foo () = 6 | use sha1 = new SHA1CryptoServiceProvider() 7 | sha1.ComputeHash(UTF8Encoding().GetBytes("foo")) 8 | 9 | -------------------------------------------------------------------------------- /tests/examples/hashing/sha1CryptoServicector.fs: -------------------------------------------------------------------------------- 1 | module sha1CryptoServicector 2 | open System.Security.Cryptography 3 | open System.Text 4 | 5 | let foo () = 6 | (new SHA1CryptoServiceProvider()).ComputeHash(UTF8Encoding().GetBytes("foo")) 7 | 8 | -------------------------------------------------------------------------------- /tests/examples/hashing/sha1create.fs: -------------------------------------------------------------------------------- 1 | module sha1create 2 | open System.Security.Cryptography 3 | open System.Text 4 | 5 | let foo () = 6 | use sha1 = SHA1.Create() 7 | sha1.ComputeHash(UTF8Encoding().GetBytes("foo")) 8 | 9 | --------------------------------------------------------------------------------