diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..9ef9c78 --- /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-003", + "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/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ce0230 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c2edcd0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +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 + DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1 + DOTNET_ROLL_FORWARD: LatestMajor + +permissions: + contents: read + pages: write + id-token: write + +jobs: + ci: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + + - 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: matrix.os == 'ubuntu-latest' && 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b6f223 --- /dev/null +++ b/.gitignore @@ -0,0 +1,492 @@ +## 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/ + +# Analyzers +*.sarif \ 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..c4a55fc --- /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..74f71ac 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ -# ionide-analyzers +# Ionide.Analyzers + +An 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..03c89c3 --- /dev/null +++ b/build.fsx @@ -0,0 +1,92 @@ +#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.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" { + run (fun _ -> + async { + cleanDirs "src/**/obj" + cleanDirs "src/**/bin" + cleanDirs "tests/**/obj" + cleanDirs "tests/**/bin" + Shell.cleanDir "bin" + return 0 + } + ) + } + stage "lint" { + run restoreTools + 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" { + 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" { + envVars + [| + "DOTNET_ROLL_FORWARD_TO_PRERELEASE", "1" + "DOTNET_ROLL_FORWARD", "LatestMajor" + |] + run restoreTools + run "dotnet fsdocs watch --port 7890" + } + runIfOnlySpecified true +} + +tryPrintPipelineCommandHelp () 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/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/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 0000000..16abdb0 Binary files /dev/null and b/docs/img/favicon.ico differ diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000..babbee5 Binary files /dev/null and b/docs/img/logo.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..20c813d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,5 @@ +# Ionide.Analyzers + +Welcome to the Ionide Analyzers project. + +Learn how to [get started contributing]({{fsdocs-next-page-link}})! diff --git a/docs/style/002.md b/docs/style/002.md new file mode 100644 index 0000000..166ffcd --- /dev/null +++ b/docs/style/002.md @@ -0,0 +1,25 @@ +--- +title: SquareBracketArrayAnalyzer +category: style +categoryindex: 3 +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/docs/suggestion/001.md b/docs/suggestion/001.md new file mode 100644 index 0000000..0aa6e1e --- /dev/null +++ b/docs/suggestion/001.md @@ -0,0 +1,31 @@ +--- +title: CopyAndUpdateRecordChangesAllFields +category: suggestion +categoryindex: 2 +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/global.json b/global.json new file mode 100644 index 0000000..1cccce5 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "8.0.100-rc.2.23502.2" + } +} \ 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..8a3b467 --- /dev/null +++ b/src/Ionide.Analyzers/Ionide.Analyzers.fsproj @@ -0,0 +1,19 @@ + + + + net6.0 + true + true + + + + + + + + + + + + + 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/src/Ionide.Analyzers/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs b/src/Ionide.Analyzers/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs new file mode 100644 index 0000000..2a0a8c6 --- /dev/null +++ b/src/Ionide.Analyzers/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs @@ -0,0 +1,65 @@ +module Ionide.Analyzers.Suggestion.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 = Severity.Hint + Range = mExpr + Fixes = [] + } + } + + match context.TypedTree with + | None -> () + | Some typedTree -> typedTree.Declarations |> List.iter (walkTast tastCollector) + + return Seq.toList messages + } 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..aafe5f8 --- /dev/null +++ b/tests/Ionide.Analyzers.Tests/Ionide.Analyzers.Tests.fsproj @@ -0,0 +1,28 @@ + + + + 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/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]) + } diff --git a/tests/Ionide.Analyzers.Tests/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs b/tests/Ionide.Analyzers.Tests/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs new file mode 100644 index 0000000..c8247fa --- /dev/null +++ b/tests/Ionide.Analyzers.Tests/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzerTests.fs @@ -0,0 +1,69 @@ +module Ionide.Analyzers.Tests.Suggestion.CopyAndUpdateRecordChangesAllFieldsAnalyzerTests + +open NUnit.Framework +open FSharp.Compiler.CodeAnalysis +open FSharp.Analyzers.SDK.Testing +open Ionide.Analyzers.Suggestion.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 + }