From 1c7b5a5107b1bfa9fd0960e0674ee5532d081e4b Mon Sep 17 00:00:00 2001 From: nojaf Date: Sun, 5 Nov 2023 17:29:56 +0100 Subject: [PATCH 01/20] Initial project structure --- .config/dotnet-tools.json | 24 + .editorconfig | 14 + .gitignore | 489 ++++++++++++++++++ CHANGELOG.md | 5 + Directory.Build.props | 42 ++ Directory.Packages.props | 17 + LICENSE.md | 21 + README.md | 13 +- build.fsx | 46 ++ docs/img/favicon.ico | Bin 0 -> 15406 bytes docs/img/logo.png | Bin 0 -> 11085 bytes docs/index.md | 5 + ionide-analyzers.sln | 36 ++ src/Ionide.Analyzers/Ionide.Analyzers.fsproj | 18 + src/Ionide.Analyzers/Library.fs | 4 + .../Ionide.Analyzers.Tests.fsproj | 27 + tests/Ionide.Analyzers.Tests/Program.fs | 4 + tests/Ionide.Analyzers.Tests/UnitTest1.fs | 9 + 18 files changed, 773 insertions(+), 1 deletion(-) create mode 100644 .config/dotnet-tools.json create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props create mode 100644 LICENSE.md create mode 100644 build.fsx create mode 100644 docs/img/favicon.ico create mode 100644 docs/img/logo.png create mode 100644 docs/index.md create mode 100644 ionide-analyzers.sln create mode 100644 src/Ionide.Analyzers/Ionide.Analyzers.fsproj create mode 100644 src/Ionide.Analyzers/Library.fs create mode 100644 tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj create mode 100644 tests/Ionide.Analyzers.Tests/Program.fs create mode 100644 tests/Ionide.Analyzers.Tests/UnitTest1.fs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..eed414c --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "fsharp-analyzers": { + "version": "0.18.0", + "commands": [ + "fsharp-analyzers" + ] + }, + "fantomas": { + "version": "6.2.3", + "commands": [ + "fantomas" + ] + }, + "fsdocs-tool": { + "version": "20.0.0-alpha-002", + "commands": [ + "fsdocs" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4ca9cea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*.{fs,fsi,fsx}] +end_of_line = lf +fsharp_keep_max_number_of_blank_lines = 1 +fsharp_multi_line_lambda_closing_newline = true +fsharp_alternative_long_member_definitions = true +fsharp_align_function_signature_to_indentation = true +fsharp_experimental_keep_indent_in_branch = true +fsharp_bar_before_discriminated_union_declaration = true +fsharp_multiline_bracket_style = aligned + +[build.fsx] +fsharp_blank_lines_around_nested_multiline_expressions = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81750ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,489 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp + +# fsdocs +output/ +.fsdocs/ +tmp/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f7c561e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +* Initial version \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..1b0e8e0 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,42 @@ + + + + $(MSBuildThisFileDirectory)CHANGELOG.md + + Ionide + David Schaefer, Florian Verdonck + false + true + + Community F# analyzers + + Copyright Ionide © $([System.DateTime]::UtcNow.Year) + F#, fsharp, analyzers + true + true + embedded + Apache-2.0 + README.md + https://ionide.io/ionide-analyzers/ + https://github.com/ionide/ionide-analyzers/blob/main/CHANGELOG.md + true + true + FS0025 + 1182;3390;$(WarnOn) + false + false + NU1603;NETSDK1057 + true + + true + true + preview + $(OtherFlags) --test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen + + + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..b3b19c0 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,17 @@ + + + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..66602bd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 Ionide + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 20152e7..964640e 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ -# ionide-analyzers +# Ionide.Analyzers + +A fsharp community project containing [analyzers](https://ionide.io/FSharp.Analyzers.SDK/). + +## Getting started + +Run `dotnet fsi build.fsx` to perform a local CI build. +Otherwise run `dotnet tool restore` and `dotnet restore` to download all the dependencies. + +## Running the documentation + +Run `dotnet fsi build.fsx -p Docs` to run the documentation locally. \ No newline at end of file diff --git a/build.fsx b/build.fsx new file mode 100644 index 0000000..f235610 --- /dev/null +++ b/build.fsx @@ -0,0 +1,46 @@ +#r "nuget: Fun.Build, 1.0.2" +#r "nuget: Fake.IO.FileSystem, 6.0.0" + +open Fake.IO +open Fake.IO.FileSystemOperators +open Fake.IO.Globbing.Operators +open Fun.Build + +let cleanDirs globExpr = (!!globExpr) |> Shell.cleanDirs + +pipeline "Build" { + workingDir __SOURCE_DIRECTORY__ + stage "clean" { + run (fun _ -> + async { + cleanDirs "src/**/obj" + cleanDirs "src/**/bin" + cleanDirs "tests/**/obj" + cleanDirs "tests/**/bin" + Shell.cleanDir "bin" + return 0 + } + ) + } + stage "lint" { + run "dotnet tool restore" + run "dotnet fantomas . --check" + } + stage "restore" { run "dotnet restore" } + stage "build" { + run "dotnet restore ionide-analyzers.sln" + run "dotnet build --no-restore -c Release ionide-analyzers.sln" + } + stage "test" { run "dotnet test --no-restore --no-build -c Release" } + stage "pack" { run "dotnet pack ./src/Ionide.Analyzers/Ionide.Analyzers.fsproj -c Release -o bin" } + stage "docs" { run "dotnet fsdocs build" } + runIfOnlySpecified false +} + +pipeline "Docs" { + workingDir __SOURCE_DIRECTORY__ + stage "main" { run "dotnet fsdocs watch --port 7890" } + runIfOnlySpecified true +} + +tryPrintPipelineCommandHelp () diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..16abdb0d0a7d527a83c1d4bfb373cbac9a78cb5e GIT binary patch literal 15406 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 X{m1@|QDBS$V-y&pz!(L-E)@7*Eo-`w literal 0 HcmV?d00001 diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..babbee57ea4f5cd7ba97b6922d06638e2a28e149 GIT binary patch literal 11085 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 + +> A gem cannot be polished without friction, nor a man perfected without trials. - Seneca \ No newline at end of file diff --git a/ionide-analyzers.sln b/ionide-analyzers.sln new file mode 100644 index 0000000..4cf7c2e --- /dev/null +++ b/ionide-analyzers.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A75DEA54-6175-4364-BCAB-23F9BF054006}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Ionide.Analyzers", "src\Ionide.Analyzers\Ionide.Analyzers.fsproj", "{37A4EE72-5FF8-4255-8004-71CFF6193E14}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{EBA500BA-82FE-4E55-82F1-0B2FE5A94E14}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Ionide.Analyzers.Tests", "tests\Ionide.Analyzers.Tests\Ionide.Analyzers.Tests.fsproj", "{93CC938F-1F27-4CE4-9AD4-52D057AB3DC8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {37A4EE72-5FF8-4255-8004-71CFF6193E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37A4EE72-5FF8-4255-8004-71CFF6193E14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37A4EE72-5FF8-4255-8004-71CFF6193E14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37A4EE72-5FF8-4255-8004-71CFF6193E14}.Release|Any CPU.Build.0 = Release|Any CPU + {93CC938F-1F27-4CE4-9AD4-52D057AB3DC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93CC938F-1F27-4CE4-9AD4-52D057AB3DC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93CC938F-1F27-4CE4-9AD4-52D057AB3DC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93CC938F-1F27-4CE4-9AD4-52D057AB3DC8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {37A4EE72-5FF8-4255-8004-71CFF6193E14} = {A75DEA54-6175-4364-BCAB-23F9BF054006} + {93CC938F-1F27-4CE4-9AD4-52D057AB3DC8} = {EBA500BA-82FE-4E55-82F1-0B2FE5A94E14} + EndGlobalSection +EndGlobal diff --git a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj new file mode 100644 index 0000000..5fea072 --- /dev/null +++ b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj @@ -0,0 +1,18 @@ + + + + net6.0 + true + true + + + + + + + + + + + + diff --git a/src/Ionide.Analyzers/Library.fs b/src/Ionide.Analyzers/Library.fs new file mode 100644 index 0000000..0adae63 --- /dev/null +++ b/src/Ionide.Analyzers/Library.fs @@ -0,0 +1,4 @@ +namespace Ionide.Analyzers + +module Say = + let hello name = printfn "Hello %s" name diff --git a/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj new file mode 100644 index 0000000..7522ead --- /dev/null +++ b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj @@ -0,0 +1,27 @@ + + + + net8.0 + + false + false + true + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ionide.Analyzers.Tests/Program.fs b/tests/Ionide.Analyzers.Tests/Program.fs new file mode 100644 index 0000000..176a7b6 --- /dev/null +++ b/tests/Ionide.Analyzers.Tests/Program.fs @@ -0,0 +1,4 @@ +module Program = + + [] + let main _ = 0 diff --git a/tests/Ionide.Analyzers.Tests/UnitTest1.fs b/tests/Ionide.Analyzers.Tests/UnitTest1.fs new file mode 100644 index 0000000..55704c8 --- /dev/null +++ b/tests/Ionide.Analyzers.Tests/UnitTest1.fs @@ -0,0 +1,9 @@ +module Ionide.Analyzers.Tests + +open NUnit.Framework + +[] +let Setup () = () + +[] +let Test1 () = Assert.Pass() From 35066d45c10a6786676da691b797d62c810a2b75 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 11:09:34 +0100 Subject: [PATCH 02/20] Add CopyAndUpdateRecordChangesAllFieldsAnalyzer --- Directory.Build.props | 2 +- build.fsx | 18 ++++- docs/content/fsdocs-theme.css | 3 + docs/hints/001.md | 31 +++++++++ ...AndUpdateRecordChangesAllFieldsAnalyzer.fs | 65 +++++++++++++++++ src/Ionide.Analyzers/Ionide.Analyzers.fsproj | 1 + ...dateRecordChangesAllFieldsAnalyzerTests.fs | 69 +++++++++++++++++++ .../Ionide.Analyzers.Tests.fsproj | 2 +- tests/Ionide.Analyzers.Tests/UnitTest1.fs | 9 --- 9 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 docs/content/fsdocs-theme.css create mode 100644 docs/hints/001.md create mode 100644 src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs create mode 100644 tests/Ionide.Analyzers.Tests/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs delete mode 100644 tests/Ionide.Analyzers.Tests/UnitTest1.fs diff --git a/Directory.Build.props b/Directory.Build.props index 1b0e8e0..30f0160 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -35,7 +35,7 @@ - + diff --git a/build.fsx b/build.fsx index f235610..971912b 100644 --- a/build.fsx +++ b/build.fsx @@ -33,13 +33,27 @@ pipeline "Build" { } stage "test" { run "dotnet test --no-restore --no-build -c Release" } stage "pack" { run "dotnet pack ./src/Ionide.Analyzers/Ionide.Analyzers.fsproj -c Release -o bin" } - stage "docs" { run "dotnet fsdocs build" } + stage "docs" { + envVars + [| + "DOTNET_ROLL_FORWARD_TO_PRERELEASE", "1" + "DOTNET_ROLL_FORWARD", "LatestMajor" + |] + run "dotnet fsdocs build --properties Configuration=Release" + } runIfOnlySpecified false } pipeline "Docs" { workingDir __SOURCE_DIRECTORY__ - stage "main" { run "dotnet fsdocs watch --port 7890" } + stage "main" { + envVars + [| + "DOTNET_ROLL_FORWARD_TO_PRERELEASE", "1" + "DOTNET_ROLL_FORWARD", "LatestMajor" + |] + run "dotnet fsdocs watch --port 7890" + } runIfOnlySpecified true } diff --git a/docs/content/fsdocs-theme.css b/docs/content/fsdocs-theme.css new file mode 100644 index 0000000..45de899 --- /dev/null +++ b/docs/content/fsdocs-theme.css @@ -0,0 +1,3 @@ +:root { + --aside-width: 300px; +} \ No newline at end of file diff --git a/docs/hints/001.md b/docs/hints/001.md new file mode 100644 index 0000000..4628d22 --- /dev/null +++ b/docs/hints/001.md @@ -0,0 +1,31 @@ +--- +title: CopyAndUpdateRecordChangesAllFields +category: hints +categoryindex: 1 +index: 1 +--- +# CopyAndUpdateRecordChangesAllFieldsAnalyzer + +## Problem + +See [fsharp/fslang-suggestions#603](https://github.com/fsharp/fslang-suggestions/issues/603), when you have a record update expression that overrides all fields, it is advised to construct a new instance instead. + +```fsharp +type Point = + { + X: int + Y: int + } + +let zero = { X = 0; Y = 0; +// Triggers analyzer +let moved = { zero with X = 1; Y = 2 } +``` + +## Fix + +Remove the `expr with` part: + +```fsharp +let moved = { X = 1; Y = 2 } +``` \ No newline at end of file diff --git a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs b/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs new file mode 100644 index 0000000..a0a441c --- /dev/null +++ b/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs @@ -0,0 +1,65 @@ +module Ionide.Analyzers.Hints.CopyAndUpdateRecordChangesAllFieldsAnalyzer + +open FSharp.Analyzers.SDK +open FSharp.Analyzers.SDK.ASTCollecting +open FSharp.Analyzers.SDK.TASTCollecting +open FSharp.Compiler.Symbols +open FSharp.Compiler.Text +open FSharp.Compiler.Syntax + +type UpdateRecord = SynExprRecordField list * range + +[] +let copyAndUpdateRecordChangesAllFieldsAnalyzer: Analyzer = + fun (context: CliContext) -> + async { + let untypedRecordUpdates = + let xs = ResizeArray() + + let collector = + { new SyntaxCollectorBase() with + override x.WalkExpr(e: SynExpr) = + match e with + | SynExpr.Record(copyInfo = Some _; recordFields = fields) -> xs.Add(fields, e.Range) + | _ -> () + } + + walkAst collector context.ParseFileResults.ParseTree + Seq.toList xs + + let messages = ResizeArray untypedRecordUpdates.Length + + let tastCollector = + { new TypedTreeCollectorBase() with + override x.WalkNewRecord (mRecord: range) (recordType: FSharpType) = + let matchingUnTypedNode = + untypedRecordUpdates + |> List.tryFind (fun (_, mExpr) -> Range.equals mExpr mRecord) + + match matchingUnTypedNode with + | None -> () + | Some(fields, mExpr) -> + + if not recordType.TypeDefinition.IsFSharpRecord then + () + else if recordType.TypeDefinition.FSharpFields.Count = fields.Length then + messages.Add + { + Type = "CopyAndUpdateRecordChangesAllFieldsAnalyzer analyzer" + Message = + "All record fields of record are being updated. Consider creating a new instance instead." + Code = "IONIDE-001" + Severity = Warning + Range = mExpr + Fixes = [] + } + } + + match context.TypedTree with + | None -> () + | Some typedTree -> typedTree.Declarations |> List.iter (walkTast tastCollector) + + return Seq.toList messages + } diff --git a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj index 5fea072..e057b07 100644 --- a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj +++ b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj @@ -8,6 +8,7 @@ + diff --git a/tests/Ionide.Analyzers.Tests/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs b/tests/Ionide.Analyzers.Tests/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs new file mode 100644 index 0000000..a419c38 --- /dev/null +++ b/tests/Ionide.Analyzers.Tests/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs @@ -0,0 +1,69 @@ +module Ionide.Analyzers.Tests.Hints.CopyAndUpdateRecordChangesAllFieldsAnalyzerTests + +open NUnit.Framework +open FSharp.Compiler.CodeAnalysis +open FSharp.Analyzers.SDK.Testing +open Ionide.Analyzers.Hints.CopyAndUpdateRecordChangesAllFieldsAnalyzer + +let mutable projectOptions: FSharpProjectOptions = FSharpProjectOptions.zero + +[] +let Setup () = + task { + let! opts = mkOptionsFromProject "net7.0" [] + + projectOptions <- opts + } + +[] +let ``single record field`` () = + async { + let source = + """ +module M + +type R = { A: int } +let a = { A = 1 } +let updated = { a with A = 2 } + """ + + let ctx = getContext projectOptions source + let! msgs = copyAndUpdateRecordChangesAllFieldsAnalyzer ctx + Assert.IsNotEmpty msgs + Assert.IsTrue(Assert.messageContains "All record fields of record are being updated" msgs[0]) + } + +[] +let ``multiple record field`` () = + async { + let source = + """ +module M + +type R = { A: int; B:int; C:int } +let a = { A = 1; B = 2; C = 3 } +let updated = { a with A = 2; B = 4; C = 5 } + """ + + let ctx = getContext projectOptions source + let! msgs = copyAndUpdateRecordChangesAllFieldsAnalyzer ctx + Assert.IsNotEmpty msgs + Assert.IsTrue(Assert.messageContains "All record fields of record are being updated" msgs[0]) + } + +[] +let ``multiple record field, neg`` () = + async { + let source = + """ +module M + +type R = { A: int; B:int; C:int } +let a = { A = 1; B = 2; C = 3 } +let updated = { a with A = 2; B = 4 } + """ + + let ctx = getContext projectOptions source + let! msgs = copyAndUpdateRecordChangesAllFieldsAnalyzer ctx + Assert.IsEmpty msgs + } diff --git a/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj index 7522ead..5464ad6 100644 --- a/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj +++ b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj @@ -9,7 +9,7 @@ - + diff --git a/tests/Ionide.Analyzers.Tests/UnitTest1.fs b/tests/Ionide.Analyzers.Tests/UnitTest1.fs deleted file mode 100644 index 55704c8..0000000 --- a/tests/Ionide.Analyzers.Tests/UnitTest1.fs +++ /dev/null @@ -1,9 +0,0 @@ -module Ionide.Analyzers.Tests - -open NUnit.Framework - -[] -let Setup () = () - -[] -let Test1 () = Assert.Pass() From 28420a7cbf1ad4531c7a91e20bfb2085a5e7e532 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 11:24:17 +0100 Subject: [PATCH 03/20] Update URL with expected live url. --- .../Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs b/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs index a0a441c..04f6c91 100644 --- a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs +++ b/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs @@ -11,7 +11,7 @@ type UpdateRecord = SynExprRecordField list * range [] + "https://ionide.io/ionide-analyzers/hints/001.html")>] let copyAndUpdateRecordChangesAllFieldsAnalyzer: Analyzer = fun (context: CliContext) -> async { From 66286f3aa6077d89d7f9f275a63ab9bd4080c15f Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 11:45:42 +0100 Subject: [PATCH 04/20] Update fsdocs-tool --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index eed414c..9ef9c78 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "fsdocs-tool": { - "version": "20.0.0-alpha-002", + "version": "20.0.0-alpha-003", "commands": [ "fsdocs" ] From c9fd0ba9328c605095dc2bf61a4be7c9e55b98ae Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 11:48:28 +0100 Subject: [PATCH 05/20] Add SquareBracketArrayAnalyzer --- build.fsx | 36 ++++++++++++++++- docs/style/002.md | 25 ++++++++++++ src/Ionide.Analyzers/Ionide.Analyzers.fsproj | 1 + .../Style/SquareBracketArrayAnalyzer.fs | 39 +++++++++++++++++++ .../Ionide.Analyzers.Tests.fsproj | 1 + .../Style/SquareBracketArrayAnalyzerTests.fs | 32 +++++++++++++++ 6 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 docs/style/002.md create mode 100644 src/Ionide.Analyzers/Style/SquareBracketArrayAnalyzer.fs create mode 100644 tests/Ionide.Analyzers.Tests/Style/SquareBracketArrayAnalyzerTests.fs diff --git a/build.fsx b/build.fsx index 971912b..03c89c3 100644 --- a/build.fsx +++ b/build.fsx @@ -1,13 +1,44 @@ #r "nuget: Fun.Build, 1.0.2" #r "nuget: Fake.IO.FileSystem, 6.0.0" +open System.Text.Json open Fake.IO -open Fake.IO.FileSystemOperators open Fake.IO.Globbing.Operators open Fun.Build let cleanDirs globExpr = (!!globExpr) |> Shell.cleanDirs +/// Workaround for https://github.com/dotnet/sdk/issues/35989 +let restoreTools (ctx: Internal.StageContext) = + async { + let json = File.readAsString ".config/dotnet-tools.json" + let jsonDocument = JsonDocument.Parse(json) + let root = jsonDocument.RootElement + let tools = root.GetProperty("tools") + + let! installs = + tools.EnumerateObject() + |> Seq.map (fun tool -> + let version = tool.Value.GetProperty("version").GetString() + ctx.RunCommand $"dotnet tool install %s{tool.Name} --version %s{version}" + ) + |> Async.Sequential + + let failedInstalls = + installs + |> Array.tryPick ( + function + | Ok _ -> None + | Error error -> Some error + ) + + match failedInstalls with + | None -> return 0 + | Some error -> + printfn $"%s{error}" + return 1 + } + pipeline "Build" { workingDir __SOURCE_DIRECTORY__ stage "clean" { @@ -23,7 +54,7 @@ pipeline "Build" { ) } stage "lint" { - run "dotnet tool restore" + run restoreTools run "dotnet fantomas . --check" } stage "restore" { run "dotnet restore" } @@ -52,6 +83,7 @@ pipeline "Docs" { "DOTNET_ROLL_FORWARD_TO_PRERELEASE", "1" "DOTNET_ROLL_FORWARD", "LatestMajor" |] + run restoreTools run "dotnet fsdocs watch --port 7890" } runIfOnlySpecified true diff --git a/docs/style/002.md b/docs/style/002.md new file mode 100644 index 0000000..2b39297 --- /dev/null +++ b/docs/style/002.md @@ -0,0 +1,25 @@ +--- +title: SquareBracketArrayAnalyzer +category: style +categoryindex: 2 +index: 1 +--- + +# SquareBracketArrayAnalyzer + +## Problem + +Using the older array type syntax (`string[]`) is discouraged by the [style guide](https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#for-types-prefer-prefix-syntax-for-generics-foot-with-some-specific-exceptions). + +```fsharp +// Triggers analyzer +let a: string[] = Array.empty +``` + +## Fix + +The `postfix` syntax is preferred: + +```fsharp +let a: string array = Array.empty +``` \ No newline at end of file diff --git a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj index e057b07..6ed7900 100644 --- a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj +++ b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj @@ -9,6 +9,7 @@ + diff --git a/src/Ionide.Analyzers/Style/SquareBracketArrayAnalyzer.fs b/src/Ionide.Analyzers/Style/SquareBracketArrayAnalyzer.fs new file mode 100644 index 0000000..e984704 --- /dev/null +++ b/src/Ionide.Analyzers/Style/SquareBracketArrayAnalyzer.fs @@ -0,0 +1,39 @@ +module Ionide.Analyzers.Style.SquareBracketArrayAnalyzer + +open FSharp.Compiler.Text +open FSharp.Compiler.Syntax +open FSharp.Analyzers.SDK +open FSharp.Analyzers.SDK.ASTCollecting + +[] +let squareBracketArrayAnalyzer: Analyzer = + fun (context: CliContext) -> + async { + let ts = ResizeArray() + + let collector = + { new SyntaxCollectorBase() with + override x.WalkType(t: SynType) = + match t with + | SynType.Array _ -> ts.Add t.Range + | _ -> () + } + + walkAst collector context.ParseFileResults.ParseTree + + return + ts + |> Seq.map (fun m -> + { + Type = "SquareBracketArrayAnalyzer" + Message = "Prefer postfix syntax for arrays." + Code = "IONIDE-002" + Severity = Info + Range = m + Fixes = [] + } + ) + |> Seq.toList + } diff --git a/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj index 5464ad6..ff9a13d 100644 --- a/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj +++ b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj @@ -10,6 +10,7 @@ + diff --git a/tests/Ionide.Analyzers.Tests/Style/SquareBracketArrayAnalyzerTests.fs b/tests/Ionide.Analyzers.Tests/Style/SquareBracketArrayAnalyzerTests.fs new file mode 100644 index 0000000..0b82bb0 --- /dev/null +++ b/tests/Ionide.Analyzers.Tests/Style/SquareBracketArrayAnalyzerTests.fs @@ -0,0 +1,32 @@ +module Ionide.Analyzers.Tests.Style.SquareBracketArrayAnalyzerTests + +open NUnit.Framework +open FSharp.Compiler.CodeAnalysis +open FSharp.Analyzers.SDK.Testing +open Ionide.Analyzers.Style.SquareBracketArrayAnalyzer + +let mutable projectOptions: FSharpProjectOptions = FSharpProjectOptions.zero + +[] +let Setup () = + task { + let! opts = mkOptionsFromProject "net7.0" [] + + projectOptions <- opts + } + +[] +let ``string array in binding`` () = + async { + let source = + """ +module M + +let a (b: string[]) = () + """ + + let ctx = getContext projectOptions source + let! msgs = squareBracketArrayAnalyzer ctx + Assert.IsNotEmpty msgs + Assert.IsTrue(Assert.messageContains "Prefer postfix syntax for arrays." msgs[0]) + } From 2637524edb8fc8e01885ca5b69b3a933d1706e1e Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 11:55:19 +0100 Subject: [PATCH 06/20] Add dependabot yml. --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bc18f00 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" From 64274ba74daf4a0a8a118f84166093cc625f7fe5 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 11:57:44 +0100 Subject: [PATCH 07/20] Add GH Actions to dependabot.yml --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bc18f00..8ce0230 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,11 @@ version: 2 updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "nuget" directory: "/" schedule: From 6681d63c3b8d752f3001260a0d59d99b27f2ae24 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 12:00:33 +0100 Subject: [PATCH 08/20] Add CI Action. --- .github/workflows/ci.yml | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c45ad33 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +permissions: + contents: read + pages: write + id-token: write + +jobs: + ci: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + + - name: Build + run: dotnet fsi build.fsx + + - name: Upload documentation + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v2 + with: + path: ./output + + deploy: + runs-on: ubuntu-latest + needs: ci + if: github.ref == 'refs/heads/main' + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 From e291a2dc4fc96313bb1f34ebbd178bf2d845ee4e Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 13:25:09 +0100 Subject: [PATCH 09/20] Add global json --- .github/global.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/global.json diff --git a/.github/global.json b/.github/global.json new file mode 100644 index 0000000..1cccce5 --- /dev/null +++ b/.github/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "8.0.100-rc.2.23502.2" + } +} \ No newline at end of file From b68182d95dbe522440f33b51cd9f9956f1b04094 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 15:23:06 +0100 Subject: [PATCH 10/20] Dogfood analyzers. --- .github/workflows/ci.yml | 20 +++++++++++++++++--- .gitignore | 5 ++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c45ad33..57e216d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,11 @@ permissions: jobs: ci: - - runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -32,8 +35,19 @@ jobs: - name: Build run: dotnet fsi build.fsx + - name: Analyzers + continue-on-error: true + run: dotnet fsharp-analyzers --project ./src/Ionide.Analyzers/Ionide.Analyzers.fsproj --project ./tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj --analyzers-path ./src/Ionide.Analyzers/bin/Release/net6.0 --report ./report.sarif + if: matrix.os == 'ubuntu-latest' + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v2 + if: matrix.os == 'ubuntu-latest' + with: + sarif_file: ./report.sarif + - name: Upload documentation - if: github.ref == 'refs/heads/main' + if: matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@v2 with: path: ./output diff --git a/.gitignore b/.gitignore index 81750ba..7b6f223 100644 --- a/.gitignore +++ b/.gitignore @@ -486,4 +486,7 @@ $RECYCLE.BIN/ # fsdocs output/ .fsdocs/ -tmp/ \ No newline at end of file +tmp/ + +# Analyzers +*.sarif \ No newline at end of file From da10a3272eadb1023bf9bcaf82d50e799bcb97c5 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 15:26:35 +0100 Subject: [PATCH 11/20] Move global.json to root --- .github/global.json => global.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/global.json => global.json (100%) diff --git a/.github/global.json b/global.json similarity index 100% rename from .github/global.json rename to global.json From ea2b32e970d5ac687cbbe78492f3d26b5bb12d85 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 15:40:59 +0100 Subject: [PATCH 12/20] Update README.md path --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 30f0160..c4a55fc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -35,7 +35,7 @@ - + From e1c52f3124e3cd70a57f22631db6b0ce3b6dcd4a Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 15:41:52 +0100 Subject: [PATCH 13/20] Remove job continue-on-error --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57e216d..14d026f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,6 @@ permissions: jobs: ci: - continue-on-error: true strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] From 1a3958488cdee6ecbcdc68e6a061d3f32698029a Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 15:52:24 +0100 Subject: [PATCH 14/20] Roll forward? --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14d026f..c2edcd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,8 @@ env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1 + DOTNET_ROLL_FORWARD: LatestMajor permissions: contents: read From 55f23bfb2b8a99f212542b6db8b7e6c16cd69b56 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 6 Nov 2023 17:27:57 +0100 Subject: [PATCH 15/20] Add basic guidelines on contributions. --- docs/content/contributions.md | 71 +++++++++++++++++++++++++++++++++++ docs/hints/001.md | 2 +- docs/index.md | 4 +- docs/style/002.md | 2 +- 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 docs/content/contributions.md diff --git a/docs/content/contributions.md b/docs/content/contributions.md new file mode 100644 index 0000000..437033a --- /dev/null +++ b/docs/content/contributions.md @@ -0,0 +1,71 @@ +--- +title: Contributing +categoryindex: 1 +index: 1 +category: docs +--- + +# Contributing + +Hi there! Thank you for considering to contribute to this community project! +We hope this project can serve as a vessel to proto-type your ideas and bring them easily into your workflow. + +The main goal of this project is to have a reference implementation for analyzers built with the [Ionide SDK](https://ionide.io/FSharp.Analyzers.SDK/). + +## What kind of contributions do we accept? + +We see this project as a collection of **general purpose analyzers for every kind of F# codebase**. The analyzers are ideally based on generally accepted guidance among the community. + +If an analyzer is more tailored towards your own use-cases, or has a distinct (controversial) opinion, we might decline your pull request. This is to benefit the out-of-the-box experience. +We wish to avoid that people turns of certain analyzers from this `NuGet` package, because they are not universally accepted as best practise rules. + +The best thing you can do is **pitch your idea before** you start any **implementation**. This is to avoid a mismatch of expectations. + +## How do I contribute a new analyzer? + +When creating a new analyzer, the typical experience is that you will provide a one-time contribution. We review your PR, we go back and forward over some details, we merge it, and ship a new NuGet package with your work! +We truly hope to provide you with a great experience while contribution, and also keep a good balance on the maintenance of your change afterwards. + +### Technical setup + +This is a very typical `dotnet` repository. Run commands like `dotnet tool restore`, `dotnet restore` and `dotnet build` to get going. + +Our build script can be invoke with `dotnet fsi build.fsx`. +Or `dotnet fsi build.fsx -- --help` to view non-default pipelines. + +### Your analyzer + +We try to split the analyzers up into several categories: + +- `hints` +- `style` +- `performance` +- `quality` + +Add your analyzer the directory that makes the most sense. Ask us if you are unsure. +Next start writing your [first analyzer](https://ionide.io/FSharp.Analyzers.SDK/content/Getting%20Started%20Writing.html#First-analyzer). + +Please use the *next available code* for your messages, we currently do not have any elaborate system in place for the message codes. + +### Your regression tests + +Because we want to ensure you analyzer keeps working with every new release, we would like to ask you to provide a series of unit tests. These should cover the most critical use-case of your analyzer. +Try and create unit tests in a fashion where the tests themselves are stable. If the SDK API changes, we only want to update your analyzer code, and your tests should run fine. + +### Your documentation + +Each analyzer should have a matching documentation page. +This is the url we will use to link in the `AnalyzerAttribute` meta data. +Use the existing pages as a reference. + +Run `dotnet fsi build.fsx -p Docs` to run the `fsdocs` tool locally. + +### Your changelog entry + +We use [KeepAChangelog](https://github.com/ionide/keepachangelog) to determine our next NuGet version. +Please add [a new entry](https://keepachangelog.com/en/1.1.0/) for your changes. + +## When will the next version ship? + +Unless, there are technical reason blocking us, we will try and ship your contribution as soon as possible. +Our CI process should pick up new version from the changelog and push new packages to NuGet.org once the code is on the `main` branch. diff --git a/docs/hints/001.md b/docs/hints/001.md index 4628d22..d803fa8 100644 --- a/docs/hints/001.md +++ b/docs/hints/001.md @@ -1,7 +1,7 @@ --- title: CopyAndUpdateRecordChangesAllFields category: hints -categoryindex: 1 +categoryindex: 2 index: 1 --- # CopyAndUpdateRecordChangesAllFieldsAnalyzer diff --git a/docs/index.md b/docs/index.md index d9e5a33..20c813d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@ # Ionide.Analyzers - +Welcome to the Ionide Analyzers project. -> A gem cannot be polished without friction, nor a man perfected without trials. - Seneca \ No newline at end of file +Learn how to [get started contributing]({{fsdocs-next-page-link}})! diff --git a/docs/style/002.md b/docs/style/002.md index 2b39297..166ffcd 100644 --- a/docs/style/002.md +++ b/docs/style/002.md @@ -1,7 +1,7 @@ --- title: SquareBracketArrayAnalyzer category: style -categoryindex: 2 +categoryindex: 3 index: 1 --- From b1fb6cfd527c2d9cde029c6c41a14d8b81be937e Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 7 Nov 2023 08:17:06 +0100 Subject: [PATCH 16/20] Remove empty file --- src/Ionide.Analyzers/Ionide.Analyzers.fsproj | 1 - src/Ionide.Analyzers/Library.fs | 4 ---- 2 files changed, 5 deletions(-) delete mode 100644 src/Ionide.Analyzers/Library.fs diff --git a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj index 6ed7900..71a94f4 100644 --- a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj +++ b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj @@ -7,7 +7,6 @@ - diff --git a/src/Ionide.Analyzers/Library.fs b/src/Ionide.Analyzers/Library.fs deleted file mode 100644 index 0adae63..0000000 --- a/src/Ionide.Analyzers/Library.fs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Ionide.Analyzers - -module Say = - let hello name = printfn "Hello %s" name From 78a7db8e613836f15a438be4027299fbc9f4f847 Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 7 Nov 2023 08:17:21 +0100 Subject: [PATCH 17/20] Change severity to hint. --- .../Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs b/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs index 04f6c91..06b43a7 100644 --- a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs +++ b/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs @@ -51,7 +51,7 @@ let copyAndUpdateRecordChangesAllFieldsAnalyzer: Analyzer = Message = "All record fields of record are being updated. Consider creating a new instance instead." Code = "IONIDE-001" - Severity = Warning + Severity = Severity.Hint Range = mExpr Fixes = [] } From 92c3b968ba752ca96f66dfcbd2a91a35688634db Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 7 Nov 2023 08:23:06 +0100 Subject: [PATCH 18/20] Update shortDescription of copyAndUpdateRecordChangesAllFieldsAnalyzer. --- .../Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs b/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs index 06b43a7..a909dae 100644 --- a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs +++ b/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs @@ -10,7 +10,7 @@ open FSharp.Compiler.Syntax type UpdateRecord = SynExprRecordField list * range [] let copyAndUpdateRecordChangesAllFieldsAnalyzer: Analyzer = fun (context: CliContext) -> From ab9a6bd50ca9270bf1ed3fac72201354363e62f5 Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 7 Nov 2023 08:25:13 +0100 Subject: [PATCH 19/20] Rename Hint to Suggestion. --- docs/{hints => suggestion}/001.md | 2 +- src/Ionide.Analyzers/Ionide.Analyzers.fsproj | 2 +- .../CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs | 2 +- tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj | 2 +- .../CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename docs/{hints => suggestion}/001.md (96%) rename src/Ionide.Analyzers/{Hints => Suggestion}/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs (97%) rename tests/Ionide.Analyzers.Tests/{Hints => Suggestion}/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs (90%) diff --git a/docs/hints/001.md b/docs/suggestion/001.md similarity index 96% rename from docs/hints/001.md rename to docs/suggestion/001.md index d803fa8..cc0beab 100644 --- a/docs/hints/001.md +++ b/docs/suggestion/001.md @@ -1,6 +1,6 @@ --- title: CopyAndUpdateRecordChangesAllFields -category: hints +category: suggestion categoryindex: 2 index: 1 --- diff --git a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj index 71a94f4..8a3b467 100644 --- a/src/Ionide.Analyzers/Ionide.Analyzers.fsproj +++ b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj @@ -7,7 +7,7 @@ - + diff --git a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs b/src/Ionide.Analyzers/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs similarity index 97% rename from src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs rename to src/Ionide.Analyzers/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs index a909dae..2a0a8c6 100644 --- a/src/Ionide.Analyzers/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs +++ b/src/Ionide.Analyzers/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs @@ -1,4 +1,4 @@ -module Ionide.Analyzers.Hints.CopyAndUpdateRecordChangesAllFieldsAnalyzer +module Ionide.Analyzers.Suggestion.CopyAndUpdateRecordChangesAllFieldsAnalyzer open FSharp.Analyzers.SDK open FSharp.Analyzers.SDK.ASTCollecting diff --git a/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj index ff9a13d..aafe5f8 100644 --- a/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj +++ b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj @@ -9,7 +9,7 @@ - + diff --git a/tests/Ionide.Analyzers.Tests/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs b/tests/Ionide.Analyzers.Tests/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs similarity index 90% rename from tests/Ionide.Analyzers.Tests/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs rename to tests/Ionide.Analyzers.Tests/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs index a419c38..c8247fa 100644 --- a/tests/Ionide.Analyzers.Tests/Hints/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs +++ b/tests/Ionide.Analyzers.Tests/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs @@ -1,9 +1,9 @@ -module Ionide.Analyzers.Tests.Hints.CopyAndUpdateRecordChangesAllFieldsAnalyzerTests +module Ionide.Analyzers.Tests.Suggestion.CopyAndUpdateRecordChangesAllFieldsAnalyzerTests open NUnit.Framework open FSharp.Compiler.CodeAnalysis open FSharp.Analyzers.SDK.Testing -open Ionide.Analyzers.Hints.CopyAndUpdateRecordChangesAllFieldsAnalyzer +open Ionide.Analyzers.Suggestion.CopyAndUpdateRecordChangesAllFieldsAnalyzer let mutable projectOptions: FSharpProjectOptions = FSharpProjectOptions.zero From 83ef7aa183476f2138cb56ba9e3707ee7dae54bc Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 7 Nov 2023 08:26:08 +0100 Subject: [PATCH 20/20] Typos --- README.md | 2 +- docs/suggestion/001.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 964640e..74f71ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Ionide.Analyzers -A fsharp community project containing [analyzers](https://ionide.io/FSharp.Analyzers.SDK/). +An fsharp community project containing [analyzers](https://ionide.io/FSharp.Analyzers.SDK/). ## Getting started diff --git a/docs/suggestion/001.md b/docs/suggestion/001.md index cc0beab..0aa6e1e 100644 --- a/docs/suggestion/001.md +++ b/docs/suggestion/001.md @@ -17,7 +17,7 @@ type Point = Y: int } -let zero = { X = 0; Y = 0; +let zero = { X = 0; Y = 0 } // Triggers analyzer let moved = { zero with X = 1; Y = 2 } ```