From 0cd0d98699c2c91b685ba348f1b540790c773d4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 17:15:47 +0000 Subject: [PATCH 01/62] Bump the all-github-actions group across 1 directory with 2 updates Bumps the all-github-actions group with 2 updates in the / directory: [julia-actions/setup-julia](https://github.com/julia-actions/setup-julia) and [julia-actions/cache](https://github.com/julia-actions/cache). Updates `julia-actions/setup-julia` from 2 to 3 - [Release notes](https://github.com/julia-actions/setup-julia/releases) - [Commits](https://github.com/julia-actions/setup-julia/compare/v2...v3) Updates `julia-actions/cache` from 2 to 3 - [Release notes](https://github.com/julia-actions/cache/releases) - [Commits](https://github.com/julia-actions/cache/compare/v2...v3) --- updated-dependencies: - dependency-name: julia-actions/setup-julia dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-github-actions - dependency-name: julia-actions/cache dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/run_tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 61cbc47..5eb5471 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -29,11 +29,11 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.julia-version }} arch: ${{ matrix.julia-arch }} - - uses: julia-actions/cache@v2 + - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 # with: From 3080e039ebb345cdcaa3f3257940db32925d5a2e Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 5 Mar 2026 13:46:16 +0100 Subject: [PATCH 02/62] Experiment with "unsafe-logging" and "unsafe-assert" rules --- Manifest.toml | 555 ++++++++++++++++++++++++++++++++- Project.toml | 3 + src/ReLint.jl | 5 +- src/interface.jl | 146 +++++---- src/linting/argus-rules.jl | 232 ++++++++++++++ src/linting/extended_checks.jl | 12 +- test/common.jl | 2 +- test/runtests.jl | 13 +- test/safe_logging_tests.jl | 4 +- 9 files changed, 887 insertions(+), 85 deletions(-) create mode 100644 src/linting/argus-rules.jl diff --git a/Manifest.toml b/Manifest.toml index 4bcc93c..6154e1d 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,11 +1,42 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.10.2" +julia_version = "1.12.5" manifest_format = "2.0" -project_hash = "64d1be42d85e48d8d22a77d412761d83485cc341" +project_hash = "615a86e01f16e17a918836e2f0db62c76583f5fa" + +[[deps.AbstractTrees]] +git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" +uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +version = "0.4.5" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" + +[[deps.Argus]] +deps = ["InteractiveUtils", "JET", "JuliaSyntax", "MacroTools", "PProf", "RuntimeGeneratedFunctions", "Test"] +path = "../../iuliadmtru/Argus.jl" +uuid = "e39743ec-b2cc-45ef-a990-b666fc093538" +version = "0.1.0" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + +[[deps.BufferedStreams]] +git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643" +uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d" +version = "1.2.2" + +[[deps.Bzip2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1b96ea4a01afe0ea4090c5c8039690672dd13f2e" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.9+0" [[deps.CSTParser]] deps = ["Tokenize"] @@ -13,19 +44,184 @@ git-tree-sha1 = "0157e592151e39fa570645e2b2debcdfb8a0f112" uuid = "00ebfdb7-1f24-5e51-bd34-a7502290713f" version = "3.4.3" +[[deps.Cairo_jll]] +deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "LZO_jll", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "a21c5464519504e41e0cbc91f0188e8ca23d7440" +uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" +version = "1.18.5+1" + +[[deps.CodeTracking]] +deps = ["InteractiveUtils", "UUIDs"] +git-tree-sha1 = "b7231a755812695b8046e8471ddc34c8268cbad5" +uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" +version = "3.0.0" + +[[deps.CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.8" + +[[deps.ColorTypes]] +deps = ["FixedPointNumbers", "Random"] +git-tree-sha1 = "67e11ee83a43eb71ddc950302c53bf33f0690dfe" +uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +version = "0.12.1" +weakdeps = ["StyledStrings"] + + [deps.ColorTypes.extensions] + StyledStringsExt = "StyledStrings" + +[[deps.Colors]] +deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] +git-tree-sha1 = "37ea44092930b1811e666c3bc38065d7d87fcc74" +uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" +version = "0.13.1" + +[[deps.Compiler]] +git-tree-sha1 = "382d79bfe72a406294faca39ef0c3cef6e6ce1f1" +uuid = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" +version = "0.1.1" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.0+1" + [[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" + +[[deps.Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" +version = "1.11.0" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.7.0" + +[[deps.EnumX]] +git-tree-sha1 = "c49898e8438c828577f04b92fc9368c388ac783c" +uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" +version = "1.0.7" + +[[deps.Expat_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "27af30de8b5445644e8ffe3bcb0d72049c089cf1" +uuid = "2e619515-83b5-522b-bb60-26c02a35a201" +version = "2.7.3+0" + +[[deps.ExprTools]] +git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" +uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" +version = "0.1.10" + +[[deps.FileIO]] +deps = ["Pkg", "Requires", "UUIDs"] +git-tree-sha1 = "6522cfb3b8fe97bec632252263057996cbd3de20" +uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +version = "1.18.0" + + [deps.FileIO.extensions] + HTTPExt = "HTTP" + + [deps.FileIO.weakdeps] + HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + +[[deps.FixedPointNumbers]] +deps = ["Statistics"] +git-tree-sha1 = "05882d6995ae5c12bb5f36dd2ed3f61c98cbb172" +uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +version = "0.8.5" + +[[deps.FlameGraphs]] +deps = ["AbstractTrees", "Colors", "FileIO", "FixedPointNumbers", "IndirectArrays", "LeftChildRightSiblingTrees", "Profile"] +git-tree-sha1 = "0166baf81babb91cf78bfcc771d8e87c43d568df" +uuid = "08572546-2f56-4bcf-ba4e-bab62c3a3f89" +version = "1.1.0" + +[[deps.Fontconfig_jll]] +deps = ["Artifacts", "Bzip2_jll", "Expat_jll", "FreeType2_jll", "JLLWrappers", "Libdl", "Libuuid_jll", "Zlib_jll"] +git-tree-sha1 = "f85dac9a96a01087df6e3a749840015a0ca3817d" +uuid = "a3f928ae-7b40-5064-980b-68af3947d34b" +version = "2.17.1+0" + +[[deps.FreeType2_jll]] +deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "2c5512e11c791d1baed2049c5652441b28fc6a31" +uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7" +version = "2.13.4+0" + +[[deps.FriBidi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "7a214fdac5ed5f59a22c2d9a885a16da1c74bbc7" +uuid = "559328eb-81f9-559d-9380-de523a88c83c" +version = "1.0.17+0" + +[[deps.GettextRuntime_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Libiconv_jll"] +git-tree-sha1 = "45288942190db7c5f760f59c04495064eedf9340" +uuid = "b0724c58-0f36-5564-988d-3bb0596ebc4a" +version = "0.22.4+0" + +[[deps.Glib_jll]] +deps = ["Artifacts", "GettextRuntime_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Zlib_jll"] +git-tree-sha1 = "24f6def62397474a297bfcec22384101609142ed" +uuid = "7746bdde-850d-59dc-9ae8-88ece973131d" +version = "2.86.3+0" + +[[deps.Graphite2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "8a6dbda1fd736d60cc477d99f2e7a042acfa46e8" +uuid = "3b182d85-2403-5c21-9c21-1e1f0cc25472" +version = "1.3.15+0" + +[[deps.Graphviz_jll]] +deps = ["Artifacts", "Cairo_jll", "Expat_jll", "JLLWrappers", "Libdl", "Pango_jll", "Pkg"] +git-tree-sha1 = "a5d45833dda71048117e8a9828bef75c03b18b1c" +uuid = "3c863552-8265-54e4-a6dc-903eb78fde85" +version = "2.50.0+1" + +[[deps.HarfBuzz_jll]] +deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll"] +git-tree-sha1 = "f923f9a774fcf3f5cb761bfa43aeadd689714813" +uuid = "2e76f6c2-a576-52d4-95c1-20adfe4de566" +version = "8.5.1+0" + +[[deps.IndirectArrays]] +git-tree-sha1 = "012e604e1c7458645cb8b436f8fba789a51b257f" +uuid = "9b13fd28-a010-5f03-acff-a1bbcff69959" +version = "1.0.0" [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" + +[[deps.JET]] +deps = ["CodeTracking", "Compiler", "InteractiveUtils", "JuliaInterpreter", "JuliaSyntax", "LoweredCodeUtils", "MacroTools", "Pkg", "PrecompileTools", "Preferences", "Revise", "Test"] +git-tree-sha1 = "3642228b0d4ab0b263a5946f60ace6c2b5616256" +uuid = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +version = "0.11.3" + +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.7.1" [[deps.JSON3]] deps = ["Dates", "Mmap", "Parsers", "PrecompileTools", "StructTypes", "UUIDs"] -git-tree-sha1 = "eb3edce0ed4fa32f75a0a11217433c31d56bd48b" +git-tree-sha1 = "411eccfe8aba0814ffa0fdf4860913ed09c34975" uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" -version = "1.14.0" +version = "1.14.3" [deps.JSON3.extensions] JSON3ArrowExt = ["ArrowTypes"] @@ -33,38 +229,259 @@ version = "1.14.0" [deps.JSON3.weakdeps] ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" +[[deps.JuliaInterpreter]] +deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] +git-tree-sha1 = "f5e59455236d8269b7868665c3665e8477af0e37" +uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" +version = "0.10.10" + +[[deps.JuliaSyntax]] +git-tree-sha1 = "0d4b3dab95018bcf3925204475693d9f09dc45b8" +uuid = "70703baa-626e-46a2-a12c-08ffd08c73b4" +version = "1.0.2" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + +[[deps.LLVMOpenMP_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "eb62a3deb62fc6d8822c0c4bef73e4412419c5d8" +uuid = "1d63c593-3942-5779-bab2-d838dc0a180e" +version = "18.1.8+0" + +[[deps.LZO_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1c602b1127f4751facb671441ca72715cc95938a" +uuid = "dd4b983a-f0e5-5f8d-a1b7-129d4a5fb1ac" +version = "2.10.3+0" + +[[deps.LeftChildRightSiblingTrees]] +deps = ["AbstractTrees"] +git-tree-sha1 = "95ba48564903b43b2462318aa243ee79d81135ff" +uuid = "1d6d02ad-be62-4b6b-8a6d-2f90e265016e" +version = "0.2.1" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.15.0+0" + +[[deps.LibGit2]] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.9.0+0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.3+1" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + +[[deps.Libffi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "c8da7e6a91781c41a863611c7e966098d783c57a" +uuid = "e9f186c6-92d2-5b65-8a66-fee21dc1b490" +version = "3.4.7+0" + +[[deps.Libiconv_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" +uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +version = "1.18.0+0" + +[[deps.Libmount_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "97bbca976196f2a1eb9607131cb108c69ec3f8a6" +uuid = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9" +version = "2.41.3+0" + +[[deps.Libuuid_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "d0205286d9eceadc518742860bf23f703779a3d6" +uuid = "38a345b3-de98-5d2b-a5d3-14cd9215e700" +version = "2.41.3+0" + +[[deps.LinearAlgebra]] +deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +version = "1.12.0" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" + +[[deps.LoweredCodeUtils]] +deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] +git-tree-sha1 = "5d4278f755440f70648d80cc6225f51e78e94094" +uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" +version = "3.5.1" + +[[deps.MacroTools]] +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.16" + [[deps.Markdown]] -deps = ["Base64"] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" +version = "1.11.0" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2025.11.4" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.3.0" + +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +version = "0.3.29+0" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.4+0" + +[[deps.OrderedCollections]] +git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.8.1" + +[[deps.PCRE2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" +version = "10.44.0+1" + +[[deps.PProf]] +deps = ["AbstractTrees", "CodecZlib", "EnumX", "FlameGraphs", "Libdl", "OrderedCollections", "Profile", "ProgressMeter", "ProtoBuf", "pprof_jll"] +git-tree-sha1 = "2b62c1a1fde38c21023d5786cd37ef95d9d7acd4" +uuid = "e4faabce-9ead-11e9-39d9-4379958e3056" +version = "3.2.0" + +[[deps.Pango_jll]] +deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "FriBidi_jll", "Glib_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "0662b083e11420952f2e62e17eddae7fc07d5997" +uuid = "36c8627f-9965-5494-a995-c6b170f724f3" +version = "1.57.0+0" [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821" +git-tree-sha1 = "7d2f8f21da5db6a806faf7b9b292296da42b2810" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.1" +version = "2.8.3" + +[[deps.Pixman_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] +git-tree-sha1 = "db76b1ecd5e9715f3d043cec13b2ec93ce015d53" +uuid = "30392449-352a-5448-841d-b1acce4e97dc" +version = "0.44.2+0" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.12.1" +weakdeps = ["REPL"] + + [deps.Pkg.extensions] + REPLExt = "REPL" [[deps.PrecompileTools]] deps = ["Preferences"] -git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" +git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.2.1" +version = "1.3.3" [[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.4.3" +version = "1.5.2" [[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" + +[[deps.Profile]] +deps = ["StyledStrings"] +uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" +version = "1.11.0" + +[[deps.ProgressMeter]] +deps = ["Distributed", "Printf"] +git-tree-sha1 = "fbb92c6c56b34e1a2c4c36058f68f332bec840e7" +uuid = "92933f4c-e287-5a05-a399-4b506db050ca" +version = "1.11.0" + +[[deps.ProtoBuf]] +deps = ["BufferedStreams", "EnumX", "TOML"] +git-tree-sha1 = "da18083a52d9d57bbe6dadaacad39731e5f7be39" +uuid = "3349acd9-ac6a-5e09-bcdb-63829b23a429" +version = "1.3.0" + +[[deps.REPL]] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" [[deps.Random]] deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + +[[deps.ReLint]] +deps = ["Argus", "CSTParser", "Dates", "InteractiveUtils", "JSON3", "JuliaSyntax", "Serialization"] +path = "." +uuid = "b3cc710f-9c33-5bdb-a03d-a94903873e97" +version = "0.9.0" + +[[deps.Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" + +[[deps.Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.3.1" + +[[deps.Revise]] +deps = ["CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] +git-tree-sha1 = "14d1bfb0a30317edc77e11094607ace3c800f193" +uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" +version = "3.13.2" +weakdeps = ["Distributed"] + + [deps.Revise.extensions] + DistributedExt = "Distributed" + +[[deps.RuntimeGeneratedFunctions]] +deps = ["ExprTools", "SHA", "Serialization"] +git-tree-sha1 = "7257165d5477fd1025f7cb656019dcb6b0512c38" +uuid = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47" +version = "0.5.17" [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" @@ -72,26 +489,138 @@ version = "0.7.0" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "1.11.0" + +[[deps.Statistics]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.11.1" + + [deps.Statistics.extensions] + SparseArraysExt = ["SparseArrays"] + + [deps.Statistics.weakdeps] + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [[deps.StructTypes]] deps = ["Dates", "UUIDs"] -git-tree-sha1 = "ca4bccb03acf9faaf4137a9abc1881ed1841aa70" +git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8" uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" -version = "1.10.0" +version = "1.11.0" + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" [[deps.TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" version = "1.0.3" +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.11.0" + [[deps.Tokenize]] git-tree-sha1 = "468b4685af4abe0e9fd4d7bf495a6554a6276e75" uuid = "0796e94c-ce3b-5d07-9a54-7f471281c624" version = "0.5.29" +[[deps.TranscodingStreams]] +git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.11.3" + [[deps.UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" [[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + +[[deps.Xorg_libX11_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll", "Xorg_xtrans_jll"] +git-tree-sha1 = "808090ede1d41644447dd5cbafced4731c56bd2f" +uuid = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc" +version = "1.8.13+0" + +[[deps.Xorg_libXau_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "aa1261ebbac3ccc8d16558ae6799524c450ed16b" +uuid = "0c0b7dd1-d40b-584c-a123-a41640f87eec" +version = "1.0.13+0" + +[[deps.Xorg_libXdmcp_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "52858d64353db33a56e13c341d7bf44cd0d7b309" +uuid = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05" +version = "1.1.6+0" + +[[deps.Xorg_libXext_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "1a4a26870bf1e5d26cd585e38038d399d7e65706" +uuid = "1082639a-0dae-5f34-9b06-72781eeb8cb3" +version = "1.3.8+0" + +[[deps.Xorg_libXrender_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "7ed9347888fac59a618302ee38216dd0379c480d" +uuid = "ea2f1a96-1ddc-540d-b46f-429655e07cfa" +version = "0.9.12+0" + +[[deps.Xorg_libxcb_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXau_jll", "Xorg_libXdmcp_jll"] +git-tree-sha1 = "bfcaf7ec088eaba362093393fe11aa141fa15422" +uuid = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b" +version = "1.17.1+0" + +[[deps.Xorg_xtrans_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a63799ff68005991f9d9491b6e95bd3478d783cb" +uuid = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10" +version = "1.6.0+0" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.3.1+2" + +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +version = "5.15.0+0" + +[[deps.libpng_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "e015f211ebb898c8180887012b938f3851e719ac" +uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" +version = "1.6.55+0" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.64.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.7.0+0" + +[[deps.pprof_jll]] +deps = ["Artifacts", "Graphviz_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "41bcaa13dd41f13b2290944e5e5be035e90c6bc4" +uuid = "cf2c5f97-e748-59fa-a03f-dda3c62118cb" +version = "2.0.0+0" diff --git a/Project.toml b/Project.toml index 0ef79f5..12ffd3c 100644 --- a/Project.toml +++ b/Project.toml @@ -3,14 +3,17 @@ uuid = "b3cc710f-9c33-5bdb-a03d-a94903873e97" version = "0.9.0" [deps] +Argus = "e39743ec-b2cc-45ef-a990-b666fc093538" CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [compat] CSTParser = "3.3" +JuliaSyntax = "1.0.2" julia = "1" [extras] diff --git a/src/ReLint.jl b/src/ReLint.jl index 271a1ce..9828d4c 100644 --- a/src/ReLint.jl +++ b/src/ReLint.jl @@ -1,5 +1,7 @@ module ReLint +using Argus +using JuliaSyntax using CSTParser: CSTParser, EXPR import InteractiveUtils @@ -9,6 +11,7 @@ mutable struct LintMeta LintMeta(v) = new(v) end +include("linting/argus-rules.jl") include("linting/extended_checks.jl") include("interface.jl") -end \ No newline at end of file +end diff --git a/src/interface.jl b/src/interface.jl index bc8c01f..19854a1 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -4,28 +4,52 @@ using JSON3 global MAX_REPORTED_ERRORS = 60 # 1_000_000 # Each individual rule violation report +# mutable struct LintRuleReport +# rule::LintRule +# msg::String +# template::String +# file::String +# line::Int64 +# column::Int64 +# is_disabled::Bool # Happens with a comments in the code + +# offset::Int64 +# end +# LintRuleReport(rule::LintRule, msg::String) = LintRuleReport(rule, msg, "", "", 0, 0, false, 0) mutable struct LintRuleReport - rule::LintRule + rule::Rule msg::String template::String file::String line::Int64 column::Int64 - is_disabled::Bool # Happens with a comments in the code - + is_disabled::Bool offset::Int64 end -LintRuleReport(rule::LintRule, msg::String) = LintRuleReport(rule, msg, "", "", 0, 0, false, 0) - -is_recommendation(::T) where {T <: RecommendationLintRule} = true -is_recommendation(::T) where {T <: LineRecommendationLintRule} = true -is_recommendation(_) = false -is_violation(::T) where {T <: ViolationLintRule} = true -is_violation(::T) where {T <: LineViolationLintRule} = true -is_violation(_) = false -is_fatal(r::T) where {T <: FatalLintRule} = true -is_fatal(r::T) where {T <: LineFatalLintRule} = true -is_fatal(_) = false +LintRuleReport(rule::Rule, msg::String) = LintRuleReport(rule, msg, "", "", 0, 0, false, 0) + +Argus_result_to_LintRuleReport(rule::Rule, bindings::BindingSet) = + LintRuleReport(rule, + rule.description, + "", + bindings.file_name, + bindings.source_location[1], + bindings.source_location[2], + false, + 0) + +# is_recommendation(::T) where {T <: RecommendationLintRule} = true +# is_recommendation(::T) where {T <: LineRecommendationLintRule} = true +# is_recommendation(_) = false +# is_violation(::T) where {T <: ViolationLintRule} = true +# is_violation(::T) where {T <: LineViolationLintRule} = true +# is_violation(_) = false +# is_fatal(r::T) where {T <: FatalLintRule} = true +# is_fatal(r::T) where {T <: LineFatalLintRule} = true +# is_fatal(_) = false +is_recommendation(r::Rule) = haskey(recommendation, r.name) +is_violation(r::Rule) = haskey(violation, r.name) +is_fatal(r::Rule) = haskey(fatal, r.name) is_recommendation(r::LintRuleReport) = is_recommendation(r.rule) is_violation(r::LintRuleReport) = is_violation(r.rule) @@ -157,58 +181,66 @@ Runs lint checks on `text`, lints will be reported as comming from `file`. """ function lint_text(file_content_string::AbstractString; filename = "", context = LintContext()) - ast = CSTParser.parse(file_content_string, true) + # ast = CSTParser.parse(file_content_string, true) + ast = JuliaSyntax.parseall(SyntaxNode, file_content_string; filename=filename) all_lines = split(file_content_string, "\n") markers::Dict{Symbol, String} = Dict(:filename => filename) - check_all(ast, markers, context) + # check_all(ast, markers, context) lint_rule_reports = [] - # AST rules - for (offset, x) in collect_lint_report(ast) - if haserror(x) - # The next line should be deleted - lint_rule_report = x.meta.error - lint_rule_report.offset = offset - - line_number, column, annotation_line = convert_offset_to_line_from_lines(lint_rule_report.offset + 1, all_lines) - lint_rule_report.line = line_number - lint_rule_report.column = column - - # If the annotation is to disable lint, - if annotation_line == "lint-disable-line" - # then we disable it. - elseif !isnothing(annotation_line) && startswith("lint-disable-line: $(lint_rule_report.msg)", annotation_line) - # then we disable it. - else - # Else we record it. - push!(lint_rule_reports, lint_rule_report) - end + for rule in context.rules_to_run + match_results = map(m -> m[1], rule_match(rule, ast).matches) + for match_result in match_results + push!(lint_rule_reports, Argus_result_to_LintRuleReport(rule, match_result)) end end - # Text rules - for (line_number, line) in enumerate(all_lines) - for rule_type in line_rules(context) - rule = rule_type() - (is_error, msg) = check(rule, string(line), markers) - if is_error - # We have a violation - lint_rule_report = LintRuleReport( - rule, - msg, - "", - filename, - line_number, - 1, - false, - 0 - ) - push!(lint_rule_reports, lint_rule_report) - end - end - end + # # AST rules + # for (offset, x) in collect_lint_report(ast) + # if haserror(x) + # # The next line should be deleted + # lint_rule_report = x.meta.error + # lint_rule_report.offset = offset + + # line_number, column, annotation_line = convert_offset_to_line_from_lines(lint_rule_report.offset + 1, all_lines) + # lint_rule_report.line = line_number + # lint_rule_report.column = column + + # # If the annotation is to disable lint, + # if annotation_line == "lint-disable-line" + # # then we disable it. + # elseif !isnothing(annotation_line) && startswith("lint-disable-line: $(lint_rule_report.msg)", annotation_line) + # # then we disable it. + # else + # # Else we record it. + # push!(lint_rule_reports, lint_rule_report) + # end + # end + # end + + # # Text rules + # for (line_number, line) in enumerate(all_lines) + # for rule_type in line_rules(context) + # rule = rule_type() + # (is_error, msg) = check(rule, string(line), markers) + # if is_error + # # We have a violation + # lint_rule_report = LintRuleReport( + # rule, + # msg, + # "", + # filename, + # line_number, + # 1, + # false, + # 0 + # ) + # push!(lint_rule_reports, lint_rule_report) + # end + # end + # end return lint_rule_reports end diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl new file mode 100644 index 0000000..536a2ad --- /dev/null +++ b/src/linting/argus-rules.jl @@ -0,0 +1,232 @@ +using Argus + +general = RuleGroup("general") +recommendation = RuleGroup("recommendation") +violation = RuleGroup("violation") +fatal = RuleGroup("fatal") + +@define_rule_in_group general "error" begin + description = """ + Use custom exception instead of the generic `error()`. + """ + + pattern = @pattern error({_}...) +end + +@define_rule_in_group general "in" begin + description = """ + Use `tin(item,collection)` instead of the Julia's `in` or `∈`. + """ + + pattern = @pattern ~or( + in({_}, {_}), + ∈({_}, {_}) + ) +end + +@define_rule_in_group general "haskey" begin + description = """ + Use `thaskey(dict,key)` instead of the Julia's `haskey`. + """ + + pattern = @pattern haskey({_}, {_}) +end + +# StringInterpolationRule +# TODO: `trivia` flag. +# @define_rule_in_group general "string-interpolation" begin +# description = raw""" +# Use $(x) instead of $x ([explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#string-interpolation)). +# """ + +# pattern = @pattern ... +# end + +# ReturnTypeAnnotationRule +@define_rule_in_group general "return-type-annotation" begin + description = """ + Avoid return type annotations `function foo()::Type`. + Return type annotations can hurt performance by forcing type conversions. + [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations). + """ + + pattern = @pattern ~or( + function ({f}({args}...)::{ret_type}) {_}... end, + {_:::funcall}::{ret_type} = {_} + ) +end + +# GeneratedRule +@define_rule_in_group fatal "generated" begin + description = """ + Don't use `@generated` + """ + + pattern = @pattern @generated {_}... +end + +# LogStatementsMustBeSafe +# +# TODO: Syntax classes with parameters (e.g. macrocall(r"@info|@warn|@error|@debug")). +@define_syntax_class :log_macro "log macro" begin + @pattern begin + {m:::macrocall} + @fail [:m] begin + log_macro_names = r"@info|@warn|@error|@debug" + !startswith(m.name, log_macro_names) + end "" + end +end +@define_syntax_class :safe_macro_arg "safe macro argument" begin + # Safe literal. + @pattern begin + {arg} + @fail [:arg] begin + s = arg.src + k = kind(s) + # Strings are not safe. + k == K"string" || endswith(string(k), "String") && return true + # `nothing` is safe. + !isnothing(s.data.val) && s.data.val == :nothing && return false + # Chars and literals other than strings are safe. + (JuliaSyntax.is_literal(s) || k == K"char") && return false + # Anything else is not safe. + return true + end "" + end + # Argument wrapped in `@safe`. + @pattern ~or(@safe({args}...), + SafeLogging.@safe({args}...), + $SafeLogging.@safe({args}...)) + # Assignment with safe rhs. + @pattern {_} = {arg:::safe_macro_arg} +end +# TODO: Rewrite with `not` pattern? +@define_rule_in_group fatal "unsafe-logging" begin + description = """ + Unsafe logging statement. You must enclose variables and strings with `@safe(...)`. + """ + + pattern = @pattern begin + {log_macro:::log_macro} + @fail [:log_macro] begin + for s in log_macro.args + c = Argus.SYNTAX_CLASS_REGISTRY[:safe_macro_arg] + is_successful(syntax_match(c, s)) || return false + end + return true + end "log macro call with no unsafe args"; + end +end + +# AssertionStatementsMustBeSafe +# +# TODO: Syntax classes with parameters (e.g. macrocall(r"@assert|@dassert")). +@define_syntax_class :assert_macro "assert macro" begin + @pattern begin + {m:::macrocall} + @fail [:m] begin + assert_macro_names = r"@assert|@dassert" + !startswith(m.name, assert_macro_names) + end "" + end +end +@define_rule_in_group fatal "unsafe-assert" begin + description = """ + Unsafe assertion statement. You must enclose the message with `@safe(...)`. + """ + + pattern = @pattern begin + {assert_macro:::assert_macro} + @fail [:assert_macro] begin + # Assert macro call with one argument is safe (the matching fails). + length(assert_macro.args) == 1 && return true + for s in assert_macro.args[2:end] + c = Argus.SYNTAX_CLASS_REGISTRY[:safe_macro_arg] + is_successful(syntax_match(c, s)) || return false + end + # All assert macro call arguments are safe. + return true + end "assert macro call with no unsafe args"; + end +end + +# NonFrontShapeAPIUsageRule + +# MustNotUseShow +# +# TODO: Template. +@define_rule_in_group fatal "show" begin + description = """ + Do not use `@show`, use `@info` instead. + """ + + pattern = @pattern @show {_}... +end + +# NoinlineAndLiteralRule +# +# TODO: Splatting. +@define_syntax_class :noinline_with_non_lit_or_id_args "@nonline call with non lit or id args" begin + # Call or dotcall. + @pattern begin + ~or( + @noinline(({_})({args}...)), + @noinline(({_}).({args}...)), + ) + @fail [:args] begin + for s in args.src + k = kind(s) + k == JuliaSyntax.K"parameters" && break + JuliaSyntax.is_literal(s) || JuliaSyntax.is_identifier(s) || k == JuliaSyntax.K"char" || k == JuliaSyntax.K"string" || + return false + end + return true + end "" + end + # Call or dotcall with kwargs. + @pattern begin + ~or( + @noinline(({_})({args}...; {kwargs}...)), + @noinline(({_}).({args}...; {kwargs}...)) + ) + @fail [:args, :kwargs] begin + for s in args.src + k = kind(s) + JuliaSyntax.is_literal(s) || JuliaSyntax.is_identifier(s) || k == JuliaSyntax.K"char" || k == JuliaSyntax.K"string" || + return false + end + for kws in kwargs.src + k = kind(kws) + JuliaSyntax.is_literal(kws) || JuliaSyntax.is_identifier(kws) || k == JuliaSyntax.K"char" || k == JuliaSyntax.K"string" || + k == JuliaSyntax.K"=" && (JuliaSyntax.is_literal(kws.children[2]) || JuliaSyntax.is_identifier(kws.children[2])) || + return false + end + return true + end "" + end + # Macrocall. + @pattern begin + @noinline({c:::macrocall}) + @fail [:c] begin + for s in c.args + k = kind(s) + JuliaSyntax.is_literal(s) || JuliaSyntax.is_identifier(s) || #k == Kind("char") || k == Kind("string") || + return false + end + return true + end "" + end +end +# TODO: Support for allowing match if the source and pattern differ only by a flag +# (e.g. INFIX_FLAG). +@define_rule_in_group fatal "noinline-literal" begin + description = """ + For call-site `@noinline` call, all args must be literals or identifiers only. \ + Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086). + """ + + pattern = @pattern {n:::noinline_with_non_lit_or_id_args} +end + +# NoReturnInAnonymousFunctionRule diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl index 50bda90..926cd27 100644 --- a/src/linting/extended_checks.jl +++ b/src/linting/extended_checks.jl @@ -18,7 +18,7 @@ # abstract type LintFileExclusion end struct LintContext - rules_to_run::Vector{DataType} + rules_to_run::Vector{Rule} regex_exclusions #::Vector{LintFileExclusion} function LintContext(dts_as_str::Vector{String}) @@ -31,8 +31,8 @@ struct LintContext return new(dt, []) end - LintContext(s::Vector{DataType}) = new(s, []) - LintContext(s::Vector{Any}) = new(convert(Vector{DataType}, s) , []) + LintContext(s::Vector{Rule}) = new(s, []) + # LintContext(s::Vector{Any}) = new(convert(Vector{DataType}, s) , []) LintContext() = new(all_rules(), []) LintContext(a, b) = new(a, b) end @@ -388,7 +388,9 @@ const all_text_lint_rule_types = Ref{Vector{DataType}}( ) function all_rules() - return vcat(all_extended_rule_types[], all_text_lint_rule_types[]) + # return vcat(all_extended_rule_types[], all_text_lint_rule_types[]) + return [general["error"], general["in"], general["haskey"], + fatal["unsafe-logging"], fatal["unsafe-assert"]] end # template -> EXPR to be compared @@ -1069,4 +1071,4 @@ function check(t::AccessingENVRule, x::EXPR) generic_check(t, x, "setindex!(ENV, hole_variable, hole_variable)", msg) generic_check(t, x, "get!(ENV, hole_variable, hole_variable)", msg) generic_check(t, x, "push!(ENV, hole_variable)", msg) -end \ No newline at end of file +end diff --git a/test/common.jl b/test/common.jl index a408526..cf21775 100644 --- a/test/common.jl +++ b/test/common.jl @@ -36,4 +36,4 @@ end function lint_has_error_test(source::String, verbose=false; directory::String = "", context::LintContext=LintContext()) return count_lint_errors(source, verbose; directory, context) > 0 -end \ No newline at end of file +end diff --git a/test/runtests.jl b/test/runtests.jl index 878e44a..3c68c1e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,13 +2,14 @@ using ReLint using CSTParser, Test using ReLint: convert_offset_to_line_from_lines, check_all +include("../src/linting/argus-rules.jl") include(joinpath(@__DIR__, "common.jl")) -include(joinpath(@__DIR__, "line_rules_tests.jl")) -include(joinpath(@__DIR__, "file_exclusion_tests.jl")) -include(joinpath(@__DIR__, "noinline_tests.jl")) +# include(joinpath(@__DIR__, "line_rules_tests.jl")) +# include(joinpath(@__DIR__, "file_exclusion_tests.jl")) +# include(joinpath(@__DIR__, "noinline_tests.jl")) include(joinpath(@__DIR__, "safe_logging_tests.jl")) -include(joinpath(@__DIR__, "lint_context_tests.jl")) -include(joinpath(@__DIR__, "rai_rules_tests.jl")) -include(joinpath(@__DIR__, "test_new_rules.jl")) +# include(joinpath(@__DIR__, "lint_context_tests.jl")) +# include(joinpath(@__DIR__, "rai_rules_tests.jl")) +# include(joinpath(@__DIR__, "test_new_rules.jl")) diff --git a/test/safe_logging_tests.jl b/test/safe_logging_tests.jl index 67d2297..7ec5c6d 100644 --- a/test/safe_logging_tests.jl +++ b/test/safe_logging_tests.jl @@ -63,7 +63,7 @@ @testset "@safe in a macro 1" begin source = raw""" macro spawn_periodic_task(name, period, expr, ending_expr=nothing) - error_msg = "Periodic task name must be `@safe(\"name\")`, so it can be logged: $(__source__)" + error_msg = "Periodic task name must be `@safe(\\\"name\\\")`, so it can be logged: $(__source__)" @dassert0 is_safe_expr_literal(name) @safe(error_msg) return quote @@ -95,4 +95,4 @@ """ @test count_lint_errors(source) == 0 end -end \ No newline at end of file +end From f5c21406b1e457e446e3d6266fa960d759e52512 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 5 Mar 2026 13:51:55 +0100 Subject: [PATCH 03/62] Merge from upstream --- Manifest.toml | 626 -------------------------------------------------- Project.toml | 3 + 2 files changed, 3 insertions(+), 626 deletions(-) delete mode 100644 Manifest.toml diff --git a/Manifest.toml b/Manifest.toml deleted file mode 100644 index 6154e1d..0000000 --- a/Manifest.toml +++ /dev/null @@ -1,626 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -julia_version = "1.12.5" -manifest_format = "2.0" -project_hash = "615a86e01f16e17a918836e2f0db62c76583f5fa" - -[[deps.AbstractTrees]] -git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" -uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" -version = "0.4.5" - -[[deps.ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.2" - -[[deps.Argus]] -deps = ["InteractiveUtils", "JET", "JuliaSyntax", "MacroTools", "PProf", "RuntimeGeneratedFunctions", "Test"] -path = "../../iuliadmtru/Argus.jl" -uuid = "e39743ec-b2cc-45ef-a990-b666fc093538" -version = "0.1.0" - -[[deps.Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" -version = "1.11.0" - -[[deps.Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -version = "1.11.0" - -[[deps.BufferedStreams]] -git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643" -uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d" -version = "1.2.2" - -[[deps.Bzip2_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "1b96ea4a01afe0ea4090c5c8039690672dd13f2e" -uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" -version = "1.0.9+0" - -[[deps.CSTParser]] -deps = ["Tokenize"] -git-tree-sha1 = "0157e592151e39fa570645e2b2debcdfb8a0f112" -uuid = "00ebfdb7-1f24-5e51-bd34-a7502290713f" -version = "3.4.3" - -[[deps.Cairo_jll]] -deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "LZO_jll", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] -git-tree-sha1 = "a21c5464519504e41e0cbc91f0188e8ca23d7440" -uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" -version = "1.18.5+1" - -[[deps.CodeTracking]] -deps = ["InteractiveUtils", "UUIDs"] -git-tree-sha1 = "b7231a755812695b8046e8471ddc34c8268cbad5" -uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" -version = "3.0.0" - -[[deps.CodecZlib]] -deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" -uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.8" - -[[deps.ColorTypes]] -deps = ["FixedPointNumbers", "Random"] -git-tree-sha1 = "67e11ee83a43eb71ddc950302c53bf33f0690dfe" -uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" -version = "0.12.1" -weakdeps = ["StyledStrings"] - - [deps.ColorTypes.extensions] - StyledStringsExt = "StyledStrings" - -[[deps.Colors]] -deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] -git-tree-sha1 = "37ea44092930b1811e666c3bc38065d7d87fcc74" -uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" -version = "0.13.1" - -[[deps.Compiler]] -git-tree-sha1 = "382d79bfe72a406294faca39ef0c3cef6e6ce1f1" -uuid = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" -version = "0.1.1" - -[[deps.CompilerSupportLibraries_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.3.0+1" - -[[deps.Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -version = "1.11.0" - -[[deps.Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" -version = "1.11.0" - -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.7.0" - -[[deps.EnumX]] -git-tree-sha1 = "c49898e8438c828577f04b92fc9368c388ac783c" -uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" -version = "1.0.7" - -[[deps.Expat_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "27af30de8b5445644e8ffe3bcb0d72049c089cf1" -uuid = "2e619515-83b5-522b-bb60-26c02a35a201" -version = "2.7.3+0" - -[[deps.ExprTools]] -git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" -uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" -version = "0.1.10" - -[[deps.FileIO]] -deps = ["Pkg", "Requires", "UUIDs"] -git-tree-sha1 = "6522cfb3b8fe97bec632252263057996cbd3de20" -uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -version = "1.18.0" - - [deps.FileIO.extensions] - HTTPExt = "HTTP" - - [deps.FileIO.weakdeps] - HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" - -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" -version = "1.11.0" - -[[deps.FixedPointNumbers]] -deps = ["Statistics"] -git-tree-sha1 = "05882d6995ae5c12bb5f36dd2ed3f61c98cbb172" -uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" -version = "0.8.5" - -[[deps.FlameGraphs]] -deps = ["AbstractTrees", "Colors", "FileIO", "FixedPointNumbers", "IndirectArrays", "LeftChildRightSiblingTrees", "Profile"] -git-tree-sha1 = "0166baf81babb91cf78bfcc771d8e87c43d568df" -uuid = "08572546-2f56-4bcf-ba4e-bab62c3a3f89" -version = "1.1.0" - -[[deps.Fontconfig_jll]] -deps = ["Artifacts", "Bzip2_jll", "Expat_jll", "FreeType2_jll", "JLLWrappers", "Libdl", "Libuuid_jll", "Zlib_jll"] -git-tree-sha1 = "f85dac9a96a01087df6e3a749840015a0ca3817d" -uuid = "a3f928ae-7b40-5064-980b-68af3947d34b" -version = "2.17.1+0" - -[[deps.FreeType2_jll]] -deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "Zlib_jll"] -git-tree-sha1 = "2c5512e11c791d1baed2049c5652441b28fc6a31" -uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7" -version = "2.13.4+0" - -[[deps.FriBidi_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "7a214fdac5ed5f59a22c2d9a885a16da1c74bbc7" -uuid = "559328eb-81f9-559d-9380-de523a88c83c" -version = "1.0.17+0" - -[[deps.GettextRuntime_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Libiconv_jll"] -git-tree-sha1 = "45288942190db7c5f760f59c04495064eedf9340" -uuid = "b0724c58-0f36-5564-988d-3bb0596ebc4a" -version = "0.22.4+0" - -[[deps.Glib_jll]] -deps = ["Artifacts", "GettextRuntime_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "24f6def62397474a297bfcec22384101609142ed" -uuid = "7746bdde-850d-59dc-9ae8-88ece973131d" -version = "2.86.3+0" - -[[deps.Graphite2_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "8a6dbda1fd736d60cc477d99f2e7a042acfa46e8" -uuid = "3b182d85-2403-5c21-9c21-1e1f0cc25472" -version = "1.3.15+0" - -[[deps.Graphviz_jll]] -deps = ["Artifacts", "Cairo_jll", "Expat_jll", "JLLWrappers", "Libdl", "Pango_jll", "Pkg"] -git-tree-sha1 = "a5d45833dda71048117e8a9828bef75c03b18b1c" -uuid = "3c863552-8265-54e4-a6dc-903eb78fde85" -version = "2.50.0+1" - -[[deps.HarfBuzz_jll]] -deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll"] -git-tree-sha1 = "f923f9a774fcf3f5cb761bfa43aeadd689714813" -uuid = "2e76f6c2-a576-52d4-95c1-20adfe4de566" -version = "8.5.1+0" - -[[deps.IndirectArrays]] -git-tree-sha1 = "012e604e1c7458645cb8b436f8fba789a51b257f" -uuid = "9b13fd28-a010-5f03-acff-a1bbcff69959" -version = "1.0.0" - -[[deps.InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -version = "1.11.0" - -[[deps.JET]] -deps = ["CodeTracking", "Compiler", "InteractiveUtils", "JuliaInterpreter", "JuliaSyntax", "LoweredCodeUtils", "MacroTools", "Pkg", "PrecompileTools", "Preferences", "Revise", "Test"] -git-tree-sha1 = "3642228b0d4ab0b263a5946f60ace6c2b5616256" -uuid = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" -version = "0.11.3" - -[[deps.JLLWrappers]] -deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" -uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.7.1" - -[[deps.JSON3]] -deps = ["Dates", "Mmap", "Parsers", "PrecompileTools", "StructTypes", "UUIDs"] -git-tree-sha1 = "411eccfe8aba0814ffa0fdf4860913ed09c34975" -uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" -version = "1.14.3" - - [deps.JSON3.extensions] - JSON3ArrowExt = ["ArrowTypes"] - - [deps.JSON3.weakdeps] - ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" - -[[deps.JuliaInterpreter]] -deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] -git-tree-sha1 = "f5e59455236d8269b7868665c3665e8477af0e37" -uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" -version = "0.10.10" - -[[deps.JuliaSyntax]] -git-tree-sha1 = "0d4b3dab95018bcf3925204475693d9f09dc45b8" -uuid = "70703baa-626e-46a2-a12c-08ffd08c73b4" -version = "1.0.2" - -[[deps.JuliaSyntaxHighlighting]] -deps = ["StyledStrings"] -uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" -version = "1.12.0" - -[[deps.LLVMOpenMP_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "eb62a3deb62fc6d8822c0c4bef73e4412419c5d8" -uuid = "1d63c593-3942-5779-bab2-d838dc0a180e" -version = "18.1.8+0" - -[[deps.LZO_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "1c602b1127f4751facb671441ca72715cc95938a" -uuid = "dd4b983a-f0e5-5f8d-a1b7-129d4a5fb1ac" -version = "2.10.3+0" - -[[deps.LeftChildRightSiblingTrees]] -deps = ["AbstractTrees"] -git-tree-sha1 = "95ba48564903b43b2462318aa243ee79d81135ff" -uuid = "1d6d02ad-be62-4b6b-8a6d-2f90e265016e" -version = "0.2.1" - -[[deps.LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.4" - -[[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.15.0+0" - -[[deps.LibGit2]] -deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -version = "1.11.0" - -[[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] -uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.9.0+0" - -[[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "OpenSSL_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.3+1" - -[[deps.Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -version = "1.11.0" - -[[deps.Libffi_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "c8da7e6a91781c41a863611c7e966098d783c57a" -uuid = "e9f186c6-92d2-5b65-8a66-fee21dc1b490" -version = "3.4.7+0" - -[[deps.Libiconv_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" -uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" -version = "1.18.0+0" - -[[deps.Libmount_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "97bbca976196f2a1eb9607131cb108c69ec3f8a6" -uuid = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9" -version = "2.41.3+0" - -[[deps.Libuuid_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "d0205286d9eceadc518742860bf23f703779a3d6" -uuid = "38a345b3-de98-5d2b-a5d3-14cd9215e700" -version = "2.41.3+0" - -[[deps.LinearAlgebra]] -deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] -uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -version = "1.12.0" - -[[deps.Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -version = "1.11.0" - -[[deps.LoweredCodeUtils]] -deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] -git-tree-sha1 = "5d4278f755440f70648d80cc6225f51e78e94094" -uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" -version = "3.5.1" - -[[deps.MacroTools]] -git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" -uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -version = "0.5.16" - -[[deps.Markdown]] -deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -version = "1.11.0" - -[[deps.Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" -version = "1.11.0" - -[[deps.MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2025.11.4" - -[[deps.NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.3.0" - -[[deps.OpenBLAS_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] -uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.29+0" - -[[deps.OpenSSL_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.4+0" - -[[deps.OrderedCollections]] -git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" -uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.8.1" - -[[deps.PCRE2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" -version = "10.44.0+1" - -[[deps.PProf]] -deps = ["AbstractTrees", "CodecZlib", "EnumX", "FlameGraphs", "Libdl", "OrderedCollections", "Profile", "ProgressMeter", "ProtoBuf", "pprof_jll"] -git-tree-sha1 = "2b62c1a1fde38c21023d5786cd37ef95d9d7acd4" -uuid = "e4faabce-9ead-11e9-39d9-4379958e3056" -version = "3.2.0" - -[[deps.Pango_jll]] -deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "FriBidi_jll", "Glib_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl"] -git-tree-sha1 = "0662b083e11420952f2e62e17eddae7fc07d5997" -uuid = "36c8627f-9965-5494-a995-c6b170f724f3" -version = "1.57.0+0" - -[[deps.Parsers]] -deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "7d2f8f21da5db6a806faf7b9b292296da42b2810" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.3" - -[[deps.Pixman_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] -git-tree-sha1 = "db76b1ecd5e9715f3d043cec13b2ec93ce015d53" -uuid = "30392449-352a-5448-841d-b1acce4e97dc" -version = "0.44.2+0" - -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.1" -weakdeps = ["REPL"] - - [deps.Pkg.extensions] - REPLExt = "REPL" - -[[deps.PrecompileTools]] -deps = ["Preferences"] -git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" -uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.3.3" - -[[deps.Preferences]] -deps = ["TOML"] -git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.5.2" - -[[deps.Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -version = "1.11.0" - -[[deps.Profile]] -deps = ["StyledStrings"] -uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" -version = "1.11.0" - -[[deps.ProgressMeter]] -deps = ["Distributed", "Printf"] -git-tree-sha1 = "fbb92c6c56b34e1a2c4c36058f68f332bec840e7" -uuid = "92933f4c-e287-5a05-a399-4b506db050ca" -version = "1.11.0" - -[[deps.ProtoBuf]] -deps = ["BufferedStreams", "EnumX", "TOML"] -git-tree-sha1 = "da18083a52d9d57bbe6dadaacad39731e5f7be39" -uuid = "3349acd9-ac6a-5e09-bcdb-63829b23a429" -version = "1.3.0" - -[[deps.REPL]] -deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -version = "1.11.0" - -[[deps.Random]] -deps = ["SHA"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -version = "1.11.0" - -[[deps.ReLint]] -deps = ["Argus", "CSTParser", "Dates", "InteractiveUtils", "JSON3", "JuliaSyntax", "Serialization"] -path = "." -uuid = "b3cc710f-9c33-5bdb-a03d-a94903873e97" -version = "0.9.0" - -[[deps.Reexport]] -git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" -uuid = "189a3867-3050-52da-a836-e630ba90ab69" -version = "1.2.2" - -[[deps.Requires]] -deps = ["UUIDs"] -git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64" -uuid = "ae029012-a4dd-5104-9daa-d747884805df" -version = "1.3.1" - -[[deps.Revise]] -deps = ["CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] -git-tree-sha1 = "14d1bfb0a30317edc77e11094607ace3c800f193" -uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" -version = "3.13.2" -weakdeps = ["Distributed"] - - [deps.Revise.extensions] - DistributedExt = "Distributed" - -[[deps.RuntimeGeneratedFunctions]] -deps = ["ExprTools", "SHA", "Serialization"] -git-tree-sha1 = "7257165d5477fd1025f7cb656019dcb6b0512c38" -uuid = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47" -version = "0.5.17" - -[[deps.SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -version = "0.7.0" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -version = "1.11.0" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -version = "1.11.0" - -[[deps.Statistics]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" -uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -version = "1.11.1" - - [deps.Statistics.extensions] - SparseArraysExt = ["SparseArrays"] - - [deps.Statistics.weakdeps] - SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" - -[[deps.StructTypes]] -deps = ["Dates", "UUIDs"] -git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8" -uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" -version = "1.11.0" - -[[deps.StyledStrings]] -uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" -version = "1.11.0" - -[[deps.TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.3" - -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.0" - -[[deps.Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -version = "1.11.0" - -[[deps.Tokenize]] -git-tree-sha1 = "468b4685af4abe0e9fd4d7bf495a6554a6276e75" -uuid = "0796e94c-ce3b-5d07-9a54-7f471281c624" -version = "0.5.29" - -[[deps.TranscodingStreams]] -git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" -uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.11.3" - -[[deps.UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -version = "1.11.0" - -[[deps.Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -version = "1.11.0" - -[[deps.Xorg_libX11_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll", "Xorg_xtrans_jll"] -git-tree-sha1 = "808090ede1d41644447dd5cbafced4731c56bd2f" -uuid = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc" -version = "1.8.13+0" - -[[deps.Xorg_libXau_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "aa1261ebbac3ccc8d16558ae6799524c450ed16b" -uuid = "0c0b7dd1-d40b-584c-a123-a41640f87eec" -version = "1.0.13+0" - -[[deps.Xorg_libXdmcp_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "52858d64353db33a56e13c341d7bf44cd0d7b309" -uuid = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05" -version = "1.1.6+0" - -[[deps.Xorg_libXext_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] -git-tree-sha1 = "1a4a26870bf1e5d26cd585e38038d399d7e65706" -uuid = "1082639a-0dae-5f34-9b06-72781eeb8cb3" -version = "1.3.8+0" - -[[deps.Xorg_libXrender_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] -git-tree-sha1 = "7ed9347888fac59a618302ee38216dd0379c480d" -uuid = "ea2f1a96-1ddc-540d-b46f-429655e07cfa" -version = "0.9.12+0" - -[[deps.Xorg_libxcb_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXau_jll", "Xorg_libXdmcp_jll"] -git-tree-sha1 = "bfcaf7ec088eaba362093393fe11aa141fa15422" -uuid = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b" -version = "1.17.1+0" - -[[deps.Xorg_xtrans_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "a63799ff68005991f9d9491b6e95bd3478d783cb" -uuid = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10" -version = "1.6.0+0" - -[[deps.Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.3.1+2" - -[[deps.libblastrampoline_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.15.0+0" - -[[deps.libpng_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] -git-tree-sha1 = "e015f211ebb898c8180887012b938f3851e719ac" -uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" -version = "1.6.55+0" - -[[deps.nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.64.0+1" - -[[deps.p7zip_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.7.0+0" - -[[deps.pprof_jll]] -deps = ["Artifacts", "Graphviz_jll", "JLLWrappers", "Libdl"] -git-tree-sha1 = "41bcaa13dd41f13b2290944e5e5be035e90c6bc4" -uuid = "cf2c5f97-e748-59fa-a03f-dda3c62118cb" -version = "2.0.0+0" diff --git a/Project.toml b/Project.toml index 12ffd3c..87864ba 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ version = "0.9.0" [deps] Argus = "e39743ec-b2cc-45ef-a990-b666fc093538" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" @@ -12,6 +13,8 @@ JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [compat] +Argus = "0.1.0" +BenchmarkTools = "1.6.3" CSTParser = "3.3" JuliaSyntax = "1.0.2" julia = "1" From 7ee9797c89cc221f7b9e79b89b6ae9333c83608e Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 6 Mar 2026 13:19:55 +0100 Subject: [PATCH 04/62] Define Argus rules and syntax classes at precompile time --- src/ReLint.jl | 6 +++++- test/runtests.jl | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ReLint.jl b/src/ReLint.jl index 9828d4c..e006e73 100644 --- a/src/ReLint.jl +++ b/src/ReLint.jl @@ -11,7 +11,11 @@ mutable struct LintMeta LintMeta(v) = new(v) end -include("linting/argus-rules.jl") include("linting/extended_checks.jl") include("interface.jl") + +function __init__() + include(joinpath(@__DIR__, "linting/argus-rules.jl")) +end + end diff --git a/test/runtests.jl b/test/runtests.jl index 3c68c1e..b89e235 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,7 +2,6 @@ using ReLint using CSTParser, Test using ReLint: convert_offset_to_line_from_lines, check_all -include("../src/linting/argus-rules.jl") include(joinpath(@__DIR__, "common.jl")) # include(joinpath(@__DIR__, "line_rules_tests.jl")) From 405313e02333ab0ac9bedfba7fbf9c6c117ae335 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 6 Mar 2026 13:20:31 +0100 Subject: [PATCH 05/62] Fix `@noinline` rule --- src/linting/argus-rules.jl | 52 +++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 536a2ad..91a3545 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -177,8 +177,12 @@ end @fail [:args] begin for s in args.src k = kind(s) - k == JuliaSyntax.K"parameters" && break - JuliaSyntax.is_literal(s) || JuliaSyntax.is_identifier(s) || k == JuliaSyntax.K"char" || k == JuliaSyntax.K"string" || + k == K"parameters" && break + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || return false end return true @@ -193,13 +197,17 @@ end @fail [:args, :kwargs] begin for s in args.src k = kind(s) - JuliaSyntax.is_literal(s) || JuliaSyntax.is_identifier(s) || k == JuliaSyntax.K"char" || k == JuliaSyntax.K"string" || + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || return false end for kws in kwargs.src k = kind(kws) - JuliaSyntax.is_literal(kws) || JuliaSyntax.is_identifier(kws) || k == JuliaSyntax.K"char" || k == JuliaSyntax.K"string" || - k == JuliaSyntax.K"=" && (JuliaSyntax.is_literal(kws.children[2]) || JuliaSyntax.is_identifier(kws.children[2])) || + JuliaSyntax.is_literal(kws) || JuliaSyntax.is_identifier(kws) || k == K"char" || k == K"string" || k == K"..." || + k == K"=" && (JuliaSyntax.is_literal(kws.children[2]) || JuliaSyntax.is_identifier(kws.children[2])) || return false end return true @@ -211,16 +219,46 @@ end @fail [:c] begin for s in c.args k = kind(s) - JuliaSyntax.is_literal(s) || JuliaSyntax.is_identifier(s) || #k == Kind("char") || k == Kind("string") || + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == JuliaSyntax.K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || return false end return true end "" end + # Infix call. + @pattern begin + ~or(@noinline({i:::infix_call}), + @noinline({i:::infix_dotcall})) + @fail [:i] begin + # Check lhs. + s = i.lhs.src + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return false + # Check rhs. + s = i.rhs.src + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return false + return true + end "" + end end # TODO: Support for allowing match if the source and pattern differ only by a flag # (e.g. INFIX_FLAG). -@define_rule_in_group fatal "noinline-literal" begin +@define_rule_in_group fatal "noinline-lit-or-id" begin description = """ For call-site `@noinline` call, all args must be literals or identifiers only. \ Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086). From 1a2140c10363e45a9d71ae2289cbc83f5ea66801 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 6 Mar 2026 13:20:50 +0100 Subject: [PATCH 06/62] Add `@noinline` rule tests --- src/linting/extended_checks.jl | 2 +- test/runtests.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl index 926cd27..78e06de 100644 --- a/src/linting/extended_checks.jl +++ b/src/linting/extended_checks.jl @@ -390,7 +390,7 @@ const all_text_lint_rule_types = Ref{Vector{DataType}}( function all_rules() # return vcat(all_extended_rule_types[], all_text_lint_rule_types[]) return [general["error"], general["in"], general["haskey"], - fatal["unsafe-logging"], fatal["unsafe-assert"]] + fatal["unsafe-logging"], fatal["unsafe-assert"], fatal["noinline-lit-or-id"]] end # template -> EXPR to be compared diff --git a/test/runtests.jl b/test/runtests.jl index b89e235..c668999 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,7 +6,7 @@ include(joinpath(@__DIR__, "common.jl")) # include(joinpath(@__DIR__, "line_rules_tests.jl")) # include(joinpath(@__DIR__, "file_exclusion_tests.jl")) -# include(joinpath(@__DIR__, "noinline_tests.jl")) +include(joinpath(@__DIR__, "noinline_tests.jl")) include(joinpath(@__DIR__, "safe_logging_tests.jl")) # include(joinpath(@__DIR__, "lint_context_tests.jl")) # include(joinpath(@__DIR__, "rai_rules_tests.jl")) From 01862936a7f363d18a41c30f52c4782b86ea9d88 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 6 Mar 2026 14:05:03 +0100 Subject: [PATCH 07/62] Remove CSTParser and InteractiveUtils dependencies --- Project.toml | 3 - src/ReLint.jl | 2 - src/interface.jl | 48 +- src/linting/extended_checks.jl | 1022 -------------------------------- test/common.jl | 9 +- test/runtests.jl | 4 +- 6 files changed, 6 insertions(+), 1082 deletions(-) diff --git a/Project.toml b/Project.toml index 87864ba..bedf18d 100644 --- a/Project.toml +++ b/Project.toml @@ -5,9 +5,7 @@ version = "0.9.0" [deps] Argus = "e39743ec-b2cc-45ef-a990-b666fc093538" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -15,7 +13,6 @@ Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [compat] Argus = "0.1.0" BenchmarkTools = "1.6.3" -CSTParser = "3.3" JuliaSyntax = "1.0.2" julia = "1" diff --git a/src/ReLint.jl b/src/ReLint.jl index e006e73..00afcda 100644 --- a/src/ReLint.jl +++ b/src/ReLint.jl @@ -2,8 +2,6 @@ module ReLint using Argus using JuliaSyntax -using CSTParser: CSTParser, EXPR -import InteractiveUtils mutable struct LintMeta error diff --git a/src/interface.jl b/src/interface.jl index 19854a1..7f3ded4 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -4,18 +4,6 @@ using JSON3 global MAX_REPORTED_ERRORS = 60 # 1_000_000 # Each individual rule violation report -# mutable struct LintRuleReport -# rule::LintRule -# msg::String -# template::String -# file::String -# line::Int64 -# column::Int64 -# is_disabled::Bool # Happens with a comments in the code - -# offset::Int64 -# end -# LintRuleReport(rule::LintRule, msg::String) = LintRuleReport(rule, msg, "", "", 0, 0, false, 0) mutable struct LintRuleReport rule::Rule msg::String @@ -38,15 +26,6 @@ Argus_result_to_LintRuleReport(rule::Rule, bindings::BindingSet) = false, 0) -# is_recommendation(::T) where {T <: RecommendationLintRule} = true -# is_recommendation(::T) where {T <: LineRecommendationLintRule} = true -# is_recommendation(_) = false -# is_violation(::T) where {T <: ViolationLintRule} = true -# is_violation(::T) where {T <: LineViolationLintRule} = true -# is_violation(_) = false -# is_fatal(r::T) where {T <: FatalLintRule} = true -# is_fatal(r::T) where {T <: LineFatalLintRule} = true -# is_fatal(_) = false is_recommendation(r::Rule) = haskey(recommendation, r.name) is_violation(r::Rule) = haskey(violation, r.name) is_fatal(r::Rule) = haskey(fatal, r.name) @@ -181,15 +160,15 @@ Runs lint checks on `text`, lints will be reported as comming from `file`. """ function lint_text(file_content_string::AbstractString; filename = "", context = LintContext()) - # ast = CSTParser.parse(file_content_string, true) ast = JuliaSyntax.parseall(SyntaxNode, file_content_string; filename=filename) all_lines = split(file_content_string, "\n") + # TODO: Integrate markers in Argus rules. markers::Dict{Symbol, String} = Dict(:filename => filename) - # check_all(ast, markers, context) lint_rule_reports = [] + # AST rules for rule in context.rules_to_run match_results = map(m -> m[1], rule_match(rule, ast).matches) for match_result in match_results @@ -197,29 +176,6 @@ function lint_text(file_content_string::AbstractString; filename = "", c end end - # # AST rules - # for (offset, x) in collect_lint_report(ast) - # if haserror(x) - # # The next line should be deleted - # lint_rule_report = x.meta.error - # lint_rule_report.offset = offset - - # line_number, column, annotation_line = convert_offset_to_line_from_lines(lint_rule_report.offset + 1, all_lines) - # lint_rule_report.line = line_number - # lint_rule_report.column = column - - # # If the annotation is to disable lint, - # if annotation_line == "lint-disable-line" - # # then we disable it. - # elseif !isnothing(annotation_line) && startswith("lint-disable-line: $(lint_rule_report.msg)", annotation_line) - # # then we disable it. - # else - # # Else we record it. - # push!(lint_rule_reports, lint_rule_report) - # end - # end - # end - # # Text rules # for (line_number, line) in enumerate(all_lines) # for rule_type in line_rules(context) diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl index 78e06de..ad51e2a 100644 --- a/src/linting/extended_checks.jl +++ b/src/linting/extended_checks.jl @@ -37,1038 +37,16 @@ struct LintContext LintContext(a, b) = new(a, b) end -################################################################################# -# UTILITY FUNCTIONS -################################################################################# -headof(x::EXPR) = x.head -valof(x::EXPR) = x.val -# kindof(t::Tokens.AbstractToken) = t.kind -parentof(x::EXPR) = x.parent -errorof(x::EXPR) = errorof(x.meta) -errorof(x) = x -haserror(m::LintMeta) = m.error !== nothing -haserror(x::EXPR) = hasmeta(x) && haserror(x.meta) -hasmeta(x::EXPR) = x.meta isa LintMeta - -function seterror!(x::EXPR, e) - if !hasmeta(x) - x.meta = LintMeta() - end - x.meta.error = e -end - -# Calling fetch_value with recursion_depth = 0 means no recursion will happen -function fetch_value(x::EXPR, tag::Symbol, should_get_value::Bool=true, recursion_depth::Int=-1, skip_head::Bool=false) - if headof(x) == tag && !skip_head - # @info x - if should_get_value - return x.val - else # return the AST - return x - end - else - isnothing(x.args) && return nothing - iszero(recursion_depth) && return nothing - for i in 1:length(x.args) - r = fetch_value(x.args[i], tag, should_get_value, recursion_depth-1, false) - isnothing(r) || return r - end - return nothing - end -end - -function fetch_values(x::EXPR, tag::Symbol, should_get_value::Bool=true) - # TODO!!! -end - -function collect_lint_report(x::EXPR, isquoted=false, errs=Tuple{Int,EXPR}[], pos=0) - if haserror(x) - push!(errs, (pos, x)) - end - - for i in 1:length(x) - collect_lint_report(x[i], isquoted, errs, pos) - pos += x[i].fullspan - end - - errs -end - -ast_rules(context::LintContext) = filter(t -> t <: ASTLintRule, context.rules_to_run) -line_rules(context::LintContext) = filter(t -> t <: LineLintRule, context.rules_to_run) - -# TODO: Need to be careful here. We actually need a linked list of markers, and not -# a dictionary. -function check_all( - x::EXPR, - markers::Dict{Symbol,String} =Dict{Symbol,String}(), - context::LintContext=LintContext() -) - # Setting up the markers - if headof(x) === :const - markers[:const] = fetch_value(x, :IDENTIFIER) - end - - if headof(x) === :function - markers[:function] = fetch_value(x, :IDENTIFIER) - end - - if headof(x) === :macro - markers[:macro] = fetch_value(x, :IDENTIFIER) - end - - if headof(x) === :macrocall - id = fetch_value(x, :IDENTIFIER) - if !isnothing(id) - markers[:macrocall] = id - end - end - - if typeof(x) == EXPR && typeof(x.head) == EXPR && headof(x.head) === :OPERATOR && x.head.val == "->" - markers[:anonymous_function] = "anonymous" - end - - if headof(x) === :do - markers[:anonymous_function] = "anonymous" - end - - # Track loop context for NotFullyParameterizedConstructorRule - if headof(x) === :for || headof(x) === :while - markers[:in_loop] = "true" - end - - for T in ast_rules(context) - check_with_process(T, x, markers) - if haserror(x) && x.meta.error isa LintRuleReport - lint_rule_report = x.meta.error - if haskey(markers, :filename) - lint_rule_report.file = markers[:filename] - end - end - end - - if x.args !== nothing - for i in 1:length(x.args) - check_all(x.args[i], markers, context) - end - end - - # Do some cleaning - headof(x) === :const && delete!(markers, :const) - headof(x) === :function && delete!(markers, :function) - headof(x) === :macrocall && delete!(markers, :macrocall) - headof(x) === :macro && delete!(markers, :macro) - typeof(x) == EXPR && - typeof(x.head) == EXPR && - headof(x.head) === :OPERATOR && - x.head.val == "->" && - delete!(markers, :anonymous_function) - headof(x) === :do && delete!(markers, :anonymous_function) - (headof(x) === :for || headof(x) === :while) && delete!(markers, :in_loop) -end - - -function is_named_hole_variable(x::CSTParser.EXPR) - return x.head == :IDENTIFIER && - startswith(x.val, "hole_variable") && - x.val != "hole_variable_star" && - length(x.val) > length("hole_variable") -end - -function is_hole_string(x::CSTParser.EXPR) - return x.head == :STRING && startswith(x.val, "LINT_STRING") -end - -function is_hole_string_with_interpolation(x::CSTParser.EXPR) - return x.head == :STRING && startswith(x.val, "LINT_STRING_WITH_INTERPOLATION") -end - -function is_hole_variable(x::CSTParser.EXPR) - return x.head == :IDENTIFIER && startswith(x.val, "hole_variable") -end - -function is_hole_variable_star(x::CSTParser.EXPR) - return x.head == :IDENTIFIER && x.val == "hole_variable_star" -end - -comp(x, y) = x == y -raw_comp(x, y, named_variable_holes) = x == y - -struct BothCannotHaveStarException <: Exception - msg::String -end - -comp_value(x, y) = x == y -function comp_value(x::String, y::String) - is_there_any_star_marker = contains(x, "QQQ") || contains(y, "QQQ") - !is_there_any_star_marker && return x == y - - contains(x, "QQQ") && contains(y, "QQQ") && - throw(BothCannotHaveStarException("Cannot both $x and $y have a star marker")) - if contains(x, "QQQ") - reg_exp = Regex(replace(x, "QQQ" => ".*")) - return !isnothing(match(reg_exp, y)) - else - reg_exp = Regex(replace(y, "QQQ" => ".*")) - return !isnothing(match(reg_exp, x)) - end -end - -function raw_comp( - x::CSTParser.EXPR, - y::CSTParser.EXPR, - named_variable_holes::Vector -) - # @info "debug:" x y - # Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) - - # If we bump into some named hole variables, then we record it. - if is_named_hole_variable(x) - push!(named_variable_holes, (x.val, y)) - end - if is_named_hole_variable(y) - push!(named_variable_holes, (y.val, x)) - end - - # If one of element to be compared is a hole, then we have a match! - (is_hole_variable(x) || is_hole_variable(y)) && return true - - if is_hole_string_with_interpolation(x) - if y.head == :string && !isnothing(y.args) - return true - else - return false - end - end - if is_hole_string_with_interpolation(y) - if x.head == :string && !isnothing(x.args) - return true - else - return false - end - end - - (is_hole_string(x) && y.head == :STRING) && return true - (is_hole_string(y) && x.head == :STRING) && return true - - - result = raw_comp(x.head, y.head, named_variable_holes) && comp_value(x.val, y.val) - !result && return false - - min_length = min(length(x), length(y)) - - for i in 1:min_length - raw_comp(x[i], y[i], named_variable_holes) || return false - (is_hole_variable_star(x[i]) || is_hole_variable_star(y[i])) && return true - end - - length(x) == length(y) && return true - - if length(x) == min_length - return is_hole_variable_star(y[min_length + 1]) - end - - if length(y) == min_length - return is_hole_variable_star(x[min_length + 1]) - end - - return false -end - -function comp(x::CSTParser.EXPR, y::CSTParser.EXPR) - named_variable_holes = Vector() - result = raw_comp(x, y, named_variable_holes) - - # If there is no or only one named variable hole, then we can exit - length(named_variable_holes) <= 1 && return result - - all_hole_names = Set(first.(named_variable_holes)) - hole_names_to_values = Dict{String, CSTParser.EXPR}() - # Else, we need to check that values under a unique named hole is the same - for k in all_hole_names - # Retrieve all the value for the named hole k - relevant = filter(tp->first(tp) == k, named_variable_holes) - relevant = map(tp->tp[2], relevant) - - # If there are more than 1 value for a given named hole k, then there is no match. - first_relevant = relevant[1] - all_others = relevant[2:end] - all(r -> comp(first_relevant, r), all_others) || return false - - hole_names_to_values[k] = first_relevant - end - - # Utility functions - remove!(a, item) = deleteat!(a, findall(x->x==item, a)) - remove(a, item) = deleteat!(copy(a), findall(x->x==item, a)) - - # At this point, we know that all the values for each named hole are the same. - # We now need to check if values for each named holes are different. - # If some values for two different named holes are the same, then there is no match - nh_values = collect(values(hole_names_to_values)) - for v in nh_values - all_to_check = remove(nh_values, v) - any(k -> comp(k, v), all_to_check) && return false - end - return true -end - -################################################################################# -# EXTENDED LINT RULES -################################################################################# abstract type LintRule end abstract type ASTLintRule <: LintRule end abstract type RecommendationLintRule <: ASTLintRule end abstract type ViolationLintRule <: ASTLintRule end abstract type FatalLintRule <: ASTLintRule end -# AST rules -struct AsyncRule <: ViolationLintRule end -struct CcallRule <: RecommendationLintRule end -struct InitializingWithFunctionRule <: ViolationLintRule end -struct FinalizerRule <: RecommendationLintRule end -struct CFunctionRule <: RecommendationLintRule end -struct UnlockRule <: RecommendationLintRule end -struct YieldRule <: RecommendationLintRule end -struct SleepRule <: RecommendationLintRule end -struct InboundsRule <: RecommendationLintRule end -struct ArrayWithNoTypeRule <: ViolationLintRule end -struct ThreadsRule <: RecommendationLintRule end -struct GeneratedRule <: FatalLintRule end -struct SyncRule <: RecommendationLintRule end -struct RemovePageRule <: ViolationLintRule end -struct TaskRule <: ViolationLintRule end -struct ErrorExceptionRule <: ViolationLintRule end -struct ErrorRule <: ViolationLintRule end -struct UnsafeRule <: ViolationLintRule end -struct InRule <: ViolationLintRule end -struct HasKeyRule <: ViolationLintRule end -struct EqualRule <: ViolationLintRule end -struct UvRule <: ViolationLintRule end -struct SplattingRule <: RecommendationLintRule end -struct UnreachableBranchRule <: ViolationLintRule end -struct StringInterpolationRule <: ViolationLintRule end -struct RelPathAPIUsageRule <: ViolationLintRule end -struct InterpolationInSafeLogRule <: RecommendationLintRule end -struct UseOfStaticThreads <: ViolationLintRule end -struct LogStatementsMustBeSafe <: FatalLintRule end -struct AssertionStatementsMustBeSafe <: FatalLintRule end -struct MustNotUseShow <: FatalLintRule end -struct NoinlineAndLiteralRule <: FatalLintRule end -struct NoReturnInAnonymousFunctionRule <: FatalLintRule end -struct NoImportRule <: ViolationLintRule end -struct NotImportingRAICodeRule <: ViolationLintRule end -struct BareUsingRule <: ViolationLintRule end -struct UntypedArrayComprehensionRule <: ViolationLintRule end -struct ReturnTypeAnnotationRule <: RecommendationLintRule end -struct StringConcatenationRule <: RecommendationLintRule end -struct NoGlobalVariablesRule <: RecommendationLintRule end -struct ConstGlobalMissingTypeRule <: ViolationLintRule end -struct IsNothingPerformanceRule <: RecommendationLintRule end -struct NotFullyParameterizedConstructorRule <: ViolationLintRule end -struct ClosureCaptureByValueRule <: RecommendationLintRule end -struct AccessingENVRule <: RecommendationLintRule end - include("text_lint_rules.jl") -const all_extended_rule_types = Ref{Vector{DataType}}( - vcat( - InteractiveUtils.subtypes(RecommendationLintRule), - InteractiveUtils.subtypes(ViolationLintRule), - InteractiveUtils.subtypes(FatalLintRule), - ) -) - -const all_text_lint_rule_types = Ref{Vector{DataType}}( - vcat( - InteractiveUtils.subtypes(LineRecommendationLintRule), - InteractiveUtils.subtypes(LineViolationLintRule), - InteractiveUtils.subtypes(LineFatalLintRule), - ) -) - function all_rules() # return vcat(all_extended_rule_types[], all_text_lint_rule_types[]) return [general["error"], general["in"], general["haskey"], fatal["unsafe-logging"], fatal["unsafe-assert"], fatal["noinline-lit-or-id"]] end - -# template -> EXPR to be compared -const check_cache = Dict{String, CSTParser.EXPR}() - -function reset_static_lint_caches() - empty!(check_cache) - all_extended_rule_types[] = vcat( - InteractiveUtils.subtypes(RecommendationLintRule), - InteractiveUtils.subtypes(ViolationLintRule), - InteractiveUtils.subtypes(FatalLintRule), - ) - - all_text_lint_rule_types[] = vcat( - InteractiveUtils.subtypes(LineRecommendationLintRule), - InteractiveUtils.subtypes(LineViolationLintRule), - InteractiveUtils.subtypes(LineFatalLintRule), - ) - return nothing -end - -function get_oracle_ast(template_code::String) - get!(()->CSTParser.parse(template_code), check_cache, template_code) - return check_cache[template_code] -end - -does_match(x::EXPR, template_code::String) = comp(x, get_oracle_ast(template_code)) - -function generic_check(t::LintRule, x::EXPR, template_code::String, error_msg::String) - generic_check(typeof(t), x, template_code, error_msg) -end - -function generic_check(T::DataType, x::EXPR, template_code::String, error_msg::String) - does_match(x, template_code) && seterror!(x, LintRuleReport(T(), error_msg)) -end - -function generic_check(t::LintRule, x::EXPR, template_code::String) - generic_check(typeof(t), x, template_code) -end - -function generic_check(T::DataType, x::EXPR, template_code::String) - keyword = first(split(template_code, ['(', '{', ' '])) - return generic_check(T, x, template_code, "`$(keyword)` should be used with extreme caution.") -end - -function check_with_process(T::DataType, x::EXPR, markers::Dict{Symbol,String}) - check(T(), x, markers) -end - -# Useful for rules that do not need markers -check(t::Any, x::EXPR, markers::Dict{Symbol,String}) = check(t, x) - -# The following function defines rules that are matched on the input Julia source code -# Each rule comes with a pattern that is checked against the abstract syntax tree -function check(t::FinalizerRule, x::EXPR) - error_msg = "`finalizer(_,_)` should not be used." - generic_check(t, x, "finalizer(hole_variable, hole_variable)", error_msg) - generic_check(t, x, "finalizer(hole_variable) do hole_variable hole_variable_star end", error_msg) -end - -function check(t::AsyncRule, x::EXPR) - msg = "Use `@spawn` instead of `@async`." - generic_check(t, x, "@async hole_variable", msg) - generic_check(t, x, "Threads.@async hole_variable", msg) -end - -check(t::CcallRule, x::EXPR) = generic_check(t, x, "ccall(hole_variable_star)", "`ccall` should be used with extreme caution.") - -function check(t::InitializingWithFunctionRule, x::EXPR, markers::Dict{Symbol,String}) - # If we are not in a const statement, then we exit this function. - haskey(markers, :const) || return - generic_check(t, x, "Threads.nthreads()", "`Threads.nthreads()` should not be used in a constant variable.") - generic_check(t, x, "is_local_deployment()", "`is_local_deployment()` should not be used in a constant variable.") - generic_check(t, x, "Deployment.is_local_deployment()", "`Deployment.is_local_deployment()` should not be used in a constant variable.") -end - -check(t::CFunctionRule, x::EXPR) = generic_check(t, x, "@cfunction(hole_variable, hole_variable_star)", "Macro `@cfunction` should not be used.") - -check(t::UnlockRule, x::EXPR) = generic_check(t, x, "unlock(hole_variable)") -check(t::YieldRule, x::EXPR) = generic_check(t, x, "yield()") -check(t::SleepRule, x::EXPR) = generic_check(t, x, "sleep(hole_variable)") - -check(t::InboundsRule, x::EXPR) = generic_check(t, x, "@inbounds hole_variable") - -function check(t::ArrayWithNoTypeRule, x::EXPR, markers::Dict{Symbol,String}) - haskey(markers, :filename) || return - contains(markers[:filename], "src/Compiler") || return - - haskey(markers, :macrocall) && markers[:macrocall] == "@match" && return - haskey(markers, :macrocall) && markers[:macrocall] == "@matchrule" && return - - generic_check(t, x, "[]", "Need a specific Array type to be provided.") -end - -function check(t::ThreadsRule, x::EXPR) - msg = "`@threads` should be used with extreme caution." - generic_check(t, x, "Threads.@threads hole_variable", msg) - generic_check(t, x, "@threads hole_variable", msg) -end - -check(t::GeneratedRule, x::EXPR) = generic_check(t, x, "@generated hole_variable") - -function check(t::SyncRule, x::EXPR) - msg = "`@sync` should be used with extreme caution." - generic_check(t, x, "@sync hole_variable", msg) - generic_check(t, x, "Threads.@sync hole_variable", msg) -end - -check(t::RemovePageRule, x::EXPR) = generic_check(t, x, "remove_page(hole_variable,hole_variable)") -check(t::TaskRule, x::EXPR) = generic_check(t, x, "Task(hole_variable)") - -function check(t::ErrorExceptionRule, x::EXPR, markers::Dict{Symbol,String}) - haskey(markers, :filename) || return - contains(markers[:filename], "test.jl") && return - contains(markers[:filename], "tests.jl") && return - contains(markers[:filename], "bench/") && return - contains(markers[:filename], "Vectorized/Test") && return - generic_check( - t, - x, - "ErrorException(hole_variable_star)", - "Use custom exception instead of the generic `ErrorException`.") -end - -function check(t::ErrorRule, x::EXPR, markers::Dict{Symbol,String}) - haskey(markers, :filename) || return - contains(markers[:filename], "test.jl") && return - contains(markers[:filename], "tests.jl") && return - contains(markers[:filename], "bench/") && return - contains(markers[:filename], "Vectorized/Test") && return - generic_check( - t, - x, - "error(hole_variable)", - "Use custom exception instead of the generic `error()`.") -end - -function check(t::UnsafeRule, x::EXPR, markers::Dict{Symbol,String}) - haskey(markers, :function) || return - isnothing(match(r"_unsafe_.*", markers[:function])) || return - isnothing(match(r"unsafe_.*", markers[:function])) || return - - generic_check( - t, - x, - "unsafe_QQQ(hole_variable_star)", - "An `unsafe_` function should be called only from an `unsafe_` function.") - generic_check( - t, - x, - "_unsafe_QQQ(hole_variable_star)", - "An `unsafe_` function should be called only from an `unsafe_` function.") -end - -function check(t::InRule, x::EXPR) - msg = "Use `tin(item,collection)` instead of the Julia's `in` or `∈`." - generic_check(t, x, "in(hole_variable,hole_variable)", msg) - generic_check(t, x, "hole_variable in hole_variable", msg) - - generic_check(t, x, "∈(hole_variable,hole_variable)", msg) - generic_check(t, x, "hole_variable ∈ hole_variable", msg) -end - -function check(t::HasKeyRule, x::EXPR) - msg = "Use `thaskey(dict,key)` instead of the Julia's `haskey`." - generic_check(t, x, "haskey(hole_variable,hole_variable)", msg) -end - -function check(t::EqualRule, x::EXPR) - msg = "Use `tequal(dict,key)` instead of the Julia's `equal`." - generic_check(t, x, "equal(hole_variable,hole_variable)", msg) -end - -function check(t::UvRule, x::EXPR) - generic_check( - t, - x, - "uv_QQQ(hole_variable_star)", - "`uv_` functions should be used with extreme caution.") -end - -function check(t::SplattingRule, x::EXPR, markers::Dict{Symbol,String}) - contains(markers[:filename], "test.jl") && return - contains(markers[:filename], "tests.jl") && return - haskey(markers, :macro) && return - - generic_check( - t, - x, - "hole_variable(hole_variable_star...)", - "Splatting (`...`) should be used with extreme caution. Splatting from dynamically sized containers could result in severe performance degradation. Splatting from statically-sized tuples is usually okay. This lint rule cannot determine if this is dynamic or static, so please check carefully. See https://github.com/RelationalAI/RAIStyle#splatting for more information.") - - generic_check( - t, - x, - "hole_variable([hole_variable(hole_variable_star) for hole_variable in hole_variable]...)", - "Splatting (`...`) should not be used with dynamically sized containers. This may result in performance degradation. See https://github.com/RelationalAI/RAIStyle#splatting for more information.") -end - -function check(t::UnreachableBranchRule, x::EXPR) - generic_check( - t, - x, - "if hole_variableA \ - hole_variable \ - elseif hole_variableA \ - hole_variable \ - end", - "Unreachable branch.") - generic_check( - t, - x, - "if hole_variableA \ - hole_variable \ - elseif hole_variable \ - hole_variable\ - elseif hole_variableA \ - hole_variable \ - end", - "Unreachable branch.") -end - -function check(t::StringInterpolationRule, x::EXPR) - # We are interested only in string with interpolation, which begins with x.head==:string - x.head == :string || return - - error_msg = raw"Use $(x) instead of $x ([explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#string-interpolation))." - # We iterate over the arguments of the CST String to check for STRING: ( - # if we find one, this means the string was incorrectly interpolated - - # The number of interpolations is the same than $ in trivia and arguments - dollars_count = length(filter(q->q.head == :OPERATOR && q.val == raw"$", x.trivia)) - - open_parent_count = length(filter(q->q.head == :LPAREN, x.trivia)) - open_parent_count != dollars_count && seterror!(x, LintRuleReport(t, error_msg)) -end - -function check(t::RelPathAPIUsageRule, x::EXPR, markers::Dict{Symbol,String}) - haskey(markers, :filename) || return - contains(markers[:filename], "src/Compiler/Front") || return - - generic_check(t, x, "hole_variable::RelPath", "Usage of type `RelPath` is not allowed in this context.") - generic_check(t, x, "RelPath(hole_variable)", "Usage of type `RelPath` is not allowed in this context.") - generic_check(t, x, "RelPath(hole_variable, hole_variable)", "Usage of type `RelPath` is not allowed in this context.") - generic_check(t, x, "split_path(hole_variable)", "Usage of `RelPath` API method `split_path` is not allowed in this context.") - generic_check(t, x, "drop_first(hole_variable)", "Usage of `RelPath` API method `drop_first` is not allowed in this context.") - generic_check(t, x, "relpath_from_signature(hole_variable)", "Usage of method `relpath_from_signature` is not allowed in this context.") -end - -function check(t::InterpolationInSafeLogRule, x::EXPR) - generic_check(t, x, "@warnv_safe_to_log hole_variable \"LINT_STRING_WITH_INTERPOLATION\"", "Safe warning log has interpolation.") -end - -function check(t::UseOfStaticThreads, x::EXPR) - msg = "Use `Threads.@threads :dynamic` instead of `Threads.@threads :static`. Static threads must not be used as generated tasks will not be able to migrate across threads." - generic_check(t, x, "@threads :static hole_variable_star", msg) - generic_check(t, x, "Threads.@threads :static hole_variable_star", msg) -end - -function all_arguments_are_safe(x::EXPR; skip_first_arg::Bool=false) - - is_safe_macro_call(y) = - (y.head == :macrocall && y.args[1].head == :IDENTIFIER && y.args[1].val == "@safe") || - (y.head == :macrocall && - y.args[1].head isa EXPR && - y.args[1].head.head == :OPERATOR && - y.args[1].args[1].args[1].val == "SafeLogging" && - y.args[1].args[2].args[1].val == "@safe") - - is_safe_literal(x) = x.head in [:NOTHING, - :INTEGER, - :FLOAT, - :TRUE, - :FALSE, - :HEXINT, - :BININT, - :CHAR, - :OCTINT - ] - - first_index = skip_first_arg ? 4 : 2 - for arg in x.args[first_index:end] - # This is safe - if is_safe_macro_call(arg) || is_safe_literal(arg) - continue - elseif arg.head isa CSTParser.EXPR && arg.head.head == :OPERATOR && arg.head.val == "=" && - (is_safe_macro_call(arg.args[2]) || is_safe_literal(arg.args[2])) - continue - else - # @info x arg - # isdefined(Main, :Infiltrator) && Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) - return false - end - end - return true -end - -function check(t::LogStatementsMustBeSafe, x::EXPR, markers::Dict{Symbol,String}) - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "bench/") && return - end - - error_msg = "Unsafe logging statement. You must enclose variables and strings with `@safe(...)`." - - # @info and its friends - if x.head == :macrocall && x.args[1].head == :IDENTIFIER && startswith(x.args[1].val, "@info") - all_arguments_are_safe(x) || seterror!(x, LintRuleReport(t, error_msg)) - end - - # @debug and its friends - if x.head == :macrocall && x.args[1].head == :IDENTIFIER && startswith(x.args[1].val, "@debug") - all_arguments_are_safe(x) || seterror!(x, LintRuleReport(t, error_msg)) - end - - # @error and its friends - if x.head == :macrocall && x.args[1].head == :IDENTIFIER && startswith(x.args[1].val, "@error") - all_arguments_are_safe(x) || seterror!(x, LintRuleReport(t, error_msg)) - end - - # @warn and its friends - if x.head == :macrocall && x.args[1].head == :IDENTIFIER && startswith(x.args[1].val, "@warn") - all_arguments_are_safe(x) || seterror!(x, LintRuleReport(t, error_msg)) - end -end - -function check(t::AssertionStatementsMustBeSafe, x::EXPR, markers::Dict{Symbol,String}) - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - end - - error_msg = "Unsafe assertion statement. You must enclose the message `@safe(...)`." - - # @assert and its friends - if x.head == :macrocall && - x.args[1].head == :IDENTIFIER && - (startswith(x.args[1].val, "@assert") || startswith(x.args[1].val, "@dassert")) - - all_arguments_are_safe(x; skip_first_arg=true) || seterror!(x, LintRuleReport(t, error_msg)) - end -end - -function check(t::MustNotUseShow, x::EXPR) - msg = "Do not use `@show`, use `@info` instead." - generic_check(t, x, "@show hole_variable", msg) -end - - -function all_arguments_are_literal_or_identifier(x::EXPR) - is_literal(x) = x.head in [:NOTHING, - :INTEGER, - :FLOAT, - :TRUE, - :FALSE, - :HEXINT, - :BININT, - :CHAR, - :OCTINT, - :STRING - ] - is_identifier(x) = x.head == :IDENTIFIER - is_splatting(x) = x.head isa EXPR && x.head.head == :OPERATOR && x.head.val == "..." - is_literal_or_identifier_or_splatting(x) = - if x.head == :parameters || x.head == :kw - all(is_literal_or_identifier_or_splatting, x.args) - else - is_literal(x) || is_identifier(x) || is_splatting(x) - end - - return all(is_literal_or_identifier_or_splatting, x.args[2:end]) -end - -function check(t::NoinlineAndLiteralRule, x::EXPR) - if does_match(x, "@noinline hole_variable(hole_variable_star) = hole_variable_star") - return - end - - if x.head == :macrocall && - x.args[1].head == :IDENTIFIER && - x.args[1].val == "@noinline" - - # Are we in a function definition? - function_def = fetch_value(x, :function, false) - isnothing(function_def) || return - - # Retrieve function call below the @noinline macro - fct_call = fetch_value(x, :call, false, 1) - - msg = "For call-site `@noinline` call, all args must be literals or identifiers only. \ - Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086)." - - # We found no function call, check for a macro call then - if isnothing(fct_call) - macro_call = fetch_value(x, :macrocall, false, -1, true) - - # If we have not found a macro call, then we merely exit. - # could happen with `@noinline 42` for example - isnothing(macro_call) && return - - # We found a macro call - seterror!(x, LintRuleReport(t, msg)) - else - # We found a function call, check if all arguments are literals or identifiers - all_arguments_are_literal_or_identifier(fct_call) || seterror!(x, LintRuleReport(t, msg)) - end - end -end - -function check(t::NoReturnInAnonymousFunctionRule, x::EXPR, markers::Dict{Symbol,String}) - # No need to check for returns in anonymous function in tests - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - end - - haskey(markers, :anonymous_function) || return - msg = "Anonymous function must not have `return` [Explanation](https://github.com/RelationalAI/RAIStyle#returning-from-a-closure)." - generic_check(t, x, "return hole_variable", msg) -end - -function check(t::NoImportRule, x::EXPR, markers::Dict{Symbol,String}) - msg = "Imports must be specified using `using` and not `import` [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#module-imports)." - generic_check(t, x, "import hole_variable", msg) - - # Arbitrary number of hole variables - # TODO: This is hacky and it deserves a better solution. - for i in 1:15 - s = join(["hole_variable" for _ in 1:i], ", ") - u = "import hole_variable : $(s)" - generic_check(t, x, u, msg) - end -end - -function check(t::NotImportingRAICodeRule, x::EXPR, markers::Dict{Symbol,String}) - msg = "Importing RAICode should be avoided (when possible)." - generic_check(t, x, "using RAICode", msg) - - # Arbitrary number of hole variables - # TODO: This is hacky and it deserves a better solution. - for i in 1:15 - s = join(["hole_variable" for _ in 1:i], ", ") - u = "using RAICode : $(s)" - generic_check(t, x, u, msg) - end -end - -function check(t::BareUsingRule, x::EXPR, markers::Dict{Symbol,String}) - # Skip test files - bare using is allowed in tests - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "test.jl") && return - end - - msg = "Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`. [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#module-imports)." - generic_check(t, x, "using hole_variable", msg) -end - -function check(t::UntypedArrayComprehensionRule, x::EXPR, markers::Dict{Symbol,String}) - # Only check in Compiler directory - this is where type stability matters most - if haskey(markers, :filename) - !contains(markers[:filename], "src/Compiler/") && return - end - - msg = "Need a specific Array type to be provided. Use `T[x for x in xs]` instead of `[x for x in xs]`. [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations)." - - # Pattern: [comprehension] without type prefix - # This catches [x for x in xs] but not T[x for x in xs] - generic_check(t, x, "[hole_variable for hole_variable in hole_variable]", msg) -end - -function check(t::ReturnTypeAnnotationRule, x::EXPR, markers::Dict{Symbol,String}) - # Skip test files - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "test.jl") && return - end - - if haskey(markers, :macrocall) - # return type annotation are mandatory in @derived functions - contains(markers[:macrocall], "derived") && return - end - - msg = "Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations)." - - # Pattern: function name()::Type ... end (multiline) - generic_check(t, x, "function hole_variable(hole_variable_star)::hole_variable hole_variable_star end", msg) - - # Pattern: name()::Type = ... (one-liner) - generic_check(t, x, "hole_variable(hole_variable_star)::hole_variable = hole_variable", msg) -end - -function check(t::StringConcatenationRule, x::EXPR, markers::Dict{Symbol,String}) - # Skip test files - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "test.jl") && return - end - - msg = "Prefer string interpolation or `string()` over `*` for string concatenation. Use `\"\$(a)\$(b)\"` instead of `a * b`. [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#strings)." - - # Pattern: "string" * something or something * "string" - generic_check(t, x, "\"LINT_STRING\" * hole_variable", msg) - generic_check(t, x, "hole_variable * \"LINT_STRING\"", msg) -end - -function check(t::NoGlobalVariablesRule, x::EXPR, markers::Dict{Symbol,String}) - # Skip test files - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "test.jl") && return - end - - msg = "Avoid non-const global variables. Use `const` for immutable globals or pass values as function arguments. [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#global-variables)." - - # Pattern: global variable assignment without const - # Matches: x = value, global x = value - generic_check(t, x, "global hole_variable = hole_variable", msg) -end - -function check(t::ConstGlobalMissingTypeRule, x::EXPR, markers::Dict{Symbol,String}) - # Skip test files - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "test.jl") && return - end - - # Detect global assignments - # AST structure from debug: global keyword followed by assignment - # global untyped = val: x[1] = GLOBAL, x[2] = (= IDENTIFIER val) - # global typed::T = val: x[1] = GLOBAL, x[2] = (= (:: IDENTIFIER T) val) - if headof(x) === :global && length(x) >= 2 - # Skip the GLOBAL keyword (x[1]), look at the assignment (x[2]) - assignment = x[2] - - # Check if this is an assignment (headof contains "=") - head_str = string(headof(assignment)) - if contains(head_str, "=") && length(assignment) >= 1 - lhs = assignment[1] # Left-hand side of assignment - - # Check if lhs is just an identifier (no type annotation) - # If headof(lhs) === :IDENTIFIER, it's untyped - # If headof(lhs) contains "::", it's typed - if headof(lhs) === :IDENTIFIER - # This is an untyped global like: global x = value - msg = "Global variable must have type annotation: `global x::Type = value`. Use `const` for immutable globals. [Explanation](https://github.com/RelationalAI/RAIStyle#global-variables)" - seterror!(x, LintRuleReport(t, msg)) - end - # If headof(lhs) contains "::", it's typed - OK, don't error - end - end -end - -function check(t::IsNothingPerformanceRule, x::EXPR, markers::Dict{Symbol,String}) - # Only check in performance-critical directories - if haskey(markers, :filename) - performance_critical = contains(markers[:filename], "src/Compiler/") || - contains(markers[:filename], "packages/Salsa/") - !performance_critical && return - else - return - end - - # Skip test files - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "test.jl") && return - end - - msg = "In performance-critical code, prefer `x === nothing` or `x isa Nothing` over `isnothing(x)`. [Explanation](https://github.com/RelationalAI/RAIStyle#isnothing-vs-isa-nothing-vs--nothing)" - - generic_check(t, x, "isnothing(hole_variable)", msg) -end - -function check(t::NotFullyParameterizedConstructorRule, x::EXPR, markers::Dict{Symbol,String}) - # Only check in performance-critical code - if haskey(markers, :filename) - !contains(markers[:filename], "src/Compiler/") && return - else - return - end - - # Skip test files - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "test.jl") && return - end - - # Check if we're in a loop - if !haskey(markers, :in_loop) - return - end - - # Detect constructor calls (capitalized function names) - if headof(x) === :call - # Get the function being called - func_expr = x[1] - - # Extract function name from different patterns - func_name = nothing - if headof(func_expr) === :IDENTIFIER - func_name = valof(func_expr) - elseif headof(func_expr) === :curly - # Fully parameterized like Vector{Int}(...) - this is OK - return - end - - # If we have a function name and it starts with capital letter (constructor pattern) - if !isnothing(func_name) && occursin(r"^[A-Z]", func_name) - # This is likely a not-fully-parameterized constructor call in a loop - msg = "Avoid not-fully-parameterized constructor `$(func_name)(...)` in loops. Use a maker function instead for better performance. [Explanation](https://github.com/RelationalAI/RAIStyle#super-costly-dynamic-dispatches)" - seterror!(x, LintRuleReport(t, msg)) - end - end -end - -function check(t::ClosureCaptureByValueRule, x::EXPR, markers::Dict{Symbol,String}) - # Only check in performance-critical directories - if haskey(markers, :filename) - !contains(markers[:filename], "src/Compiler/") && return - else - return - end - - # Skip test files - if haskey(markers, :filename) - contains(markers[:filename], "test/") && return - contains(markers[:filename], "test.jl") && return - end - - # Detect nested function definitions (closures) - # We want to find function definitions that occur INSIDE other functions - # Pattern 1: Lambda function: f = x -> ... - # Pattern 2: Short-form function: f() = ... - # Pattern 3: Anonymous functions are marked separately - # Note: Full function definitions (function f() end) are problematic because - # the :function marker is set BEFORE checking, so we can't distinguish top-level - # from nested just by checking the marker. Skip those for now. - - is_nested_function = false - - # Only check if we're inside a function - if !haskey(markers, :function) - return - end - - if headof(x) === :(=) - # Check for anonymous function assignment: f = x -> ... - # or short form function: f() = ... - if length(x) >= 2 - rhs = x[2] - if headof(rhs) === :(->) - # Lambda function - definitely a closure - is_nested_function = true - elseif headof(x[1]) === :call - # Short form function definition - definitely nested - is_nested_function = true - end - end - elseif haskey(markers, :anonymous_function) - # Anonymous functions (do blocks, etc.) - these are closures - is_nested_function = true - end - - if is_nested_function - # We found a nested function - recommend considering let-binding - # Note: This is a recommendation, not an error, as capture-by-reference is sometimes correct - msg = "Nested function may capture variables by reference, causing boxing and type instability. Consider using `let x = x` to capture by value for better performance. [Explanation](https://github.com/RelationalAI/RAIStyle#closure-capture-performance)" - seterror!(x, LintRuleReport(t, msg)) - end -end - - -function check(t::AccessingENVRule, x::EXPR) - msg = "Mutating `ENV` is not thread-safe, only do so in a single-threaded context. [URL](https://relationalai.slack.com/archives/C0AJLPBD273/p1772635179172879)" - - generic_check(t, x, "ENV[hole_variable] = hole_variable", msg) - generic_check(t, x, "pop!(ENV, hole_variable)", msg) - generic_check(t, x, "delete!(ENV, hole_variable)", msg) - generic_check(t, x, "setindex!(ENV, hole_variable, hole_variable)", msg) - generic_check(t, x, "get!(ENV, hole_variable, hole_variable)", msg) - generic_check(t, x, "push!(ENV, hole_variable)", msg) -end diff --git a/test/common.jl b/test/common.jl index cf21775..f12d82f 100644 --- a/test/common.jl +++ b/test/common.jl @@ -1,15 +1,10 @@ -using ReLint: ReLint, run_lint_on_text, comp, convert_offset_to_line, - convert_offset_to_line_from_lines, MarkdownFormat, PlainFormat, - fetch_value, has_values +using ReLint: ReLint, run_lint_on_text, convert_offset_to_line, + convert_offset_to_line_from_lines, MarkdownFormat, PlainFormat using ReLint: LintResult, LintContext -import CSTParser using Test using JSON3 -# Reset the caches before running the tests. -ReLint.reset_static_lint_caches() - function lint_test(source::String, expected_substring::String; verbose=true, directory::String = "", context::LintContext=LintContext()) io = IOBuffer() run_lint_on_text(source; io, directory, context) diff --git a/test/runtests.jl b/test/runtests.jl index c668999..69d0b26 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ using ReLint -using CSTParser, Test -using ReLint: convert_offset_to_line_from_lines, check_all +using Test +using ReLint: convert_offset_to_line_from_lines include(joinpath(@__DIR__, "common.jl")) From 718f882dd5908b15ee532a5db33444981d51df6c Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 6 Mar 2026 14:15:32 +0100 Subject: [PATCH 08/62] Reorganise rules and rule groups --- src/interface.jl | 6 ++-- src/linting/argus-rules.jl | 60 ++++++++++++++++++++-------------- src/linting/extended_checks.jl | 7 ++-- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 7f3ded4..2874c0c 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -26,9 +26,9 @@ Argus_result_to_LintRuleReport(rule::Rule, bindings::BindingSet) = false, 0) -is_recommendation(r::Rule) = haskey(recommendation, r.name) -is_violation(r::Rule) = haskey(violation, r.name) -is_fatal(r::Rule) = haskey(fatal, r.name) +is_recommendation(r::Rule) = haskey(RECOMMENDATION_RULE_GROUP, r.name) +is_violation(r::Rule) = haskey(VIOLATION_RULE_GROUP, r.name) +is_fatal(r::Rule) = haskey(FATAL_RULE_GROUP, r.name) is_recommendation(r::LintRuleReport) = is_recommendation(r.rule) is_violation(r::LintRuleReport) = is_violation(r.rule) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 91a3545..44cbd52 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -1,11 +1,30 @@ using Argus -general = RuleGroup("general") -recommendation = RuleGroup("recommendation") -violation = RuleGroup("violation") -fatal = RuleGroup("fatal") +# Recommendation rules +# ==================== -@define_rule_in_group general "error" begin +RECOMMENDATION_RULE_GROUP = RuleGroup("recommendation") + +# ReturnTypeAnnotationRule +@define_rule_in_group RECOMMENDATION_RULE_GROUP "return-type-annotation" begin + description = """ + Avoid return type annotations `function foo()::Type`. + Return type annotations can hurt performance by forcing type conversions. + [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations). + """ + + pattern = @pattern ~or( + function ({f}({args}...)::{ret_type}) {_}... end, + {_:::funcall}::{ret_type} = {_} + ) +end + +# Violation rules +# =============== + +VIOLATION_RULE_GROUP = RuleGroup("violation") + +@define_rule_in_group VIOLATION_RULE_GROUP "error" begin description = """ Use custom exception instead of the generic `error()`. """ @@ -13,7 +32,7 @@ fatal = RuleGroup("fatal") pattern = @pattern error({_}...) end -@define_rule_in_group general "in" begin +@define_rule_in_group VIOLATION_RULE_GROUP "in" begin description = """ Use `tin(item,collection)` instead of the Julia's `in` or `∈`. """ @@ -24,7 +43,7 @@ end ) end -@define_rule_in_group general "haskey" begin +@define_rule_in_group VIOLATION_RULE_GROUP "haskey" begin description = """ Use `thaskey(dict,key)` instead of the Julia's `haskey`. """ @@ -34,7 +53,7 @@ end # StringInterpolationRule # TODO: `trivia` flag. -# @define_rule_in_group general "string-interpolation" begin +# @define_rule_in_group VIOLATION_RULE_GROUP "string-interpolation" begin # description = raw""" # Use $(x) instead of $x ([explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#string-interpolation)). # """ @@ -42,22 +61,13 @@ end # pattern = @pattern ... # end -# ReturnTypeAnnotationRule -@define_rule_in_group general "return-type-annotation" begin - description = """ - Avoid return type annotations `function foo()::Type`. - Return type annotations can hurt performance by forcing type conversions. - [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations). - """ +# Fatal rules +# =========== - pattern = @pattern ~or( - function ({f}({args}...)::{ret_type}) {_}... end, - {_:::funcall}::{ret_type} = {_} - ) -end +FATAL_RULE_GROUP = RuleGroup("fatal") # GeneratedRule -@define_rule_in_group fatal "generated" begin +@define_rule_in_group FATAL_RULE_GROUP "generated" begin description = """ Don't use `@generated` """ @@ -102,7 +112,7 @@ end @pattern {_} = {arg:::safe_macro_arg} end # TODO: Rewrite with `not` pattern? -@define_rule_in_group fatal "unsafe-logging" begin +@define_rule_in_group FATAL_RULE_GROUP "unsafe-logging" begin description = """ Unsafe logging statement. You must enclose variables and strings with `@safe(...)`. """ @@ -131,7 +141,7 @@ end end "" end end -@define_rule_in_group fatal "unsafe-assert" begin +@define_rule_in_group FATAL_RULE_GROUP "unsafe-assert" begin description = """ Unsafe assertion statement. You must enclose the message with `@safe(...)`. """ @@ -156,7 +166,7 @@ end # MustNotUseShow # # TODO: Template. -@define_rule_in_group fatal "show" begin +@define_rule_in_group FATAL_RULE_GROUP "show" begin description = """ Do not use `@show`, use `@info` instead. """ @@ -258,7 +268,7 @@ end end # TODO: Support for allowing match if the source and pattern differ only by a flag # (e.g. INFIX_FLAG). -@define_rule_in_group fatal "noinline-lit-or-id" begin +@define_rule_in_group FATAL_RULE_GROUP "noinline-lit-or-id" begin description = """ For call-site `@noinline` call, all args must be literals or identifiers only. \ Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086). diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl index ad51e2a..03c51c2 100644 --- a/src/linting/extended_checks.jl +++ b/src/linting/extended_checks.jl @@ -32,7 +32,6 @@ struct LintContext end LintContext(s::Vector{Rule}) = new(s, []) - # LintContext(s::Vector{Any}) = new(convert(Vector{DataType}, s) , []) LintContext() = new(all_rules(), []) LintContext(a, b) = new(a, b) end @@ -46,7 +45,7 @@ abstract type FatalLintRule <: ASTLintRule end include("text_lint_rules.jl") function all_rules() - # return vcat(all_extended_rule_types[], all_text_lint_rule_types[]) - return [general["error"], general["in"], general["haskey"], - fatal["unsafe-logging"], fatal["unsafe-assert"], fatal["noinline-lit-or-id"]] + return [values(RECOMMENDATION_RULE_GROUP)..., + values(VIOLATION_RULE_GROUP)..., + values(FATAL_RULE_GROUP)...] end From 59c98443cfc432ada4fcd4780c1b38de452b8299 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 6 Mar 2026 14:21:43 +0100 Subject: [PATCH 09/62] Add file with benchmarks on raicode/src --- benchmark/raicode-benchmarks.jl | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 benchmark/raicode-benchmarks.jl diff --git a/benchmark/raicode-benchmarks.jl b/benchmark/raicode-benchmarks.jl new file mode 100644 index 0000000..f5d23ba --- /dev/null +++ b/benchmark/raicode-benchmarks.jl @@ -0,0 +1,59 @@ +using BenchmarkTools +import Git + +const git = Git.git() + +const RAICODE_DIR = if !isnothing(ARGS[1]) && isdir(ARGS[1]) + joinpath(ARGS[1], "src") +else + const TMP_DIR = mktempdir() + # Clone `raicode`. + run(pipeline(`$git clone https://github.com/RelationalAI/raicode.git $TMP_DIR`, + stdout=devnull)) + + joinpath(TMP_DIR, "raicode/src") +end + +function run_benchmarks(io=stdout) + # Setup worktree main branch. + run(pipeline(`$git worktree add ../main-with-modified-tests main-with-modified-tests`, + stdout=devnull)) + # Run benchmarks. + b_main = try + benchmark_branch("../main-with-modified-tests") + catch err + run(`$git worktree remove ../main-with-modified-tests`) + rethrow(err) + end + b_argus = benchmark_branch() + # Print results. + println(io, "Benchmarks on `main`:") + println(io, b_main) + println(io) + println(io, "Benchmarks on `iulia/argus`:") + println(io, b_argus) + println(io) + println(io, "Comparison (`iulia/argus` vs `main`):") + println(io, judge(median(b_argus), median(b_main))) + # Clean up. + run(`$git worktree remove ../main-with-modified-tests`) + + return nothing +end + +function benchmark_branch(branch_path::String=pwd()) + # Save current directory so we can get back to it. + current_dir = pwd() + # Switch to `branch_path` and run the benchmark there. + cd(branch_path) + @eval using ReLint + b = redirect_stdout(devnull) do + @benchmark ReLint.run_lint(RAICODE_DIR) + end + # Return to the original directory. + cd(current_dir) + + return b +end + +run_benchmarks() From 450b810be0ebd97167175fbbc159529fe74dcdec Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 10 Mar 2026 13:07:20 +0100 Subject: [PATCH 10/62] Move tests for fatal violations to dedicated file --- test/fatal.jl | 197 +++++++++++++++++++++++++++++++++++++ test/noinline_tests.jl | 67 ------------- test/runtests.jl | 4 +- test/safe_logging_tests.jl | 98 ------------------ 4 files changed, 199 insertions(+), 167 deletions(-) create mode 100644 test/fatal.jl delete mode 100644 test/noinline_tests.jl delete mode 100644 test/safe_logging_tests.jl diff --git a/test/fatal.jl b/test/fatal.jl new file mode 100644 index 0000000..6b1dab0 --- /dev/null +++ b/test/fatal.jl @@ -0,0 +1,197 @@ +@testset "Fatal violations" begin + + @testset "`@generated`" begin + source = """ + @generated function _empty_vector(::Type{T}) where {T} + vec = T[] + return vec + end + + let x = y + @generated f(z) = z + f(x) + end + """ + + @test lint_has_error_test(source) + @test lint_test(source, + "Line 1, column 1: `@generated` should be used with extreme caution.") + @test lint_test(source, + "Line 7, column 5: `@generated` should be used with extreme caution.") + end + + @testset "Unsafe logging" begin + let + source = raw""" + function f() + @info "Unsafe logging $(x)" + @info @safe("Unsafe logging") job + @info @safe("Unsafe logging") my_value=job + @info @safe("Unsafe logging") my_value=@safe(job) my_value2=job + @info @safe("Unsafe logging") my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) "$(x)" + @info @safe("Unsafe logging") my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) "$(x)" + @debug_connection @safe("Unsafe logging") my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) "$(x)" + @warn_with_current_exceptions_safe_to_log @safe("Unsafe logging") my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) "$(x)" + @info "Unsafe logging" + @info "Unsafe logging" my_value=@safe(job) + @info "Unsafe logging" my_value=@safe(job) my_value=@safe(job2) + @info "Unsafe logging" my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) + + @info @safe("Safe logging $(x)") + @info @safe("Safe logging") + + @warnv 1 @safe("Safe logging") + + @warnv 1 @safe("Safe logging with non-common literals") 0x12 'c' 0b0 0o0 + + @infov 1 @safe( + "[Compilation] \ + Creating a new BeTreeV2 specialization: $(K) and $(V) where eps = $(E) \n\ + List of all encountered types so far \ + (total: $(length(UNIQUE_BETREE_TYPES))): \n\ + $(total_report)" + ) total = @safe(length(UNIQUE_BETREE_TYPES)) + + @warnv( + 0, + @safe("Precompiling: parse error: $(e)"), + precompile_statement=@safe(repr(statement)), + # Log the message that the exception would print, else JSONLogger logs each of + # the fields of the exception separately which is much less useful. + exception=@safe(sprint(showerror, e)), + maxlog=100, + ) + end + """ + @test count_lint_errors(source) == 12 + for line in 2:count_lint_errors(source) + 1 + @test lint_test(source, "Line $(line), column 5: Unsafe logging statement. You must enclose variables and strings with `@safe(...)`.") + end + end + let + source = raw""" + macro foo() + @info @safe("error_msg") + end + + macro foo() + @info $SafeLogging.@safe("error_msg") + end + """ + @test count_lint_errors(source) == 0 + end + end + + @testset "Unsafe assertion" begin + let + source = raw""" + function f() + @assert 1 ==1 "this is an unsafe assertion" + @assert 1 ==1 @safe("this is a safe assertion") + @dassert3 1 == 1 # Okay + @dassert3 1 == 1 "not okay" + @dassert3 1 == 1 @safe("Okay") + end + """ + @test count_lint_errors(source) == 2 + end + let + source = raw""" + macro spawn_periodic_task(name, period, expr, ending_expr=nothing) + error_msg = "Periodic task name must be `@safe(\\\"name\\\")`, so it can be logged: $(__source__)" + @dassert0 is_safe_expr_literal(name) @safe(error_msg) + + return quote + n = $(esc(name)) + p = $(esc(period)) + # And therefore n is safe to log. + @dassert0 n isa $SafeLoggable $SafeLogging.@safe($(error_msg)) + n = string(n) + end + end + """ + @test count_lint_errors(source) == 0 + end + end + + @testset "`@show`" begin + source = """ + function f() + @show "foo" + @show 1 + 1 + end + """ + @test count_lint_errors(source) == 2 + @test lint_test(source, + "Line 2, column 5: Do not use `@show`, use `@info` instead.") + @test lint_test(source, + "Line 3, column 5: Do not use `@show`, use `@info` instead.") + end + + @testset "`@noinline` with non-literal/identifiers" begin + source = raw""" + function with_errors() + @noinline g(x.a) + @noinline g("$(x)") + @noinline h(g(x)) + @noinline i(1 + 2) + @noinline h(x) + 1 + @noinline @eval 1 + 1 + @noinline g(x[1]) + @noinline g(x[:a]) + @noinline (1:10) .+ 4 + @noinline get_page(pager, magic, pid; is_prefetch=check()) + @noinline get_page(pager, magic, pid; is_prefetch=1 + 2) + end + + function without_errors() + @noinline g(x) + @noinline g(1, 2, 3, "abc", 'a') + @noinline 1 + 1 + @noinline x[1] + @noinline get_page(pager, magic, pid; is_prefetch=true) + @noinline get_page(pager, magic, pid; is_prefetch=false) + @noinline get_page(pager, magic, pid; is_prefetch) + + # lint-disable-next-line: Splatting (`...`) + @noinline foo(x...) + # lint-disable-next-line: Splatting (`...`) + @noinline foo(x, y...) + # lint-disable-next-line: Splatting (`...`) + @noinline foo(x; kws...) + end + + @noinline function no_error(x::Int=y.a) + (@noinline dv_isgreater(seek_key, upper_bounds)) && return false, i + return 42 + end + + @noinline foo() = 42 + @noinline bar(::Integer) = @assert false @safe("unreachable") + @noinline _throw_empty_weight_list_exception() = + throw(QueryEvaluatorInternalException(@safe(" + Adjacency matrix materialization within a graph primitive assumes a \ + non-empty weight list. For this exception to fire, some inconsistency must \ + have existed between the weight list and node and/or edge counts passed \ + to a graph primitive." + ))) + + @noinline get_page(pager, magic, pid; is_prefetch) + + """ + + @test count_lint_errors(source) == 11 + @test lint_test(source, "Line 2, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 3, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 4, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 5, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 6, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 7, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 8, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 9, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 10, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 11, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + @test lint_test(source, "Line 12, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") + end + +end diff --git a/test/noinline_tests.jl b/test/noinline_tests.jl deleted file mode 100644 index f7b5376..0000000 --- a/test/noinline_tests.jl +++ /dev/null @@ -1,67 +0,0 @@ -@testset "Noinline" begin - using ReLint - - source = raw""" - function with_errors() - @noinline g(x.a) - @noinline g("$(x)") - @noinline h(g(x)) - @noinline i(1 + 2) - @noinline h(x) + 1 - @noinline @eval 1 + 1 - @noinline g(x[1]) - @noinline g(x[:a]) - @noinline (1:10) .+ 4 - @noinline get_page(pager, magic, pid; is_prefetch=check()) - @noinline get_page(pager, magic, pid; is_prefetch=1 + 2) - end - - function without_errors() - @noinline g(x) - @noinline g(1, 2, 3, "abc", 'a') - @noinline 1 + 1 - @noinline x[1] - @noinline get_page(pager, magic, pid; is_prefetch=true) - @noinline get_page(pager, magic, pid; is_prefetch=false) - @noinline get_page(pager, magic, pid; is_prefetch) - - # lint-disable-next-line: Splatting (`...`) - @noinline foo(x...) - # lint-disable-next-line: Splatting (`...`) - @noinline foo(x, y...) - # lint-disable-next-line: Splatting (`...`) - @noinline foo(x; kws...) - end - - @noinline function no_error(x::Int=y.a) - (@noinline dv_isgreater(seek_key, upper_bounds)) && return false, i - return 42 - end - - @noinline foo() = 42 - @noinline bar(::Integer) = @assert false @safe("unreachable") - @noinline _throw_empty_weight_list_exception() = - throw(QueryEvaluatorInternalException(@safe(" - Adjacency matrix materialization within a graph primitive assumes a \ - non-empty weight list. For this exception to fire, some inconsistency must \ - have existed between the weight list and node and/or edge counts passed \ - to a graph primitive." - ))) - - @noinline get_page(pager, magic, pid; is_prefetch) - - """ - - @test count_lint_errors(source) == 11 - @test lint_test(source, "Line 2, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 3, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 4, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 5, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 6, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 7, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 8, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 9, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 10, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 11, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") - @test lint_test(source, "Line 12, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") -end diff --git a/test/runtests.jl b/test/runtests.jl index 69d0b26..43ba72e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,8 +6,8 @@ include(joinpath(@__DIR__, "common.jl")) # include(joinpath(@__DIR__, "line_rules_tests.jl")) # include(joinpath(@__DIR__, "file_exclusion_tests.jl")) -include(joinpath(@__DIR__, "noinline_tests.jl")) -include(joinpath(@__DIR__, "safe_logging_tests.jl")) +include(joinpath(@__DIR__, "fatal.jl")) + # include(joinpath(@__DIR__, "lint_context_tests.jl")) # include(joinpath(@__DIR__, "rai_rules_tests.jl")) # include(joinpath(@__DIR__, "test_new_rules.jl")) diff --git a/test/safe_logging_tests.jl b/test/safe_logging_tests.jl deleted file mode 100644 index 7ec5c6d..0000000 --- a/test/safe_logging_tests.jl +++ /dev/null @@ -1,98 +0,0 @@ -@testset "Safe logging" begin - @testset "Safe and unsafe logs" begin - source = raw""" - function f() - @info "Unsafe logging $(x)" - @info @safe("Unsafe logging") job - @info @safe("Unsafe logging") my_value=job - @info @safe("Unsafe logging") my_value=@safe(job) my_value2=job - @info @safe("Unsafe logging") my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) "$(x)" - @info @safe("Unsafe logging") my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) "$(x)" - @debug_connection @safe("Unsafe logging") my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) "$(x)" - @warn_with_current_exceptions_safe_to_log @safe("Unsafe logging") my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) "$(x)" - @info "Unsafe logging" - @info "Unsafe logging" my_value=@safe(job) - @info "Unsafe logging" my_value=@safe(job) my_value=@safe(job2) - @info "Unsafe logging" my_value=@safe(job) my_value=@safe(job2) my_value=@safe(job3) - - @info @safe("Safe logging $(x)") - @info @safe("Safe logging") - - @warnv 1 @safe("Safe logging") - - @warnv 1 @safe("Safe logging with non-common literals") 0x12 'c' 0b0 0o0 - - @infov 1 @safe( - "[Compilation] \ - Creating a new BeTreeV2 specialization: $(K) and $(V) where eps = $(E) \n\ - List of all encountered types so far \ - (total: $(length(UNIQUE_BETREE_TYPES))): \n\ - $(total_report)" - ) total = @safe(length(UNIQUE_BETREE_TYPES)) - - @warnv( - 0, - @safe("Precompiling: parse error: $(e)"), - precompile_statement=@safe(repr(statement)), - # Log the message that the exception would print, else JSONLogger logs each of - # the fields of the exception separately which is much less useful. - exception=@safe(sprint(showerror, e)), - maxlog=100, - ) - end - """ - @test count_lint_errors(source) == 12 - for line in 2:count_lint_errors(source) + 1 - @test lint_test(source, "Line $(line), column 5: Unsafe logging statement. You must enclose variables and strings with `@safe(...)`.") - end - end - - @testset "Safe and unsafe assertions" begin - source = raw""" - function f() - @assert 1 ==1 "this is an unsafe assertion" - @assert 1 ==1 @safe("this is a safe assertion") - @dassert3 1 == 1 # Okay - @dassert3 1 == 1 "not okay" - @dassert3 1 == 1 @safe("Okay") - end - """ - @test count_lint_errors(source) == 2 - end - - @testset "@safe in a macro 1" begin - source = raw""" - macro spawn_periodic_task(name, period, expr, ending_expr=nothing) - error_msg = "Periodic task name must be `@safe(\\\"name\\\")`, so it can be logged: $(__source__)" - @dassert0 is_safe_expr_literal(name) @safe(error_msg) - - return quote - n = $(esc(name)) - p = $(esc(period)) - # And therefore n is safe to log. - @dassert0 n isa $SafeLoggable $SafeLogging.@safe($(error_msg)) - n = string(n) - end - end - """ - @test count_lint_errors(source) == 0 - end - - @testset "@safe in a macro 2" begin - source = raw""" - macro foo() - @info @safe("error_msg") - end - """ - @test count_lint_errors(source) == 0 - end - - @testset "@safe in a macro 3" begin - source = raw""" - macro foo() - @info $SafeLogging.@safe("error_msg") - end - """ - @test count_lint_errors(source) == 0 - end -end From a416406b72f4c4a958843ff5eeba861054af1c48 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 10 Mar 2026 14:30:28 +0100 Subject: [PATCH 11/62] Change rule group names --- src/interface.jl | 6 +++--- src/linting/argus-rules.jl | 28 ++++++++++++++-------------- src/linting/extended_checks.jl | 25 +++---------------------- 3 files changed, 20 insertions(+), 39 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 2874c0c..da8765f 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -26,9 +26,9 @@ Argus_result_to_LintRuleReport(rule::Rule, bindings::BindingSet) = false, 0) -is_recommendation(r::Rule) = haskey(RECOMMENDATION_RULE_GROUP, r.name) -is_violation(r::Rule) = haskey(VIOLATION_RULE_GROUP, r.name) -is_fatal(r::Rule) = haskey(FATAL_RULE_GROUP, r.name) +is_recommendation(r::Rule) = haskey(RECOMMENDATIONS, r.name) +is_violation(r::Rule) = haskey(VIOLATIONS, r.name) +is_fatal(r::Rule) = haskey(FATAL_VIOLATIONS, r.name) is_recommendation(r::LintRuleReport) = is_recommendation(r.rule) is_violation(r::LintRuleReport) = is_violation(r.rule) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 44cbd52..85eb133 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -3,10 +3,10 @@ using Argus # Recommendation rules # ==================== -RECOMMENDATION_RULE_GROUP = RuleGroup("recommendation") +RECOMMENDATIONS = RuleGroup("recommendations") # ReturnTypeAnnotationRule -@define_rule_in_group RECOMMENDATION_RULE_GROUP "return-type-annotation" begin +@define_rule_in_group RECOMMENDATIONS "return-type-annotation" begin description = """ Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. @@ -22,9 +22,9 @@ end # Violation rules # =============== -VIOLATION_RULE_GROUP = RuleGroup("violation") +VIOLATIONS = RuleGroup("violations") -@define_rule_in_group VIOLATION_RULE_GROUP "error" begin +@define_rule_in_group VIOLATIONS "error" begin description = """ Use custom exception instead of the generic `error()`. """ @@ -32,7 +32,7 @@ VIOLATION_RULE_GROUP = RuleGroup("violation") pattern = @pattern error({_}...) end -@define_rule_in_group VIOLATION_RULE_GROUP "in" begin +@define_rule_in_group VIOLATIONS "in" begin description = """ Use `tin(item,collection)` instead of the Julia's `in` or `∈`. """ @@ -43,7 +43,7 @@ end ) end -@define_rule_in_group VIOLATION_RULE_GROUP "haskey" begin +@define_rule_in_group VIOLATIONS "haskey" begin description = """ Use `thaskey(dict,key)` instead of the Julia's `haskey`. """ @@ -53,7 +53,7 @@ end # StringInterpolationRule # TODO: `trivia` flag. -# @define_rule_in_group VIOLATION_RULE_GROUP "string-interpolation" begin +# @define_rule_in_group VIOLATIONS "string-interpolation" begin # description = raw""" # Use $(x) instead of $x ([explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#string-interpolation)). # """ @@ -64,12 +64,12 @@ end # Fatal rules # =========== -FATAL_RULE_GROUP = RuleGroup("fatal") +FATAL_VIOLATIONS = RuleGroup("fatal violations") # GeneratedRule -@define_rule_in_group FATAL_RULE_GROUP "generated" begin +@define_rule_in_group FATAL_VIOLATIONS "generated" begin description = """ - Don't use `@generated` + `@generated` should be used with extreme caution. """ pattern = @pattern @generated {_}... @@ -112,7 +112,7 @@ end @pattern {_} = {arg:::safe_macro_arg} end # TODO: Rewrite with `not` pattern? -@define_rule_in_group FATAL_RULE_GROUP "unsafe-logging" begin +@define_rule_in_group FATAL_VIOLATIONS "unsafe-logging" begin description = """ Unsafe logging statement. You must enclose variables and strings with `@safe(...)`. """ @@ -141,7 +141,7 @@ end end "" end end -@define_rule_in_group FATAL_RULE_GROUP "unsafe-assert" begin +@define_rule_in_group FATAL_VIOLATIONS "unsafe-assert" begin description = """ Unsafe assertion statement. You must enclose the message with `@safe(...)`. """ @@ -166,7 +166,7 @@ end # MustNotUseShow # # TODO: Template. -@define_rule_in_group FATAL_RULE_GROUP "show" begin +@define_rule_in_group FATAL_VIOLATIONS "show" begin description = """ Do not use `@show`, use `@info` instead. """ @@ -268,7 +268,7 @@ end end # TODO: Support for allowing match if the source and pattern differ only by a flag # (e.g. INFIX_FLAG). -@define_rule_in_group FATAL_RULE_GROUP "noinline-lit-or-id" begin +@define_rule_in_group FATAL_VIOLATIONS "noinline with non-literal/identifier args" begin description = """ For call-site `@noinline` call, all args must be literals or identifiers only. \ Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086). diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl index 03c51c2..24c34e3 100644 --- a/src/linting/extended_checks.jl +++ b/src/linting/extended_checks.jl @@ -1,22 +1,3 @@ -################################################################################# -# This file contains many specific and extended rules for Lint. -# You probably needs to modify this files if: -# - you wish to add a new lint rule -# - document an existing rule -# -# If you wish to add a new Lint rule, you need: -# 1. Define a new type, subtype of RecommendationLintRule or ViolationLintRule -# 2. Write a new function function check(t::YOUR_NEW_TYPE, x::EXPR) -# 3. Add your unit tests in rai_rules_tests.jl -# 4. Restart your REPL if you use it -# -# If you wish to modify the report produced by Lint, interface.jl -# is probably the place to start, not this file. -################################################################################# - - -# abstract type LintFileExclusion end - struct LintContext rules_to_run::Vector{Rule} regex_exclusions #::Vector{LintFileExclusion} @@ -45,7 +26,7 @@ abstract type FatalLintRule <: ASTLintRule end include("text_lint_rules.jl") function all_rules() - return [values(RECOMMENDATION_RULE_GROUP)..., - values(VIOLATION_RULE_GROUP)..., - values(FATAL_RULE_GROUP)...] + return [values(RECOMMENDATIONS)..., + values(VIOLATIONS)..., + values(FATAL_VIOLATIONS)...] end From fbb489bb13b6a735ed6f75d501222c18a392708e Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 10 Mar 2026 14:30:40 +0100 Subject: [PATCH 12/62] Add "no return in anonymous functions" rule and tests --- src/linting/argus-rules.jl | 16 ++++++++++++ test/fatal.jl | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 85eb133..e88aeca 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -278,3 +278,19 @@ end end # NoReturnInAnonymousFunctionRule +@define_rule_in_group FATAL_VIOLATIONS "return in anonymous function" begin + description = """ + Anonymous function must not have `return` [Explanation](https://github.com/RelationalAI/RAIStyle#returning-from-a-closure). + """ + + pattern = @pattern ~or( + ({_}...) -> begin + {_}... + return {_}... + end, + {_}({_}...) do {_}... + {_}... + return {_}... + end + ) +end diff --git a/test/fatal.jl b/test/fatal.jl index 6b1dab0..230387b 100644 --- a/test/fatal.jl +++ b/test/fatal.jl @@ -194,4 +194,54 @@ @test lint_test(source, "Line 12, column 5: For call-site `@noinline` call, all args must be literals or identifiers only. Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086).") end + @testset "Return in anonymous functions" begin + + let + source = raw""" + function foo(f) + f() + return "foo" + end + + foo(() -> begin return "anonymous function" end) + foo() do ; return "anonymous function" end + foo() do + return "anonymous function" + end + + macro bar(x) + return :(($(x))()) + end + @bar(() -> begin return "anonymous function" end) + @bar() do + return "blah" + end + @bar() do ; return "blah" end + """ + @test count_lint_errors(source) == 4 # TODO: 6 + @test lint_test(source, + "Line 6, column 5: Anonymous function must not have `return` [Explanation](https") + @test lint_test(source, + "Line 7, column 1: Anonymous function must not have `return` [Explanation](https") + @test lint_test(source, + "Line 8, column 1: Anonymous function must not have `return` [Explanation](https") + @test lint_test(source, + "Line 15, column 6: Anonymous function must not have `return` [Explanation](https") + # TODO: Find a way around wrong pattern syntax with `{_} do {_}...`. + # @test lint_test(source, + # "Line 16, column 1: Anonymous function must not have `return` [Explanation](https") + # @test lint_test(source, + # "Line 19, column 1: Anonymous function must not have `return` [Explanation](https") + end + let + source = """ + @bar(() -> begin "anonymous function" end) + function f() + return 12 + end + """ + @test count_lint_errors(source) == 0 + end + end + end From 0592b8af7dc33803f6e73095e2641ecd23b87862 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 10 Mar 2026 18:08:31 +0100 Subject: [PATCH 13/62] Fix "return in anonymous function" rule --- src/linting/argus-rules.jl | 26 ++++++++++++++++++++++---- test/fatal.jl | 11 +++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index e88aeca..6a9732d 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -278,6 +278,27 @@ end end # NoReturnInAnonymousFunctionRule +@define_syntax_class :do_call_with_return "do call with explicit `return`" begin + @pattern begin + {f:::funcall} + @fail [:f] begin + isempty(f.args.src) && return true + do_node = f.args.src[end] + return !(kind(do_node) == K"do" && + kind(do_node.children[2].children[end]) == K"return") + end "" + end + @pattern begin + {m:::macrocall} + @fail [:m] begin + isempty(m.args) && return true + do_node = m.args[end] + return !(kind(do_node) == K"do" && + kind(do_node.children[2].children[end]) == K"return") + end "" + end +end + @define_rule_in_group FATAL_VIOLATIONS "return in anonymous function" begin description = """ Anonymous function must not have `return` [Explanation](https://github.com/RelationalAI/RAIStyle#returning-from-a-closure). @@ -288,9 +309,6 @@ end {_}... return {_}... end, - {_}({_}...) do {_}... - {_}... - return {_}... - end + {_:::do_call_with_return} ) end diff --git a/test/fatal.jl b/test/fatal.jl index 230387b..fcb5763 100644 --- a/test/fatal.jl +++ b/test/fatal.jl @@ -218,7 +218,7 @@ end @bar() do ; return "blah" end """ - @test count_lint_errors(source) == 4 # TODO: 6 + @test count_lint_errors(source) == 6 @test lint_test(source, "Line 6, column 5: Anonymous function must not have `return` [Explanation](https") @test lint_test(source, @@ -227,11 +227,10 @@ "Line 8, column 1: Anonymous function must not have `return` [Explanation](https") @test lint_test(source, "Line 15, column 6: Anonymous function must not have `return` [Explanation](https") - # TODO: Find a way around wrong pattern syntax with `{_} do {_}...`. - # @test lint_test(source, - # "Line 16, column 1: Anonymous function must not have `return` [Explanation](https") - # @test lint_test(source, - # "Line 19, column 1: Anonymous function must not have `return` [Explanation](https") + @test lint_test(source, + "Line 16, column 1: Anonymous function must not have `return` [Explanation](https") + @test lint_test(source, + "Line 19, column 1: Anonymous function must not have `return` [Explanation](https") end let source = """ From 74402c4a137f5efb852e0e626a07fb42aaa7f187 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 11 Mar 2026 15:39:28 +0100 Subject: [PATCH 14/62] Update benchmarks --- Project.toml | 2 ++ benchmark/benchmark_path.jl | 13 +++++++ benchmark/raicode-benchmarks.jl | 59 ------------------------------ benchmark/run_benchmarks.jl | 64 +++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 59 deletions(-) create mode 100644 benchmark/benchmark_path.jl delete mode 100644 benchmark/raicode-benchmarks.jl create mode 100644 benchmark/run_benchmarks.jl diff --git a/Project.toml b/Project.toml index bedf18d..09e2856 100644 --- a/Project.toml +++ b/Project.toml @@ -6,6 +6,7 @@ version = "0.9.0" Argus = "e39743ec-b2cc-45ef-a990-b666fc093538" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Git = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -13,6 +14,7 @@ Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [compat] Argus = "0.1.0" BenchmarkTools = "1.6.3" +Git = "1.5.0" JuliaSyntax = "1.0.2" julia = "1" diff --git a/benchmark/benchmark_path.jl b/benchmark/benchmark_path.jl new file mode 100644 index 0000000..cc4b109 --- /dev/null +++ b/benchmark/benchmark_path.jl @@ -0,0 +1,13 @@ +if isempty(ARGS) || length(ARGS) == 1 + error("provide package path as argument") +end + +using BenchmarkTools +using Serialization + +import ReLint +b = redirect_stdout(devnull) do + @benchmark ReLint.run_lint(ARGS[2]) +end + +serialize(ARGS[1], b) diff --git a/benchmark/raicode-benchmarks.jl b/benchmark/raicode-benchmarks.jl deleted file mode 100644 index f5d23ba..0000000 --- a/benchmark/raicode-benchmarks.jl +++ /dev/null @@ -1,59 +0,0 @@ -using BenchmarkTools -import Git - -const git = Git.git() - -const RAICODE_DIR = if !isnothing(ARGS[1]) && isdir(ARGS[1]) - joinpath(ARGS[1], "src") -else - const TMP_DIR = mktempdir() - # Clone `raicode`. - run(pipeline(`$git clone https://github.com/RelationalAI/raicode.git $TMP_DIR`, - stdout=devnull)) - - joinpath(TMP_DIR, "raicode/src") -end - -function run_benchmarks(io=stdout) - # Setup worktree main branch. - run(pipeline(`$git worktree add ../main-with-modified-tests main-with-modified-tests`, - stdout=devnull)) - # Run benchmarks. - b_main = try - benchmark_branch("../main-with-modified-tests") - catch err - run(`$git worktree remove ../main-with-modified-tests`) - rethrow(err) - end - b_argus = benchmark_branch() - # Print results. - println(io, "Benchmarks on `main`:") - println(io, b_main) - println(io) - println(io, "Benchmarks on `iulia/argus`:") - println(io, b_argus) - println(io) - println(io, "Comparison (`iulia/argus` vs `main`):") - println(io, judge(median(b_argus), median(b_main))) - # Clean up. - run(`$git worktree remove ../main-with-modified-tests`) - - return nothing -end - -function benchmark_branch(branch_path::String=pwd()) - # Save current directory so we can get back to it. - current_dir = pwd() - # Switch to `branch_path` and run the benchmark there. - cd(branch_path) - @eval using ReLint - b = redirect_stdout(devnull) do - @benchmark ReLint.run_lint(RAICODE_DIR) - end - # Return to the original directory. - cd(current_dir) - - return b -end - -run_benchmarks() diff --git a/benchmark/run_benchmarks.jl b/benchmark/run_benchmarks.jl new file mode 100644 index 0000000..6d386e0 --- /dev/null +++ b/benchmark/run_benchmarks.jl @@ -0,0 +1,64 @@ +using BenchmarkTools, Serialization +import Pkg +import Git + +const git = Git.git() + +const BENCHMARK_DIR = if !isempty(ARGS) && isdir(ARGS[1]) + joinpath(ARGS[1], "src") +else + const TMP_DIR = mktempdir() + # Clone `Pluto`. + run(pipeline(`$git clone https://github.com/JuliaPluto/Pluto.jl.git $TMP_DIR`, + stdout=devnull)) + joinpath(TMP_DIR, "Pluto.jl/src") +end + +const MAIN_BRANCH = "main-with-modified-rules" + +# Save current directory so we can get back to it. +const CURRENT_DIR = pwd() + +# Setup benchmarking files. +const BENCHMARKS_FILE = joinpath(CURRENT_DIR, "benchmark/benchmark_path.jl") +const SERIALIZED_BENCHMARKS_FILE = mktemp()[1] + +# Setup worktree main branch. +run(pipeline(`$git worktree add ../$(MAIN_BRANCH) $(MAIN_BRANCH)`, + stdout=devnull)) +# Run benchmarks on main branch. +b_main = try + # Switch to `branch_path` and run the benchmark there. + cd("../$(MAIN_BRANCH)") + Pkg.activate("../$(MAIN_BRANCH)") + redirect_stdout(devnull) do + Pkg.instantiate() + end + run(`julia --project=. $(BENCHMARKS_FILE) $(SERIALIZED_BENCHMARKS_FILE) ../$(MAIN_BRANCH)`) + deserialize(SERIALIZED_BENCHMARKS_FILE) +catch err + run(`$git worktree remove ../$(MAIN_BRANCH)`) + rethrow(err) +end +# Clean up. +cd(CURRENT_DIR) +run(`$git worktree remove ../$(MAIN_BRANCH)`) + +# Run benchmarks on Argus branch. +Pkg.activate(CURRENT_DIR) +redirect_stdout(devnull) do + Pkg.add(url="https://github.com/iuliadmtru/Argus.jl") + Pkg.instantiate() +end +run(`julia --project=. $(BENCHMARKS_FILE) $(SERIALIZED_BENCHMARKS_FILE) $(CURRENT_DIR)`) +b_argus = deserialize(SERIALIZED_BENCHMARKS_FILE) + +# Print results. +println("Benchmarks on `main`:") +println(b_main) +println() +println("Benchmarks on `iulia/argus`:") +println(b_argus) +println() +println("Comparison (`iulia/argus` vs `main`):") +println(judge(median(b_argus), median(b_main))) From c1e2da11f6bbedab81a2f86e074f7e485ff653d2 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 13 Mar 2026 10:20:43 +0100 Subject: [PATCH 15/62] Add `@async` and function call in `const` rules --- src/linting/argus-rules.jl | 28 +++++++++++++++++++ test/runtests.jl | 1 + test/violations.jl | 55 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 test/violations.jl diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 6a9732d..0a4be62 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -24,6 +24,34 @@ end VIOLATIONS = RuleGroup("violations") +# TODO: Template for `@spawn`. +@define_rule_in_group VIOLATIONS "`@async`" begin + description = """ + Use `@spawn` instead of `@async`. + """ + + pattern = @pattern ~or(@async({_}), Threads.@async({_})) +end + +# TODO: Pattern variables in rule messages. +@define_rule_in_group VIOLATIONS "initializing with `nthreads`" begin + description = """ + `Threads.nthreads()` should not be used in a constant variable. + """ + + pattern = @pattern const {_} = Threads.nthreads() +end +@define_rule_in_group VIOLATIONS "initializing with `is_local_deployment`" begin + description = """ + `is_local_deployment()` should not be used in a constant variable. + """ + + pattern = @pattern ~or( + :(const {_} = is_local_deployment()), + :(const {_} = Deployment.is_local_deployment()) + ) +end + @define_rule_in_group VIOLATIONS "error" begin description = """ Use custom exception instead of the generic `error()`. diff --git a/test/runtests.jl b/test/runtests.jl index 43ba72e..6f0a94a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,6 +7,7 @@ include(joinpath(@__DIR__, "common.jl")) # include(joinpath(@__DIR__, "line_rules_tests.jl")) # include(joinpath(@__DIR__, "file_exclusion_tests.jl")) include(joinpath(@__DIR__, "fatal.jl")) +include(joinpath(@__DIR__, "violations.jl")) # include(joinpath(@__DIR__, "lint_context_tests.jl")) # include(joinpath(@__DIR__, "rai_rules_tests.jl")) diff --git a/test/violations.jl b/test/violations.jl new file mode 100644 index 0000000..f36610c --- /dev/null +++ b/test/violations.jl @@ -0,0 +1,55 @@ +@testset "Violations" begin + + @testset "`@async`" begin + source = """ + function f() + @async 1 + 2 + end + + function f() + Threads.@async 1 + 2 + end + """ + + @test lint_has_error_test(source) + @test count_lint_errors(source) == 2 + @test lint_test(source, + "Line 2, column 5: Use `@spawn` instead of `@async`.") + @test lint_test(source, + "Line 6, column 5: Use `@spawn` instead of `@async`.") + end + + @testset "Initializing with function" begin + let + source = """ + const x = Threads.nthreads() + const y = Deployment.is_local_deployment() + const z = is_local_deployment() + const u = foo() # Allowed + + function f() + if Deployment.is_local_deployment() + print("It is all good!") + end + return x + Threads.nthreads() + end + """ + @test count_lint_errors(source) == 3 + @test lint_test(source, + "Line 1, column 1: `Threads.nthreads()` should not be used in a constant variable.") + @test lint_test(source, + "Line 2, column 1: `is_local_deployment()` should not be used in a constant variable.") + @test lint_test(source, + "Line 3, column 1: `is_local_deployment()` should not be used in a constant variable.") + end + let + source = """ + function f() + return Threads.nthreads() + end + """ + @test !lint_has_error_test(source) + end + end + +end From d93c77539f0e944be24e8fc2c1232b6187a14b5c Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 17 Mar 2026 12:57:53 +0100 Subject: [PATCH 16/62] Add empty array rule --- src/linting/argus-rules.jl | 48 +++++++++++++++++++++++++++++++ test/violations.jl | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 0a4be62..af5446a 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -1,5 +1,35 @@ using Argus +# Rule metadata +# ============= + +# TODO: `string_literal` syntax class +@define_rule_metadata :exclude_files begin + args = @pattern [{files}...] + + pre_check = @check [:files] begin + file_names = map(s -> s.children[1].val, files.src) + if current_file() in file_names + skip_match() + end + end + + post_check = nothing +end + +@define_rule_metadata :only_in_dir begin + args = @pattern {dir} + + pre_check = @check [:dir] begin + dir_name = dir.src.children[1].val + if !contains(current_file(), dir_name) + skip_match() + end + end + + post_check = nothing +end + # Recommendation rules # ==================== @@ -52,6 +82,24 @@ end ) end +@define_rule_in_group VIOLATIONS "array with no specific type" begin + description = """ + Need a specific Array type to be provided. + """ + + pattern = @pattern ~and( + [], + ~not(~inside(~and( + {m:::macrocall}, + ~fail([:m], !in(m.name, ["@match", "@matchrule"]), "") + ))) + ) + + metadata = Dict( + :only_in_dir => "src/Compiler" + ) +end + @define_rule_in_group VIOLATIONS "error" begin description = """ Use custom exception instead of the generic `error()`. diff --git a/test/violations.jl b/test/violations.jl index f36610c..fe639e1 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -52,4 +52,63 @@ end end + @testset "Array with no specific type" begin + let + source = """ + function f() + x = [] + y = String[] + return vcat(x, y) + end + """ + # No error because this tmp file is not in the src/Compiler + @test !lint_has_error_test(source) + end + + let + source = """ + function f() + x = [] + y = String[] + return vcat(x, y) + end + """ + @test lint_test( + source, + "Line 2, column 9: Need a specific Array type to be provided.", + directory = "src/Compiler/") + end + + let + source = """ + function f() + @matchrule bindings_empty() = + Bindings([], _::Missing) => CoreBindings([]) + + @matchrule and_to_true() = + And([], annos) => BoolConstant(true, annos) + + @matchrule and_singleton() = + And([f], annos) => f + + @match CoreRelAbstract(bs2, [], as2) = e2 + + @matchrule and_to_true() = + And([], annos) => BoolConstant(true, annos) + + @matchrule exists_empty() = + Exists(e, _) where is_definitely_empty_expr(e) => + slice_to_false(input_val) + f = [] + end + """ + count_errors = count_lint_errors(source; directory = "src/Compiler/") + @test count_errors == 1 + @test lint_test( + source, + "Line 19, column 9: Need a specific Array type to be provided."; + directory = "src/Compiler") + end + end + end From dbdd81d373bee205184a7e662bbb89b367d8e5c7 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 17 Mar 2026 14:08:48 +0100 Subject: [PATCH 17/62] Normalise AST for Expr-SyntaxNode compatibility --- src/interface.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interface.jl b/src/interface.jl index da8765f..c4f4f5c 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -161,6 +161,7 @@ Runs lint checks on `text`, lints will be reported as comming from """ function lint_text(file_content_string::AbstractString; filename = "", context = LintContext()) ast = JuliaSyntax.parseall(SyntaxNode, file_content_string; filename=filename) + ast = Argus._normalise!(ast) all_lines = split(file_content_string, "\n") # TODO: Integrate markers in Argus rules. From 94ab89e368f1469240af460c3af5781c026c7d71 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 17 Mar 2026 14:25:42 +0100 Subject: [PATCH 18/62] Argus: rename rule metadata to rule hooks --- src/linting/argus-rules.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index af5446a..3d41e63 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -4,7 +4,7 @@ using Argus # ============= # TODO: `string_literal` syntax class -@define_rule_metadata :exclude_files begin +@define_rule_hook :exclude_files begin args = @pattern [{files}...] pre_check = @check [:files] begin @@ -17,7 +17,7 @@ using Argus post_check = nothing end -@define_rule_metadata :only_in_dir begin +@define_rule_hook :only_in_dir begin args = @pattern {dir} pre_check = @check [:dir] begin @@ -95,7 +95,7 @@ end ))) ) - metadata = Dict( + hooks = Dict( :only_in_dir => "src/Compiler" ) end From 9d7a701470b2e236224dd9b2e3c999b3eca38127 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 17 Mar 2026 17:03:45 +0100 Subject: [PATCH 19/62] Rewrite rules and syntax classes with functions --- src/linting/argus-rules.jl | 512 ++++++++++++++++++------------------- 1 file changed, 253 insertions(+), 259 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 3d41e63..3310b06 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -36,18 +36,18 @@ end RECOMMENDATIONS = RuleGroup("recommendations") # ReturnTypeAnnotationRule -@define_rule_in_group RECOMMENDATIONS "return-type-annotation" begin - description = """ +RECOMMENDATIONS["return-type-annotation"] = Rule( + "return-type-annotation", + """ Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations). - """ - - pattern = @pattern ~or( + """, + @pattern ~or( function ({f}({args}...)::{ret_type}) {_}... end, {_:::funcall}::{ret_type} = {_} ) -end +) # Violation rules # =============== @@ -55,87 +55,70 @@ end VIOLATIONS = RuleGroup("violations") # TODO: Template for `@spawn`. -@define_rule_in_group VIOLATIONS "`@async`" begin - description = """ - Use `@spawn` instead of `@async`. - """ - - pattern = @pattern ~or(@async({_}), Threads.@async({_})) -end +VIOLATIONS["`@async`"] = Rule( + "`@async`", + "Use `@spawn` instead of `@async`.", + @pattern ~or(@async({_}), Threads.@async({_})) +) # TODO: Pattern variables in rule messages. -@define_rule_in_group VIOLATIONS "initializing with `nthreads`" begin - description = """ - `Threads.nthreads()` should not be used in a constant variable. - """ - - pattern = @pattern const {_} = Threads.nthreads() -end -@define_rule_in_group VIOLATIONS "initializing with `is_local_deployment`" begin - description = """ - `is_local_deployment()` should not be used in a constant variable. - """ - - pattern = @pattern ~or( +VIOLATIONS["initializing with `nthreads`"] = Rule( + "initializing with `nthreads`", + "`Threads.nthreads()` should not be used in a constant variable.", + @pattern const {_} = Threads.nthreads() +) +VIOLATIONS["initializing with `is_local_deployment`"] = Rule( + "initializing with `is_local_deployment`", + "`is_local_deployment()` should not be used in a constant variable.", + @pattern ~or( :(const {_} = is_local_deployment()), :(const {_} = Deployment.is_local_deployment()) ) -end +) -@define_rule_in_group VIOLATIONS "array with no specific type" begin - description = """ - Need a specific Array type to be provided. - """ - - pattern = @pattern ~and( +VIOLATIONS["array with no specific type"] = Rule( + "array with no specific type", + "Need a specific Array type to be provided.", + (@pattern ~and( [], ~not(~inside(~and( {m:::macrocall}, ~fail([:m], !in(m.name, ["@match", "@matchrule"]), "") ))) - ) - - hooks = Dict( + )), + Dict( :only_in_dir => "src/Compiler" ) -end - -@define_rule_in_group VIOLATIONS "error" begin - description = """ - Use custom exception instead of the generic `error()`. - """ - - pattern = @pattern error({_}...) -end - -@define_rule_in_group VIOLATIONS "in" begin - description = """ - Use `tin(item,collection)` instead of the Julia's `in` or `∈`. - """ - - pattern = @pattern ~or( +) + +VIOLATIONS["error"] = Rule( + "error", + "Use custom exception instead of the generic `error()`.", + @pattern error({_}...) +) + +VIOLATIONS["in"] = Rule( + "in", + "Use `tin(item,collection)` instead of the Julia's `in` or `∈`.", + @pattern ~or( in({_}, {_}), ∈({_}, {_}) ) -end +) -@define_rule_in_group VIOLATIONS "haskey" begin - description = """ - Use `thaskey(dict,key)` instead of the Julia's `haskey`. - """ - - pattern = @pattern haskey({_}, {_}) -end +VIOLATIONS["haskey"] = Rule( + "haskey", + "Use `thaskey(dict,key)` instead of the Julia's `haskey`.", + @pattern haskey({_}, {_}) +) # StringInterpolationRule # TODO: `trivia` flag. -# @define_rule_in_group VIOLATIONS "string-interpolation" begin -# description = raw""" -# Use $(x) instead of $x ([explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#string-interpolation)). -# """ - -# pattern = @pattern ... -# end +# VIOLATIONS["string-interpolation"] = Rule( +# "string-interpolation", +# raw"Use $(x) instead of $x ([explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#string-interpolation)).", +# @pattern ... +# ) # Fatal rules # =========== @@ -143,57 +126,60 @@ end FATAL_VIOLATIONS = RuleGroup("fatal violations") # GeneratedRule -@define_rule_in_group FATAL_VIOLATIONS "generated" begin - description = """ - `@generated` should be used with extreme caution. - """ - - pattern = @pattern @generated {_}... -end +FATAL_VIOLATIONS["generated"] = Rule( + "generated", + "`@generated` should be used with extreme caution.", + @pattern @generated {_}... +) # LogStatementsMustBeSafe # # TODO: Syntax classes with parameters (e.g. macrocall(r"@info|@warn|@error|@debug")). -@define_syntax_class :log_macro "log macro" begin - @pattern begin - {m:::macrocall} - @fail [:m] begin - log_macro_names = r"@info|@warn|@error|@debug" - !startswith(m.name, log_macro_names) - end "" - end -end -@define_syntax_class :safe_macro_arg "safe macro argument" begin - # Safe literal. - @pattern begin - {arg} - @fail [:arg] begin - s = arg.src - k = kind(s) - # Strings are not safe. - k == K"string" || endswith(string(k), "String") && return true - # `nothing` is safe. - !isnothing(s.data.val) && s.data.val == :nothing && return false - # Chars and literals other than strings are safe. - (JuliaSyntax.is_literal(s) || k == K"char") && return false - # Anything else is not safe. - return true - end "" - end - # Argument wrapped in `@safe`. - @pattern ~or(@safe({args}...), - SafeLogging.@safe({args}...), - $SafeLogging.@safe({args}...)) - # Assignment with safe rhs. - @pattern {_} = {arg:::safe_macro_arg} -end +register_syntax_class!(:log_macro, SyntaxClass( + "log macro", + [ + @pattern begin + {m:::macrocall} + @fail [:m] begin + log_macro_names = r"@info|@warn|@error|@debug" + !startswith(m.name, log_macro_names) + end "" + end + ] +)) + +register_syntax_class!(:safe_macro_arg, SyntaxClass( + "safe macro argument", + [ + # Safe literal. + (@pattern begin + {arg} + @fail [:arg] begin + s = arg.src + k = kind(s) + # Strings are not safe. + k == K"string" || endswith(string(k), "String") && return true + # `nothing` is safe. + !isnothing(s.data.val) && s.data.val == :nothing && return false + # Chars and literals other than strings are safe. + (JuliaSyntax.is_literal(s) || k == K"char") && return false + # Anything else is not safe. + return true + end "" + end), + # Argument wrapped in `@safe`. + (@pattern ~or(@safe({args}...), + SafeLogging.@safe({args}...), + $SafeLogging.@safe({args}...))), + # Assignment with safe rhs. + (@pattern {_} = {arg:::safe_macro_arg}) + ] +)) # TODO: Rewrite with `not` pattern? -@define_rule_in_group FATAL_VIOLATIONS "unsafe-logging" begin - description = """ - Unsafe logging statement. You must enclose variables and strings with `@safe(...)`. - """ - - pattern = @pattern begin +FATAL_VIOLATIONS["unsafe-logging"] = Rule( + "unsafe-logging", + "Unsafe logging statement. You must enclose variables and strings with `@safe(...)`.", + @pattern begin {log_macro:::log_macro} @fail [:log_macro] begin for s in log_macro.args @@ -203,26 +189,27 @@ end return true end "log macro call with no unsafe args"; end -end +) # AssertionStatementsMustBeSafe # # TODO: Syntax classes with parameters (e.g. macrocall(r"@assert|@dassert")). -@define_syntax_class :assert_macro "assert macro" begin +register_syntax_class!(:assert_macro, SyntaxClass( + "assert macro", + [ + @pattern begin + {m:::macrocall} + @fail [:m] begin + assert_macro_names = r"@assert|@dassert" + !startswith(m.name, assert_macro_names) + end "" + end + ] +)) +FATAL_VIOLATIONS["unsafe-assert"] = Rule( + "unsafe-assert", + "Unsafe assertion statement. You must enclose the message with `@safe(...)`.", @pattern begin - {m:::macrocall} - @fail [:m] begin - assert_macro_names = r"@assert|@dassert" - !startswith(m.name, assert_macro_names) - end "" - end -end -@define_rule_in_group FATAL_VIOLATIONS "unsafe-assert" begin - description = """ - Unsafe assertion statement. You must enclose the message with `@safe(...)`. - """ - - pattern = @pattern begin {assert_macro:::assert_macro} @fail [:assert_macro] begin # Assert macro call with one argument is safe (the matching fails). @@ -235,156 +222,163 @@ end return true end "assert macro call with no unsafe args"; end -end +) # NonFrontShapeAPIUsageRule # MustNotUseShow # # TODO: Template. -@define_rule_in_group FATAL_VIOLATIONS "show" begin - description = """ - Do not use `@show`, use `@info` instead. - """ - - pattern = @pattern @show {_}... -end +FATAL_VIOLATIONS["show"] = Rule( + "show", + "Do not use `@show`, use `@info` instead.", + @pattern @show {_}... +) # NoinlineAndLiteralRule # # TODO: Splatting. -@define_syntax_class :noinline_with_non_lit_or_id_args "@nonline call with non lit or id args" begin - # Call or dotcall. - @pattern begin - ~or( - @noinline(({_})({args}...)), - @noinline(({_}).({args}...)), - ) - @fail [:args] begin - for s in args.src - k = kind(s) - k == K"parameters" && break - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return false - end - return true - end "" - end - # Call or dotcall with kwargs. - @pattern begin - ~or( - @noinline(({_})({args}...; {kwargs}...)), - @noinline(({_}).({args}...; {kwargs}...)) - ) - @fail [:args, :kwargs] begin - for s in args.src - k = kind(s) - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return false - end - for kws in kwargs.src - k = kind(kws) - JuliaSyntax.is_literal(kws) || JuliaSyntax.is_identifier(kws) || k == K"char" || k == K"string" || k == K"..." || - k == K"=" && (JuliaSyntax.is_literal(kws.children[2]) || JuliaSyntax.is_identifier(kws.children[2])) || - return false - end - return true - end "" - end - # Macrocall. - @pattern begin - @noinline({c:::macrocall}) - @fail [:c] begin - for s in c.args - k = kind(s) - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == JuliaSyntax.K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return false - end - return true - end "" - end - # Infix call. - @pattern begin - ~or(@noinline({i:::infix_call}), - @noinline({i:::infix_dotcall})) - @fail [:i] begin - # Check lhs. - s = i.lhs.src - k = kind(s) - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return false - # Check rhs. - s = i.rhs.src - k = kind(s) - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return false - return true - end "" - end -end +register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( + "@nonline call with non lit or id args", + [ + # Call or dotcall. + (@pattern begin + ~or( + @noinline(({_})({args}...)), + @noinline(({_}).({args}...)), + ) + @fail [:args] begin + for s in args.src + k = kind(s) + k == K"parameters" && break + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return false + end + return true + end "" + end), + # Call or dotcall with kwargs. + (@pattern begin + ~or( + @noinline(({_})({args}...; {kwargs}...)), + @noinline(({_}).({args}...; {kwargs}...)) + ) + @fail [:args, :kwargs] begin + for s in args.src + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return false + end + for kws in kwargs.src + k = kind(kws) + JuliaSyntax.is_literal(kws) || + JuliaSyntax.is_identifier(kws) || + k == K"char" || + k == K"string" || + k == K"..." || + k == K"=" && (JuliaSyntax.is_literal(kws.children[2]) || + JuliaSyntax.is_identifier(kws.children[2])) || + return false + end + return true + end "" + end), + # Macrocall. + (@pattern begin + @noinline({c:::macrocall}) + @fail [:c] begin + for s in c.args + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == JuliaSyntax.K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return false + end + return true + end "" + end), + # Infix call. + (@pattern begin + ~or(@noinline({i:::infix_call}), + @noinline({i:::infix_dotcall})) + @fail [:i] begin + # Check lhs. + s = i.lhs.src + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return false + # Check rhs. + s = i.rhs.src + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return false + return true + end "" + end) + ] +)) # TODO: Support for allowing match if the source and pattern differ only by a flag # (e.g. INFIX_FLAG). -@define_rule_in_group FATAL_VIOLATIONS "noinline with non-literal/identifier args" begin - description = """ +FATAL_VIOLATIONS["noinline with non-literal/identifier args"] = Rule( + "noinline with non-literal/identifier args", + """ For call-site `@noinline` call, all args must be literals or identifiers only. \ Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086). - """ - - pattern = @pattern {n:::noinline_with_non_lit_or_id_args} -end + """, + @pattern {n:::noinline_with_non_lit_or_id_args} +) # NoReturnInAnonymousFunctionRule -@define_syntax_class :do_call_with_return "do call with explicit `return`" begin - @pattern begin - {f:::funcall} - @fail [:f] begin - isempty(f.args.src) && return true - do_node = f.args.src[end] - return !(kind(do_node) == K"do" && - kind(do_node.children[2].children[end]) == K"return") - end "" - end - @pattern begin - {m:::macrocall} - @fail [:m] begin - isempty(m.args) && return true - do_node = m.args[end] - return !(kind(do_node) == K"do" && - kind(do_node.children[2].children[end]) == K"return") - end "" - end -end - -@define_rule_in_group FATAL_VIOLATIONS "return in anonymous function" begin - description = """ - Anonymous function must not have `return` [Explanation](https://github.com/RelationalAI/RAIStyle#returning-from-a-closure). - """ - - pattern = @pattern ~or( +register_syntax_class!(:do_call_with_return, SyntaxClass( + "do call with explicit `return`", + [ + (@pattern begin + {f:::funcall} + @fail [:f] begin + isempty(f.args.src) && return true + do_node = f.args.src[end] + return !(kind(do_node) == K"do" && + kind(do_node.children[2].children[end]) == K"return") + end "" + end), + (@pattern begin + {m:::macrocall} + @fail [:m] begin + isempty(m.args) && return true + do_node = m.args[end] + return !(kind(do_node) == K"do" && + kind(do_node.children[2].children[end]) == K"return") + end "" + end) + ] +)) + +FATAL_VIOLATIONS["return in anonymous function"] = Rule( + "return in anonymous function", + "Anonymous function must not have `return` [Explanation](https://github.com/RelationalAI/RAIStyle#returning-from-a-closure).", + @pattern ~or( ({_}...) -> begin {_}... return {_}... end, {_:::do_call_with_return} ) -end +) From f7e7697d5502d6240eff4c928788cf63961a2d43 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 18 Mar 2026 16:14:39 +0100 Subject: [PATCH 20/62] Change all `~fail`s to `~when`s --- src/linting/argus-rules.jl | 117 +++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 3310b06..ca1543d 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -41,8 +41,7 @@ RECOMMENDATIONS["return-type-annotation"] = Rule( """ Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. - [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations). - """, + [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations).""", @pattern ~or( function ({f}({args}...)::{ret_type}) {_}... end, {_:::funcall}::{ret_type} = {_} @@ -83,7 +82,7 @@ VIOLATIONS["array with no specific type"] = Rule( [], ~not(~inside(~and( {m:::macrocall}, - ~fail([:m], !in(m.name, ["@match", "@matchrule"]), "") + ~when([:m], m.name in ["@match", "@matchrule"]) ))) )), Dict( @@ -140,10 +139,10 @@ register_syntax_class!(:log_macro, SyntaxClass( [ @pattern begin {m:::macrocall} - @fail [:m] begin + @when [:m] begin log_macro_names = r"@info|@warn|@error|@debug" - !startswith(m.name, log_macro_names) - end "" + startswith(m.name, log_macro_names) + end end ] )) @@ -154,18 +153,18 @@ register_syntax_class!(:safe_macro_arg, SyntaxClass( # Safe literal. (@pattern begin {arg} - @fail [:arg] begin + @when [:arg] begin s = arg.src k = kind(s) # Strings are not safe. - k == K"string" || endswith(string(k), "String") && return true + (k == K"string" || endswith(string(k), "String")) && return false # `nothing` is safe. - !isnothing(s.data.val) && s.data.val == :nothing && return false + !isnothing(s.data.val) && s.data.val == :nothing && return true # Chars and literals other than strings are safe. - (JuliaSyntax.is_literal(s) || k == K"char") && return false + (JuliaSyntax.is_literal(s) || k == K"char") && return true # Anything else is not safe. - return true - end "" + return false + end end), # Argument wrapped in `@safe`. (@pattern ~or(@safe({args}...), @@ -175,19 +174,21 @@ register_syntax_class!(:safe_macro_arg, SyntaxClass( (@pattern {_} = {arg:::safe_macro_arg}) ] )) -# TODO: Rewrite with `not` pattern? + FATAL_VIOLATIONS["unsafe-logging"] = Rule( "unsafe-logging", - "Unsafe logging statement. You must enclose variables and strings with `@safe(...)`.", + """ + Unsafe logging statement. \ + You must enclose variables and strings with `@safe(...)`.""", @pattern begin {log_macro:::log_macro} - @fail [:log_macro] begin + @when [:log_macro] begin for s in log_macro.args c = Argus.SYNTAX_CLASS_REGISTRY[:safe_macro_arg] - is_successful(syntax_match(c, s)) || return false + is_successful(syntax_match(c, s)) || return true end - return true - end "log macro call with no unsafe args"; + return false + end end ) @@ -199,28 +200,30 @@ register_syntax_class!(:assert_macro, SyntaxClass( [ @pattern begin {m:::macrocall} - @fail [:m] begin + @when [:m] begin assert_macro_names = r"@assert|@dassert" - !startswith(m.name, assert_macro_names) - end "" + startswith(m.name, assert_macro_names) + end end ] )) FATAL_VIOLATIONS["unsafe-assert"] = Rule( "unsafe-assert", - "Unsafe assertion statement. You must enclose the message with `@safe(...)`.", + """ + Unsafe assertion statement. \ + You must enclose the message with `@safe(...)`.""", @pattern begin {assert_macro:::assert_macro} - @fail [:assert_macro] begin - # Assert macro call with one argument is safe (the matching fails). - length(assert_macro.args) == 1 && return true + @when [:assert_macro] begin + # Assert macro call with one argument is safe. + length(assert_macro.args) == 1 && return false for s in assert_macro.args[2:end] c = Argus.SYNTAX_CLASS_REGISTRY[:safe_macro_arg] - is_successful(syntax_match(c, s)) || return false + is_successful(syntax_match(c, s)) || return true end # All assert macro call arguments are safe. - return true - end "assert macro call with no unsafe args"; + return false + end end ) @@ -236,8 +239,6 @@ FATAL_VIOLATIONS["show"] = Rule( ) # NoinlineAndLiteralRule -# -# TODO: Splatting. register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( "@nonline call with non lit or id args", [ @@ -247,7 +248,7 @@ register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( @noinline(({_})({args}...)), @noinline(({_}).({args}...)), ) - @fail [:args] begin + @when [:args] begin for s in args.src k = kind(s) k == K"parameters" && break @@ -256,10 +257,10 @@ register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( k == K"char" || k == K"string" && all(c -> isa(c.data.val, String), s.children) || k == K"..." || - return false + return true end - return true - end "" + return false + end end), # Call or dotcall with kwargs. (@pattern begin @@ -267,7 +268,7 @@ register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( @noinline(({_})({args}...; {kwargs}...)), @noinline(({_}).({args}...; {kwargs}...)) ) - @fail [:args, :kwargs] begin + @when [:args, :kwargs] begin for s in args.src k = kind(s) JuliaSyntax.is_literal(s) || @@ -275,7 +276,7 @@ register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( k == K"char" || k == K"string" && all(c -> isa(c.data.val, String), s.children) || k == K"..." || - return false + return true end for kws in kwargs.src k = kind(kws) @@ -286,15 +287,15 @@ register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( k == K"..." || k == K"=" && (JuliaSyntax.is_literal(kws.children[2]) || JuliaSyntax.is_identifier(kws.children[2])) || - return false + return true end - return true - end "" + return false + end end), # Macrocall. (@pattern begin @noinline({c:::macrocall}) - @fail [:c] begin + @when [:c] begin for s in c.args k = kind(s) JuliaSyntax.is_literal(s) || @@ -302,16 +303,16 @@ register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( k == JuliaSyntax.K"char" || k == K"string" && all(c -> isa(c.data.val, String), s.children) || k == K"..." || - return false + return true end - return true - end "" + return false + end end), # Infix call. (@pattern begin ~or(@noinline({i:::infix_call}), @noinline({i:::infix_dotcall})) - @fail [:i] begin + @when [:i] begin # Check lhs. s = i.lhs.src k = kind(s) @@ -320,7 +321,7 @@ register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( k == K"char" || k == K"string" && all(c -> isa(c.data.val, String), s.children) || k == K"..." || - return false + return true # Check rhs. s = i.rhs.src k = kind(s) @@ -329,9 +330,9 @@ register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( k == K"char" || k == K"string" && all(c -> isa(c.data.val, String), s.children) || k == K"..." || - return false - return true - end "" + return true + return false + end end) ] )) @@ -352,21 +353,21 @@ register_syntax_class!(:do_call_with_return, SyntaxClass( [ (@pattern begin {f:::funcall} - @fail [:f] begin - isempty(f.args.src) && return true + @when [:f] begin + isempty(f.args.src) && return false do_node = f.args.src[end] - return !(kind(do_node) == K"do" && - kind(do_node.children[2].children[end]) == K"return") - end "" + return kind(do_node) == K"do" && + kind(do_node.children[2].children[end]) == K"return" + end end), (@pattern begin {m:::macrocall} - @fail [:m] begin - isempty(m.args) && return true + @when [:m] begin + isempty(m.args) && return false do_node = m.args[end] - return !(kind(do_node) == K"do" && - kind(do_node.children[2].children[end]) == K"return") - end "" + return kind(do_node) == K"do" && + kind(do_node.children[2].children[end]) == K"return" + end end) ] )) From 7b0aaa37656425e5e3db1fc8b14e9c226e79612e Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 20 Mar 2026 12:54:38 +0100 Subject: [PATCH 21/62] Add more violation rules --- src/linting/argus-rules.jl | 91 +++++++++++++++++++++++--- test/violations.jl | 128 +++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 9 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index ca1543d..15ee2a8 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -9,7 +9,7 @@ using Argus pre_check = @check [:files] begin file_names = map(s -> s.children[1].val, files.src) - if current_file() in file_names + if any(contains.(current_file(), file_names)) skip_match() end end @@ -54,8 +54,8 @@ RECOMMENDATIONS["return-type-annotation"] = Rule( VIOLATIONS = RuleGroup("violations") # TODO: Template for `@spawn`. -VIOLATIONS["`@async`"] = Rule( - "`@async`", +VIOLATIONS["@async"] = Rule( + "@async", "Use `@spawn` instead of `@async`.", @pattern ~or(@async({_}), Threads.@async({_})) ) @@ -90,12 +90,6 @@ VIOLATIONS["array with no specific type"] = Rule( ) ) -VIOLATIONS["error"] = Rule( - "error", - "Use custom exception instead of the generic `error()`.", - @pattern error({_}...) -) - VIOLATIONS["in"] = Rule( "in", "Use `tin(item,collection)` instead of the Julia's `in` or `∈`.", @@ -111,6 +105,85 @@ VIOLATIONS["haskey"] = Rule( @pattern haskey({_}, {_}) ) +VIOLATIONS["remove_page"] = Rule( + "remove_page", + "`remove_page` should be used with extreme caution.", + @pattern remove_page({_}, {_}) +) + +VIOLATIONS["Task"] = Rule( + "Task", + "`Task` should be used with extreme caution.", + @pattern Task({_}) +) + +VIOLATIONS["ErrorException"] = Rule( + "ErrorException", + "Use custom exception instead of the generic `ErrorException`.", + (@pattern ErrorException({_}...)), + Dict( + :exclude_files => ["test.jl", "tests.jl", "bench/", "Vectorized/Test"] + ) +) + +VIOLATIONS["error"] = Rule( + "error", + "Use custom exception instead of the generic `error()`.", + (@pattern error({_}...)), + Dict( + :exclude_files => ["test.jl", "tests.jl", "bench/", "Vectorized/Test"] + ) +) + +register_syntax_class!(:unsafe_funcall, SyntaxClass( + "`unsafe_*` function call", + [ + @pattern begin + {_f:::funcall} + @when [:_f] startswith(_f.fun_name.name, r"_?unsafe_") + end + ] +)) +VIOLATIONS["unsafe_ function"] = Rule( + "unsafe_ function", + "An `unsafe_` function should be called only from an `unsafe_` function.", + @pattern ~and( + {_:::unsafe_funcall}, + ~not(~inside(~or( + {_:::unsafe_funcall} = {_}, + function ({_:::unsafe_funcall}) {_}... end + ))) + ) +) + +# InRule + +# HasKeyRule + +# EqualRule + +# UvRule + +# UnreachableBranchRule + +# StringInterpolationRule + +# RelPathAPIUsageRule + +# UseOfStaticThreads + +# NoImportRule + +# NotImportingRAICodeRule + +# BareUsingRule + +# UntypedArrayComprehensionRule + +# ConstGlobalMissingTypeRule + +# NotFullyParametrizedConstructorRule + # StringInterpolationRule # TODO: `trivia` flag. # VIOLATIONS["string-interpolation"] = Rule( diff --git a/test/violations.jl b/test/violations.jl index fe639e1..74981cd 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -111,4 +111,132 @@ end end + @testset "`remove_page`" begin + source = """ + function _clear_pager!(pager) + for (pid, _) in pager.owned_pages + remove_page(pager, pid) + end + end + """ + @test lint_test(source, + """ + Line 3, column 9: \ + `remove_page` should be used with extreme caution.""") + end + + @testset "`Task`" begin + source = """ + function foo() + ch1 = Channel() + ch2 = Channel(10) + + a() = sum(i for i in 1:1000); + b = Task(a); + + return (ch1, ch2) + end + """ + @test lint_test(source, + "Line 6, column 9: `Task` should be used with extreme caution.") + end + + @testset "`ErrorException`" begin + source = """ + function foo() + ch1 = Channel() + ch2 = Channel(10) + + a() = sum(i for i in 1:1000); + + e = ErrorException("failure") + return (ch1, ch2) + end + + function bar(x) + throw(ErrorException("My error")) + end + """ + @test lint_test(source, + """ + Line 7, column 9: \ + Use custom exception instead of the generic `ErrorException`.""") + @test lint_test(source, + """ + Line 12, column 11: \ + Use custom exception instead of the generic `ErrorException`.""") + end + + @testset "`error`" begin + source = """ + bar() = error("My fault") + """ + @test lint_test(source, + """ + Line 1, column 9: \ + Use custom exception instead of the generic `error()`.""") + end + + @testset "`unsafe_` functions" begin + + let + source = """ + function unsafe_f() + unsafe_g() + end + + function unsafe_g() + return 42 + end + """ + @test !lint_has_error_test(source) + end + let + source = """ + function f() + unsafe_g() + end + + function unsafe_g() + return 42 + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + """ + Line 2, column 5: An `unsafe_` function \ + should be called only from an `unsafe_` function.""") + end + let + source = """ + function f() + _unsafe_g() + end + + function _unsafe_g() + return 42 + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + """ + Line 2, column 5: An `unsafe_` function \ + should be called only from an `unsafe_` function.""") + end + # TODO: Disabling. + # let + # source = """ + # function f() + # # lint-disable-next-line + # _unsafe_g() + # end + + # function _unsafe_g() + # return 42 + # end + # """ + # @test !lint_has_error_test(source) + # end + end + end From 866437ec10bf81c22755675544b46781776dafec Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 20 Mar 2026 13:52:21 +0100 Subject: [PATCH 22/62] Run all rules per node in the AST --- src/interface.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index c4f4f5c..a976cca 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -170,10 +170,14 @@ function lint_text(file_content_string::AbstractString; filename = "", c lint_rule_reports = [] # AST rules - for rule in context.rules_to_run - match_results = map(m -> m[1], rule_match(rule, ast).matches) - for match_result in match_results - push!(lint_rule_reports, Argus_result_to_LintRuleReport(rule, match_result)) + match_results = rules_match(context.rules_to_run, ast) + # Remove refactorings. + for (rule_name, match_result) in match_results + successful_match_results = map(m -> m[1], match_result.matches) + rule = + context.rules_to_run[findfirst(r -> r.name == rule_name, context.rules_to_run)] + for m in successful_match_results + push!(lint_rule_reports, Argus_result_to_LintRuleReport(rule, m)) end end From 8c6ef4382a3cf86b3462f19b7a5c70893b5b573f Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 25 Mar 2026 14:38:06 +0100 Subject: [PATCH 23/62] Argus change: `BindingSet`s store the source node, not just location and file --- src/interface.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index a976cca..21fcda1 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -20,9 +20,9 @@ Argus_result_to_LintRuleReport(rule::Rule, bindings::BindingSet) = LintRuleReport(rule, rule.description, "", - bindings.file_name, - bindings.source_location[1], - bindings.source_location[2], + JuliaSyntax.filename(bindings), + JuliaSyntax.source_location(bindings)[1], + JuliaSyntax.source_location(bindings)[2], false, 0) From 790e2b8b8a6ed86d2000a54fbed60cb9c11db67a Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 25 Mar 2026 14:38:46 +0100 Subject: [PATCH 24/62] Add rule disabling --- src/interface.jl | 28 ++++++++++++++++++++++++---- test/violations.jl | 26 +++++++++++++------------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 21fcda1..dfdba7c 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -164,13 +164,10 @@ function lint_text(file_content_string::AbstractString; filename = "", c ast = Argus._normalise!(ast) all_lines = split(file_content_string, "\n") - # TODO: Integrate markers in Argus rules. - markers::Dict{Symbol, String} = Dict(:filename => filename) - lint_rule_reports = [] # AST rules - match_results = rules_match(context.rules_to_run, ast) + match_results = rules_match(context.rules_to_run, ast; disabler=relint_disabler) # Remove refactorings. for (rule_name, match_result) in match_results successful_match_results = map(m -> m[1], match_result.matches) @@ -207,6 +204,29 @@ function lint_text(file_content_string::AbstractString; filename = "", c end get_all_lines_from_filename(filename::String) = open(io -> readlines(io), filename) +struct ReLintDisabler <: CommentDisabler end +const relint_disabler = ReLintDisabler() + +relint_disabler(line::AbstractString) = is_disable_all_comment(line) +function relint_disabler(rule::Rule, line::AbstractString) + is_disable_comment(line) || return false + is_disable_all_comment(line) && return true + is_disable_rule_comment(line, rule) && return true + return false +end +is_disable_comment(str::AbstractString) = + startswith(str, "# lint-disable-next-line") +is_disable_all_comment(str::AbstractString) = str == "# lint-disable-next-line" +function is_disable_rule_comment(str::AbstractString, rule::Rule) + is_disable_comment(str) || return false + split_command_from_rules = split(str, ":") + length(split_command_from_rules) == 2 || return false + rules_list = strip.(split(split_command_from_rules[2], ",")) + rule.name in rules_list && return true + # Allow disabling by rule description as well. + return startswith(rule.description, lstrip(split_command_from_rules[2])) +end + # Return (index_line, index_column, annotation) for a given offset in a source # Useful in tests? function convert_offset_to_line(offset::Integer, source::String) diff --git a/test/violations.jl b/test/violations.jl index 74981cd..13d72fb 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -224,19 +224,19 @@ should be called only from an `unsafe_` function.""") end # TODO: Disabling. - # let - # source = """ - # function f() - # # lint-disable-next-line - # _unsafe_g() - # end - - # function _unsafe_g() - # return 42 - # end - # """ - # @test !lint_has_error_test(source) - # end + let + source = """ + function f() + # lint-disable-next-line + _unsafe_g() + end + + function _unsafe_g() + return 42 + end + """ + @test !lint_has_error_test(source) + end end end From e0bca9aea74365c75e77bc3e9dd613ad2af60107 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 25 Mar 2026 17:42:45 +0100 Subject: [PATCH 25/62] More violation rules --- src/linting/argus-rules.jl | 170 +++++++++++++++++++++++++------ test/violations.jl | 199 ++++++++++++++++++++++++++++++++++++- 2 files changed, 336 insertions(+), 33 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 15ee2a8..f1918b6 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -90,21 +90,6 @@ VIOLATIONS["array with no specific type"] = Rule( ) ) -VIOLATIONS["in"] = Rule( - "in", - "Use `tin(item,collection)` instead of the Julia's `in` or `∈`.", - @pattern ~or( - in({_}, {_}), - ∈({_}, {_}) - ) -) - -VIOLATIONS["haskey"] = Rule( - "haskey", - "Use `thaskey(dict,key)` instead of the Julia's `haskey`.", - @pattern haskey({_}, {_}) -) - VIOLATIONS["remove_page"] = Rule( "remove_page", "`remove_page` should be used with extreme caution.", @@ -156,23 +141,152 @@ VIOLATIONS["unsafe_ function"] = Rule( ) ) -# InRule +VIOLATIONS["in"] = Rule( + "in", + "Use `tin(item,collection)` instead of the Julia's `in` or `∈`.", + @pattern ~or( + in({_}, {_}), + ∈({_}, {_}) + ) +) -# HasKeyRule +VIOLATIONS["haskey"] = Rule( + "haskey", + "Use `thaskey(dict,key)` instead of the Julia's `haskey`.", + @pattern haskey({_}, {_}) +) -# EqualRule +VIOLATIONS["equal"] = Rule( + "equal", + "Use `tequal(dict,key)` instead of the Julia's `equal`.", + @pattern equal({_}, {_}) +) -# UvRule +VIOLATIONS["uv"] = Rule( + "uv", + "`uv_` functions should be used with extreme caution.", + @pattern ~and( + {f:::funcall}, + ~when([:f], startswith(f.fun_name.name, "uv_")) + ) +) -# UnreachableBranchRule +# TODO: Find a way to express this in its most general form. Something like: +# +# @pattern ~or( +# if {cond} +# {_}... +# (elseif {_} {_}...)... +# elseif {cond} +# {_}... +# end, +# if {cond} +# {_}... +# (elseif {_} {_}...)... +# elseif {cond} +# {_}... +# else +# {_}... +# end +VIOLATIONS["unreachable branch"] = Rule( + "unreachable branch", + "Unreachable branch.", + @pattern ~or( + if {cond} + {_}... + elseif {cond} + {_}... + end, + if {cond} + {_}... + elseif {cond} + {_}... + else + {_}... + end, + if {cond} + {_}... + elseif {_} + {_}... + elseif {cond} + {_}... + end, + if {cond} + {_}... + elseif {_} + {_}... + elseif {cond} + {_}... + else + {_}... + end + ) +) -# StringInterpolationRule +VIOLATIONS["string interpolation"] = Rule( + "string interpolation", + "Use \$(x) instead of \$x.", + @pattern ~and( + "$({_}...)$({x:::identifier})$({_}...)", + ~when([:x], !inside_parens(x.src)) + ) +) -# RelPathAPIUsageRule +VIOLATIONS["RelPath"] = Rule( + "RelPath", + "Usage of type `RelPath` is not allowed in this context.", + (@pattern ~or( + {_}::RelPath, + RelPath({_}), + RelPath({_}, {_}) + )), + Dict( + :only_in_dir => "src/Compiler/Front" + ) +) +VIOLATIONS["RelPath split_path"] = Rule( + "RelPath split_path", + "Usage of `RelPath` API method `split_path` is not allowed in this context.", + (@pattern split_path({_})), + Dict( + :only_in_dir => "src/Compiler/Front" + ) +) +VIOLATIONS["RelPath drop_first"] = Rule( + "RelPath drop_first", + "Usage of `RelPath` API method `drop_first` is not allowed in this context.", + (@pattern drop_first({_})), + Dict( + :only_in_dir => "src/Compiler/Front" + ) +) +VIOLATIONS["RelPath relpath_from_signature"] = Rule( + "RelPath relpath_from_signature", + """ + Usage of `RelPath` API method `relpath_from_signature` is not allowed \ + in this context.""", + (@pattern relpath_from_signature({_})), + Dict( + :only_in_dir => "src/Compiler/Front" + ) +) -# UseOfStaticThreads +VIOLATIONS["static threads"] = Rule( + "static threads", + """ + Use `Threads.@threads :dynamic` instead of `Threads.@threads :static`. Static threads \ + must not be used as generated tasks will not be able to migrate across threads.""", + @pattern ~or( + (@threads :static {_}...), + Threads.@threads :static {_}... + ) +) -# NoImportRule +VIOLATIONS["no import"] = Rule( + "no import", + "Imports must be specified with `using` and not `import`.", + @pattern {_:::import} +) # NotImportingRAICodeRule @@ -184,14 +298,6 @@ VIOLATIONS["unsafe_ function"] = Rule( # NotFullyParametrizedConstructorRule -# StringInterpolationRule -# TODO: `trivia` flag. -# VIOLATIONS["string-interpolation"] = Rule( -# "string-interpolation", -# raw"Use $(x) instead of $x ([explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#string-interpolation)).", -# @pattern ... -# ) - # Fatal rules # =========== diff --git a/test/violations.jl b/test/violations.jl index 13d72fb..ffc79ea 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -223,7 +223,6 @@ Line 2, column 5: An `unsafe_` function \ should be called only from an `unsafe_` function.""") end - # TODO: Disabling. let source = """ function f() @@ -239,4 +238,202 @@ end end + @testset "in, equal, haskey, uv_" begin + source = """ + function f() + x = 10 in [10] + y = in(10, [10]) + z = equal(10, "hello") + w = haskey(Dict(1=>1000), 1) + a = uv_foo(10, 20) + b = ∈(10, [10]) + c = 10 ∈ [10] + + # No error + d = [x for x in 1:10] + groupby_vars = [v for v in child_vars if !tin(v, sort_input.vars)] + end + """ + @test count_lint_errors(source) == 7 + @test lint_test(source, + """ + Line 2, column 9: Use `tin(item,collection)` instead of \ + the Julia's `in`""") + @test lint_test(source, + """ + Line 3, column 9: Use `tin(item,collection)` instead of \ + the Julia's `in`""") + @test lint_test(source, + """ + Line 4, column 9: Use `tequal(dict,key)` instead of the \ + Julia's `equal`.""") + @test lint_test(source, + """ + Line 5, column 9: Use `thaskey(dict,key)` instead of the \ + Julia's `haskey`.""") + @test lint_test(source, + """ + Line 6, column 9: `uv_` functions should be used with \ + extreme caution.""") + @test lint_test(source, + """ + Line 7, column 9: Use `tin(item,collection)` instead of \ + the Julia's `in` or `∈`.""") + @test lint_test(source, + """ + Line 8, column 9: Use `tin(item,collection)` instead of \ + the Julia's `in` or `∈`.""") + end + + @testset "Unreachable branch" begin + let + source = """ + function f(x) + if x == 1 + return 12 + elseif x== 2 + return "Reachable_branch" + end + end + """ + @test !lint_has_error_test(source) + end + let + source = """ + function f(x) + if x == 1 + return 12 + elseif x== 1 + return "Unreachable_branch" + end + end + """ + @test lint_test(source, "Line 2, column 5: Unreachable branch.") + end + let + source = """ + function f(x) + if x <= 1 + return 12 + elseif x <= 1 + return "Unreachable_branch" + end + if x <= 1 + return 12 + elseif x + if cond1 + body1 + elseif cond2 + body2 + elseif cond1 + body3 + else + body4 + end + end + end + """ + @test count_lint_errors(source) == 2 + @test lint_test(source, "Line 2, column 5: Unreachable branch.") + @test lint_test(source, "Line 10, column 9: Unreachable branch.") + end + end + + @testset "Checking string interpolation" begin + source_with_error = raw""" + @INFO "($a.b.c)" + @INFO "$a.b.c" + @INFO "this string contains an error $a.b indeed!" + @INFO "this string contains an error $a .b.c indeed!" + @INFO "Test $test..." + Bla("bla/$blu", read(joinpath(@__DIR__, "bla", "$blu.bli"), String)) + path = "$dir/$name.csv" + """ + @test count_lint_errors(source_with_error) == 9 + @test lint_test(source_with_error, raw"Line 1, column 7: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 2, column 7: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 3, column 7: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 4, column 7: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 5, column 7: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 6, column 5: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 6, column 49: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 7, column 8: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 7, column 8: Use $(x) instead of $x.") + + source_without_error = raw""" + @INFO "$(a.b.c)" + @INFO "this string contains an error $(a.b.c) indeed!" + f = "bla-$(blu).pb.gz" + """ + @test count_lint_errors(source_without_error) == 0 + end + + @testset "RelPath front-end" begin + source = """ + function rel_sig_from_relpath(path) + (name, types) = split_path(path) + return RelationSignature(name, types.elements) + end + + function interpret(x, y, path) + rest = drop_first(path) + return RelPath(rest.elements[2:end]) + end + + function reverse(decl::EdbDecl) + return relpath_from_signature(decl.signature) + end + + function use_path(x, y::RelPath, z) + return y.elements + end + """ + + @test count_lint_errors(source; directory="/src/Compiler/Front") == 5 + @test count_lint_errors(source; directory="") == 0 + + @test lint_test(source, + """ + Line 2, column 21: Usage of `RelPath` API method `split_path` is \ + not allowed in this context."""; + directory="/src/Compiler/Front") + end + + @testset "Use of static threads" begin + source = raw""" + function f() + Threads.@threads :static for _ in 1:10 + println("foo") + end + + @threads :static for _ in 1:10 + println("foo") + end + + Threads.@threads :dynamic for _ in 1:10 + println("foo") + end + end + """ + @test lint_test(source, + """ + Line 2, column 5: Use `Threads.@threads :dynamic` instead of \ + `Threads.@threads :static`.""") + @test lint_test(source, + """ + Line 6, column 5: Use `Threads.@threads :dynamic` instead of \ + `Threads.@threads :static`.""") + end + + @testset "Forbid import" begin + source = """ + import foobar + import M: a + import N: a, b + + using M + """ + @test count_lint_errors(source) == 3 + end + end From 19ba78ee24ab1071d2428094b5ceea0dd7822c48 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 2 Apr 2026 18:56:17 +0200 Subject: [PATCH 26/62] `cmdstring` test for string violation rule --- test/violations.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/violations.jl b/test/violations.jl index ffc79ea..e06c60c 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -348,8 +348,9 @@ @INFO "Test $test..." Bla("bla/$blu", read(joinpath(@__DIR__, "bla", "$blu.bli"), String)) path = "$dir/$name.csv" + cmd = `bla $blu bli` """ - @test count_lint_errors(source_with_error) == 9 + @test count_lint_errors(source_with_error) == 10 @test lint_test(source_with_error, raw"Line 1, column 7: Use $(x) instead of $x.") @test lint_test(source_with_error, raw"Line 2, column 7: Use $(x) instead of $x.") @test lint_test(source_with_error, raw"Line 3, column 7: Use $(x) instead of $x.") @@ -359,11 +360,13 @@ @test lint_test(source_with_error, raw"Line 6, column 49: Use $(x) instead of $x.") @test lint_test(source_with_error, raw"Line 7, column 8: Use $(x) instead of $x.") @test lint_test(source_with_error, raw"Line 7, column 8: Use $(x) instead of $x.") + @test lint_test(source_with_error, raw"Line 8, column 7: Use $(x) instead of $x.") source_without_error = raw""" @INFO "$(a.b.c)" @INFO "this string contains an error $(a.b.c) indeed!" f = "bla-$(blu).pb.gz" + cmd = `bla $(blu) bli` """ @test count_lint_errors(source_without_error) == 0 end From 9e5e289d180a19b3c79091090b7c71a9b5ab116d Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 2 Apr 2026 19:36:45 +0200 Subject: [PATCH 27/62] More violation rules --- src/linting/argus-rules.jl | 21 +++++++++-- test/violations.jl | 73 +++++++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index f1918b6..686f4d7 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -288,9 +288,26 @@ VIOLATIONS["no import"] = Rule( @pattern {_:::import} ) -# NotImportingRAICodeRule +VIOLATIONS["no import RAICode"] = Rule( + "no import RAICode", + "Importing RAICode should be avoided (when possible).", + @pattern begin + {u:::using} + @when [:u] u.module_name.name == "RAICode" + end +) -# BareUsingRule +VIOLATIONS["bare using"] = Rule( + "bare using", + "Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`.", + (@pattern begin + {u:::using} + @when [:u] isempty(u.ids.src) + end), + Dict( + :exclude_files => ["test/", "test.jl"] + ) +) # UntypedArrayComprehensionRule diff --git a/test/violations.jl b/test/violations.jl index e06c60c..135f97e 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -434,9 +434,80 @@ import M: a import N: a, b - using M + using M: a """ @test count_lint_errors(source) == 3 end + @testset "Forbid using RAICode" begin + source = """ + using RAICode + using RAICode: rai_rules_tests + + println("hello world") + """ + @test count_lint_errors(source) == 3 # One extra from base `using`. + end + + @testset "Forbid bare using" begin + let + source = """ + using LinearAlgebra + """ + @test lint_has_error_test(source) + @test lint_test(source, + """ + Line 1, column 1: Use `using Foo: Foo` or \ + `using Foo: specific_function` instead of bare `using Foo`.""") + end + let + source = """ + using Statistics + using LinearAlgebra + """ + @test count_lint_errors(source) == 2 + @test lint_test(source, + """ + Line 1, column 1: Use `using Foo: Foo` or \ + `using Foo: specific_function` instead of bare `using Foo`.""") + @test lint_test(source, + """ + Line 2, column 1: Use `using Foo: Foo` or \ + `using Foo: specific_function` instead of bare `using Foo`.""") + end + let + source = """ + using LinearAlgebra: LinearAlgebra + """ + @test !lint_has_error_test(source) + end + let + source = """ + using Statistics: mean, std + using LinearAlgebra: norm, dot + """ + @test !lint_has_error_test(source) + end + let + source = """ + using LinearAlgebra + using Statistics: mean, std + using Base: show + """ + @test count_lint_errors(source) == 1 + @test lint_test(source, + """ + Line 1, column 1: Use `using Foo: Foo` or \ + `using Foo: specific_function` instead of bare `using Foo`.""") + end + let + source = """ + using Test + using LinearAlgebra + """ + # Note: This will only pass when running on a file with "test" in the path + @test !lint_has_error_test(source; directory = "test/") + end + end + end From 28e249619ee62ce56fab03d48bae9cb7e96d1b60 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 13:12:19 +0200 Subject: [PATCH 28/62] Finish violation rules --- src/linting/argus-rules.jl | 65 ++++++++++++- test/violations.jl | 195 +++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 3 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 686f4d7..dce1d91 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -309,11 +309,70 @@ VIOLATIONS["bare using"] = Rule( ) ) -# UntypedArrayComprehensionRule +VIOLATIONS["untyped array comprehension"] = Rule( + "untyped array comprehension", + """ + Need a specific Array type to be provided. \ + Use `T[x for x in xs]` instead of `[x for x in xs]`.""", + (@pattern [{_} for {_} in {_}]), + Dict( + :only_in_dir => "src/Compiler/" + ) +) -# ConstGlobalMissingTypeRule +register_syntax_class!(:const, SyntaxClass( + "const assignment", + [ + (@pattern const {_} = {_}), + (@pattern const global {_} = {_}) + ] +)) +VIOLATIONS["non-const global"] = Rule( + "non-const global", + """ + Global variable must have type annotation: `global x::Type = value`. \ + Use `const` for immutable globals.""", + (@pattern ~and( + :(global {_:::identifier} = {_}), + ~not(~inside({_:::const}, 1)) + )), + Dict( + :exclude_files => ["test/", "test.jl"] + ) +) -# NotFullyParametrizedConstructorRule +register_syntax_class!(:loop, SyntaxClass( + "`for` or `while` loop", + [ + (@pattern (for {_} in {_} + {_}... + end)), + (@pattern (for {_} = {_} + {_}... + end)), + (@pattern (for {_} ∈ {_} + {_}... + end)), + (@pattern while {_} + {_}... + end) + ] +)) +VIOLATIONS["not fully parametrised constructor"] = Rule( + "not fully parametrised constructor", + """ + Avoid not-fully-parameterized constructor in loops. \ + Use a maker function instead for better performance.""", + (@pattern ~and( + {id:::identifier}({_}...), + ~inside({_:::loop}), + ~when([:id], startswith(id.name, r"[A-Z]")) + )), + Dict( + :only_in_dir => "src/Compiler", + :exclude_files => ["test/", "test.jl"] + ) +) # Fatal rules # =========== diff --git a/test/violations.jl b/test/violations.jl index 135f97e..f4dc7f7 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -510,4 +510,199 @@ end end + @testset "Untyped array comprehensions" begin + let + source = """ + function f() + x = [i for i in 1:10] + return x + end + """ + @test lint_test(source, + "Line 2, column 9: Need a specific Array type to be provided."; + directory = "src/Compiler/") + end + let + source = """ + function f() + x = Int[i for i in 1:10] + return x + end + """ + @test count_lint_errors(source; directory = "src/Compiler/") == 0 + end + let + source = """ + function f() + x = [i for i in 1:10] + return x + end + """ + @test count_lint_errors(source; directory = "src/Other/") == 0 + end + let + source = """ + function f() + x = [i for i in 1:10] + y = String[string(i) for i in 1:10] + return (x, y) + end + """ + @test count_lint_errors(source; directory = "src/Compiler/") == 1 + @test lint_test(source, + "Line 2, column 9: Need a specific Array type to be provided."; + directory = "src/Compiler/") + end + end + + @testset "Non const global variables" begin + let + source = """ + global counter = 0 + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 1, column 1: Global variable must have type annotation") + end + let + source = """ + const global MAX_SIZE = 100 + """ + @test !lint_has_error_test(source) + end + let + source = """ + function f() + x = 10 + return x + end + """ + @test !lint_has_error_test(source) + end + let + source = """ + global x = 1 + global y = 2 + const z = 3 + const global q = 3 + global yy::Int = 2 + """ + @test count_lint_errors(source) == 2 + @test lint_test(source, + "Line 1, column 1: Global variable must have type annotation") + @test lint_test(source, + "Line 2, column 1: Global variable must have type annotation") + end + end + + @testset "NotFullyParameterizedConstructorRule" begin + let + source = """ + function process(columns_list) + results = [] + for columns in columns_list + push!(results, ColumnarVector(columns)) + end + return results + end + """ + @test lint_test(source, + """ + Line 4, column 24: \ + Avoid not-fully-parameterized constructor in loops"""; + directory="src/Compiler/") + end + let + source = """ + function process(data_list) + outputs = [] + for data in data_list + push!(outputs, Vector(data)) + end + return outputs + end + """ + @test lint_test(source, + """ + Line 4, column 24: \ + Avoid not-fully-parameterized constructor in loops"""; + directory="src/Compiler/") + end + let + source = """ + function build_dicts(items) + results = [] + i = 1 + while i <= length(items) + push!(results, Dict(items[i])) + i += 1 + end + return results + end + """ + @test lint_test(source, + """ + Line 5, column 24: \ + Avoid not-fully-parameterized constructor in loops"""; + directory="src/Compiler/") + end + let + source = """ + function process(columns_list) + results = Vector{ColumnarVector{Int,Vector{Int}}}() + for columns in columns_list + push!(results, ColumnarVector{Int,Vector{Int}}(columns)) + end + return results + end + """ + @test !lint_has_error_test(source; directory="src/Compiler/") + end + let + source = """ + function process(columns_list) + results = Any[] + for columns in columns_list + push!(results, make_columnar_vector(columns)) + end + return results + end + """ + @test !lint_has_error_test(source; directory="src/Compiler/") + end + let + source = """ + function process(columns) + return ColumnarVector(columns) + end + """ + @test !lint_has_error_test(source; directory="src/Compiler/") + end + let + source = """ + function process(columns_list) + results = T[] + for columns in columns_list + push!(results, ColumnarVector(columns)) + end + return results + end + """ + # Should not trigger outside src/Compiler/. + @test !lint_has_error_test(source; directory="src/API/") + end + let + source = """ + function process(items) + results = Any[] + for item in items + push!(results, process_item(item)) + end + return results + end + """ + @test !lint_has_error_test(source; directory="src/Compiler/") + end + end + end From dad3268b54f2140bcbbbb5a9214c4c1307befd17 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 16:10:45 +0200 Subject: [PATCH 29/62] Change `only_in_dir` hook to `only_in_dirs` --- src/linting/argus-rules.jl | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index dce1d91..ad0619a 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -17,12 +17,12 @@ using Argus post_check = nothing end -@define_rule_hook :only_in_dir begin - args = @pattern {dir} +@define_rule_hook :only_in_dirs begin + args = @pattern [{dirs}...] - pre_check = @check [:dir] begin - dir_name = dir.src.children[1].val - if !contains(current_file(), dir_name) + pre_check = @check [:dirs] begin + dir_names = map(s -> s.children[1].val, dirs.src) + if !any(contains.(current_file(), dir_names)) skip_match() end end @@ -86,22 +86,25 @@ VIOLATIONS["array with no specific type"] = Rule( ))) )), Dict( - :only_in_dir => "src/Compiler" + :only_in_dirs => ["src/Compiler"] ) ) +# RemovePageRule VIOLATIONS["remove_page"] = Rule( "remove_page", "`remove_page` should be used with extreme caution.", @pattern remove_page({_}, {_}) ) +# TaskRule VIOLATIONS["Task"] = Rule( "Task", "`Task` should be used with extreme caution.", @pattern Task({_}) ) +# ErrorExceptionRule VIOLATIONS["ErrorException"] = Rule( "ErrorException", "Use custom exception instead of the generic `ErrorException`.", @@ -241,7 +244,7 @@ VIOLATIONS["RelPath"] = Rule( RelPath({_}, {_}) )), Dict( - :only_in_dir => "src/Compiler/Front" + :only_in_dirs => ["src/Compiler/Front"] ) ) VIOLATIONS["RelPath split_path"] = Rule( @@ -249,7 +252,7 @@ VIOLATIONS["RelPath split_path"] = Rule( "Usage of `RelPath` API method `split_path` is not allowed in this context.", (@pattern split_path({_})), Dict( - :only_in_dir => "src/Compiler/Front" + :only_in_dirs => ["src/Compiler/Front"] ) ) VIOLATIONS["RelPath drop_first"] = Rule( @@ -257,7 +260,7 @@ VIOLATIONS["RelPath drop_first"] = Rule( "Usage of `RelPath` API method `drop_first` is not allowed in this context.", (@pattern drop_first({_})), Dict( - :only_in_dir => "src/Compiler/Front" + :only_in_dirs => ["src/Compiler/Front"] ) ) VIOLATIONS["RelPath relpath_from_signature"] = Rule( @@ -267,10 +270,11 @@ VIOLATIONS["RelPath relpath_from_signature"] = Rule( in this context.""", (@pattern relpath_from_signature({_})), Dict( - :only_in_dir => "src/Compiler/Front" + :only_in_dirs => ["src/Compiler/Front"] ) ) +# UseOfStaticThreads VIOLATIONS["static threads"] = Rule( "static threads", """ @@ -316,10 +320,11 @@ VIOLATIONS["untyped array comprehension"] = Rule( Use `T[x for x in xs]` instead of `[x for x in xs]`.""", (@pattern [{_} for {_} in {_}]), Dict( - :only_in_dir => "src/Compiler/" + :only_in_dirs => ["src/Compiler/"] ) ) +# ConstGlobalMissingTypeRule register_syntax_class!(:const, SyntaxClass( "const assignment", [ @@ -369,7 +374,7 @@ VIOLATIONS["not fully parametrised constructor"] = Rule( ~when([:id], startswith(id.name, r"[A-Z]")) )), Dict( - :only_in_dir => "src/Compiler", + :only_in_dirs => ["src/Compiler"], :exclude_files => ["test/", "test.jl"] ) ) From cf2fd474352062bd725041ce472c416291f49bc3 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 16:11:41 +0200 Subject: [PATCH 30/62] Add comments with the previous rule names; minor changes --- src/linting/argus-rules.jl | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index ad0619a..af94b3c 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -53,6 +53,8 @@ RECOMMENDATIONS["return-type-annotation"] = Rule( VIOLATIONS = RuleGroup("violations") +# AsyncRule +# # TODO: Template for `@spawn`. VIOLATIONS["@async"] = Rule( "@async", @@ -60,6 +62,8 @@ VIOLATIONS["@async"] = Rule( @pattern ~or(@async({_}), Threads.@async({_})) ) +# InitializingWithFunctionRule +# # TODO: Pattern variables in rule messages. VIOLATIONS["initializing with `nthreads`"] = Rule( "initializing with `nthreads`", @@ -75,6 +79,7 @@ VIOLATIONS["initializing with `is_local_deployment`"] = Rule( ) ) +# ArrayWithNoTypeRule VIOLATIONS["array with no specific type"] = Rule( "array with no specific type", "Need a specific Array type to be provided.", @@ -114,6 +119,7 @@ VIOLATIONS["ErrorException"] = Rule( ) ) +# ErrorRule VIOLATIONS["error"] = Rule( "error", "Use custom exception instead of the generic `error()`.", @@ -123,6 +129,7 @@ VIOLATIONS["error"] = Rule( ) ) +# UnsafeRule register_syntax_class!(:unsafe_funcall, SyntaxClass( "`unsafe_*` function call", [ @@ -144,6 +151,7 @@ VIOLATIONS["unsafe_ function"] = Rule( ) ) +# InRule VIOLATIONS["in"] = Rule( "in", "Use `tin(item,collection)` instead of the Julia's `in` or `∈`.", @@ -153,18 +161,21 @@ VIOLATIONS["in"] = Rule( ) ) +# HasKeyRule VIOLATIONS["haskey"] = Rule( "haskey", "Use `thaskey(dict,key)` instead of the Julia's `haskey`.", @pattern haskey({_}, {_}) ) +# EqualRule VIOLATIONS["equal"] = Rule( "equal", "Use `tequal(dict,key)` instead of the Julia's `equal`.", @pattern equal({_}, {_}) ) +# UvRule VIOLATIONS["uv"] = Rule( "uv", "`uv_` functions should be used with extreme caution.", @@ -174,6 +185,8 @@ VIOLATIONS["uv"] = Rule( ) ) +# UnreachableBranchRule +# # TODO: Find a way to express this in its most general form. Something like: # # @pattern ~or( @@ -226,6 +239,7 @@ VIOLATIONS["unreachable branch"] = Rule( ) ) +# StringInterpolationRule VIOLATIONS["string interpolation"] = Rule( "string interpolation", "Use \$(x) instead of \$x.", @@ -235,6 +249,7 @@ VIOLATIONS["string interpolation"] = Rule( ) ) +# RelPathAPIUsageRule VIOLATIONS["RelPath"] = Rule( "RelPath", "Usage of type `RelPath` is not allowed in this context.", @@ -286,21 +301,24 @@ VIOLATIONS["static threads"] = Rule( ) ) +# NoImportRule VIOLATIONS["no import"] = Rule( "no import", "Imports must be specified with `using` and not `import`.", @pattern {_:::import} ) +# NotImportingRAICodeRule VIOLATIONS["no import RAICode"] = Rule( "no import RAICode", "Importing RAICode should be avoided (when possible).", @pattern begin {u:::using} - @when [:u] u.module_name.name == "RAICode" + @when [:u] u.module.module_name == "RAICode" end ) +# BareUsingRule VIOLATIONS["bare using"] = Rule( "bare using", "Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`.", @@ -313,6 +331,7 @@ VIOLATIONS["bare using"] = Rule( ) ) +# UntypedArrayComprehensionRule VIOLATIONS["untyped array comprehension"] = Rule( "untyped array comprehension", """ @@ -332,8 +351,8 @@ register_syntax_class!(:const, SyntaxClass( (@pattern const global {_} = {_}) ] )) -VIOLATIONS["non-const global"] = Rule( - "non-const global", +VIOLATIONS["non-const untyped global"] = Rule( + "non-const untyped global", """ Global variable must have type annotation: `global x::Type = value`. \ Use `const` for immutable globals.""", @@ -346,6 +365,7 @@ VIOLATIONS["non-const global"] = Rule( ) ) +# NotFullyParameterizedConstructorRule register_syntax_class!(:loop, SyntaxClass( "`for` or `while` loop", [ From d7fd7b7f02a8e51961201282be2feb00d2a797ba Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 17:40:21 +0200 Subject: [PATCH 31/62] Add recommendation rules --- src/linting/argus-rules.jl | 177 +++++++++++++- test/recommendations.jl | 476 +++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 3 files changed, 647 insertions(+), 7 deletions(-) create mode 100644 test/recommendations.jl diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index af94b3c..27a5c57 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -35,16 +35,179 @@ end RECOMMENDATIONS = RuleGroup("recommendations") +# CcallRule +RECOMMENDATIONS["ccall"] = Rule( + "ccall", + "`ccall` should be used with extreme caution.", + @pattern ccall({_}...) +) + +# FinalizerRule +RECOMMENDATIONS["finalizer"] = Rule( + "finalizer", + "`finalizer(_,_)` should not be used.", + @pattern finalizer({_}...) +) + +# CFunctionRule +RECOMMENDATIONS["@cfunction"] = Rule( + "@cfunction", + "`@cfunction` should not be used.", + @pattern @cfunction({_}...) +) + +# UnlockRule +RECOMMENDATIONS["unlock"] = Rule( + "unlock", + "`unlock` should be used with extreme caution.", + @pattern unlock({_}) +) + +# YieldRule +RECOMMENDATIONS["yield"] = Rule( + "yield", + "`yield` should be used with extreme caution.", + @pattern yield() +) + +# SleepRule +RECOMMENDATIONS["sleep"] = Rule( + "sleep", + "`sleep` should be used with extreme caution.", + @pattern sleep({_}) +) + +# InboundsRule +RECOMMENDATIONS["@inbounds"] = Rule( + "@inbounds", + "`@inbounds` should be used with extreme caution.", + @pattern @inbounds({_}) +) + +# ThreadsRule +RECOMMENDATIONS["@threads"] = Rule( + "@threads", + "`@threads` should be used with extreme caution.", + @pattern ~or(@threads({_}), Threads.@threads({_})) +) + +# SyncRule +RECOMMENDATIONS["@sync"] = Rule( + "@sync", + "`@sync` should be used with extreme caution.", + @pattern ~or(@sync({_}), Threads.@sync({_})) +) + +# SplattingRule +RECOMMENDATIONS["splatting"] = Rule( + "splatting", + """ + Splatting (`...`) should be used with extreme caution. Splatting from dynamically \ + sized containers could result in severe performance degradation. Splatting from \ + statically-sized tuples is usually okay.""", + (@pattern ~and( + {_}(@esc(({_}...)..., 1)), + ~not(~inside({_:::macrodef})) + )), + Dict( + :exclude_files => ["test.jl", "tests.jl"] + ) +) + +# InterpolationInSafeLogRule +RECOMMENDATIONS["interpolation in `@warnv_safe_to_log`"] = Rule( + "interpolation in `@warnv_safe_to_log`", + "Safe warning log has interpolation.", + @pattern @warnv_safe_to_log {_} "$({_}...)$({x:::identifier})$({_}...)" +) + # ReturnTypeAnnotationRule -RECOMMENDATIONS["return-type-annotation"] = Rule( - "return-type-annotation", +RECOMMENDATIONS["return type annotation"] = Rule( + "return type annotation", + """ + Avoid return type annotations `function foo()::Type`. \ + Return type annotations can hurt performance by forcing type conversions.""", + (@pattern ~and( + ~or( + function ({f}({args}...)::{ret_type}) {_}... end, + {_:::funcall}::{ret_type} = {_} + ), + ~not(~inside(@derived {_}...)) + )), + Dict( + :exclude_files => ["test/", "test.jl"] + ) +) + +# StringConcatenationRule +RECOMMENDATIONS["string concatenation"] = Rule( + "string concatenation", + """ + Prefer string interpolation or `string()` over `*` for string concatenation. \ + Use `\"\$(a)\$(b)\"` instead of `a * b`.""", + (@pattern ~or( + {_:::string} * {_}, + {_} * {_:::string} + )), + Dict( + :exclude_files => ["test/", "test.jl"] + ) +) + +# NoGlobalVariablesRule +# +# Removed because it is too similar to "non-const untyped global" in `VIOLATIONS`. + +# IsNothingPerformanceRule +RECOMMENDATIONS["isnothing"] = Rule( + "isnothing", + """ + In performance-critical code, prefer `x === nothing` or `x isa Nothing` over \ + `isnothing(x)`""", + (@pattern isnothing({_})), + Dict( + :only_in_dirs => ["src/Compiler/", "packages/Salsa/"], + :exclude_files => ["test/", "test.jl"] + ) +) + +# ClosureCaptureByValueRule +RECOMMENDATIONS["closure capture by reference"] = Rule( + "closure capture by reference", + """ + Nested function may capture variables by reference, causing boxing and type \ + instability. Consider using `let x = x` to capture by value for better performance.""", + (@pattern ~and( + ~or( + ({_}...) -> {_}, # Anonymous function. + {_}({_}...) do {x}... # `do` block. + {_}... + end, + {_:::fundef} # Function definition + ), + ~inside({_:::fundef}), + ~not(~inside(let {_}... + {_}... + end)) + )), + Dict( + :only_in_dirs => ["src/Compiler/"], + :exclude_files => ["test/", "test.jl"] + ) +) + +# AccessingENVRuleq +RECOMMENDATIONS["mutating ENV"] = Rule( + "mutating ENV", """ - Avoid return type annotations `function foo()::Type`. - Return type annotations can hurt performance by forcing type conversions. - [Explanation](https://github.com/RelationalAI/RAIStyle?tab=readme-ov-file#type-annotations).""", + Mutating `ENV` is not thread-safe, only do so in a single-threaded context.""", @pattern ~or( - function ({f}({args}...)::{ret_type}) {_}... end, - {_:::funcall}::{ret_type} = {_} + ENV[{_}] = {_}, + ~and( + {mutating_fun}(ENV, {_}...), + ~when([:mutating_fun], + mutating_fun.name in ["pop!", "delete!", "setindex!", "get!", "push!"]) + ) ) ) diff --git a/test/recommendations.jl b/test/recommendations.jl new file mode 100644 index 0000000..fed88a6 --- /dev/null +++ b/test/recommendations.jl @@ -0,0 +1,476 @@ +@testset "Recommendations" begin + + @testset "ccall" begin + source = """ + function bla(x::T = DEFAULT) + y = 1 + ccall(:bla, Cint, (Cint, Ptr{Cvoid}), x, y) + ccall(:jl_print_task_backtraces, Cvoid, ()) + return x + end + """ + @test count_lint_errors(source) == 2 + @test lint_test(source, + "Line 3, column 5: `ccall` should be used with extreme caution.") + @test lint_test(source, + "Line 4, column 5: `ccall` should be used with extreme caution.") + end + + @testset "finalizer" begin + let + source = """ + function f(x) + ref = Ref(1) + x = ___MutableFoo(ref) + finalizer(x) do x + ref[] = 3 + end + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 4, column 5: `finalizer(_,_)` should not be used.") + + end + let + source = """ + function f(x) + finalizer(q->nothing, x) + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 2, column 5: `finalizer(_,_)` should not be used.") + end + let + source = """ + finalizer("hello") do x + println("hello ") + println("world") + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 1, column 1: `finalizer(_,_)` should not be used.") + end + end + + @testset "@cfunction" begin + source = """ + function f() + @cfunction(_readwrite_cb, Cvoid, (Ptr{Cvoid}, )) + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 2, column 5: `@cfunction` should not be used.") + end + + @testset "unlock, yield, sleep" begin + source = """ + function wait_for_cooldown(count::UInt64, counts::HistogramCounts) + while count != @atomic counts.total_observations + yield() + sleep(0.011) + end + unlock(bla) + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 3, column 9: `yield` should be used with extreme caution.") + @test lint_test(source, + "Line 4, column 9: `sleep` should be used with extreme caution.") + @test lint_test(source, + "Line 6, column 5: `unlock` should be used with extreme caution.") + end + + @testset "@inbounds, @threads, @sync" begin + source = """ + function f() + @inbounds begin + at_end(iter) && return 0 + i = 1 + while next!(iter) + i += 1 + end + return i + end + Threads.@threads for (e, v) in slots + @test e[] == v + free_slot!(pool, Blob{Nothing}(e)) + end + try + @sync begin + bla + end + catch + Threads.@sync begin + 1 + 2 + end + nothing + end + end + """ + @test lint_test(source, + """ + Line 2, column 5: `@inbounds` should be used with extreme \ + caution.""") + @test lint_test(source, + """ + Line 10, column 5: `@threads` should be used with extreme \ + caution.""") + @test lint_test(source, + """ + Line 15, column 9: `@sync` should be used with extreme \ + caution.""") + @test lint_test(source, + """ + Line 19, column 9: `@sync` should be used with extreme \ + caution.""") + end + + @testset "Splatting" begin + @test lint_test("f(x...) + 10", + """ + Line 1, column 1: Splatting (`...`) should be used with extreme \ + caution. Splatting from dynamically sized containers could result \ + in severe performance degradation.""") + + @test lint_test("hcat([f(x) for x in r]...)", + """ + Line 1, column 1: Splatting (`...`) should be used with extreme \ + caution. Splatting from dynamically sized containers could result \ + in severe performance degradation.""") + + source = raw""" + macro infov(verbosity::Int64, msg, exs...) + return quote + if $(esc(:($DebugLevels.@should_emit_log($Logging.Info, $verbosity)))) + $(Base.CoreLogging.logmsg_code((Base.CoreLogging.@_sourceinfo)..., :Info, msg, :(verbosity=$verbosity), exs...)) + end + end + end + """ + @test count_lint_errors(source) == 0 + + @test count_lint_errors("""macro foo(x...)\nzork(x...)\nend""") == 0 + end + + @testset "Interpolation in @warnv_safe_to_log" begin + source = raw""" + function bla(blu) + if cond + @warnv_safe_to_log 0 "bla: $blu and $bli" + @ensure @warnv_safe_to_log 0 "bla: $blu and $bli" + @ensure @warnv_safe_to_log 0 "no interpolation" + return false + end + return true + end + """ + rule_name = "interpolation in `@warnv_safe_to_log`" + context = ReLint.LintContext([ReLint.RECOMMENDATIONS[rule_name]]) + @test count_lint_errors(source; context) == 4 + @test lint_test(source, "Line 3, column 9: Safe warning log has interpolation.") + @test lint_test(source, "Line 4, column 17: Safe warning log has interpolation.") + end + + @testset "Return type annotations" begin + @testset "function with return type annotation" begin + source = """ + function foo(x::Int)::String + return string(x) + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 1, column 1: Avoid return type annotations") + end + + @testset "function without return type annotation is ok" begin + source = """ + function foo(x::Int) + return string(x) + end + """ + @test !lint_has_error_test(source) + end + + @testset "multiple functions with annotations" begin + source = """ + function foo(x::Int)::String + return string(x) + end + + function bar(y::Float64)::Int + return round(Int, y) + end + + function baz(z) + return z + 1 + end + """ + @test count_lint_errors(source) == 2 + @test lint_test(source, + "Line 1, column 1: Avoid return type annotations") + @test lint_test(source, + "Line 5, column 1: Avoid return type annotations") + end + + @testset "one-liner with return type" begin + source = """ + foo(x::Int)::String = string(x) + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 1, column 1: Avoid return type annotations") + end + + @testset "one-liner without return type" begin + source = """ + foo(x::Int) = string(x) + """ + @test !lint_has_error_test(source) + end + + @testset "derived functions" begin + source = """ + @derived v=1 function foo(x::Int)::String + return string(x) + end + """ + @test !lint_has_error_test(source) + end + end + + @testset "String concatenation with *" begin + let + source = """ + function f() + x = "hello" * " world" + return x + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 2, column 9: Prefer string interpolation") + end + let + source = """ + function f(name) + msg = "Hello " * name + return msg + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 2, column 11: Prefer string interpolation") + end + let + source = raw""" + function f(name) + msg = "Hello $(name)" + return msg + end + """ + @test !lint_has_error_test(source) + end + let + source = """ + function f(x, y) + msg = string(x, y) + return msg + end + """ + @test !lint_has_error_test(source) + end + let + source = """ + function f() + x = 2 * 3 + return x + end + """ + @test !lint_has_error_test(source) + end + end + + @testset "isnothing" begin + let + source = """ + function process(x) + if isnothing(x) + return + end + end + """ + @test count_lint_errors(source; directory="src/Bla/") == 0 + @test count_lint_errors(source; directory="src/Compiler/") == 1 + @test lint_test(source, + """ + Line 2, column 8: In performance-critical code, prefer \ + `x === nothing`"""; + directory="src/Compiler/") + end + let + source = """ + function process(x) + if x === nothing + return + end + end + """ + @test count_lint_errors(source; directory="src/Compiler/") == 0 + end + end + + @testset "ClosureCaptureByValueRule" begin + let + source = """ + function outer(x) + inner() = x + 1 + return inner() + end + """ + @test lint_test(source, + """ + Line 2, column 5: Nested function may capture variables \ + by reference"""; + directory="src/Compiler/") + end + let + source = """ + function create_adder(n) + adder = x -> x + n + return adder + end + """ + @test lint_test(source, + """ + Line 2, column 13: Nested function may capture variables \ + by reference"""; + directory="src/Compiler/") + end + let + source = """ + function process(data) + map(data) do item + item * 2 + end + end + """ + @test lint_test(source, + """ + Line 2, column 5: Nested function may capture variables \ + by reference"""; + directory="src/Compiler/") + end + let + source = """ + function outer(x, y) + function inner(z) + return x + y + z + end + return inner(10) + end + """ + @test lint_test(source, + """ + Line 2, column 5: Nested function may capture variables \ + by reference"""; + directory="src/Compiler/") + end + let + source = """ + function main(x) + f = () -> x + 1 + if x < 0 + x = -x + end + return f() + end + """ + @test lint_test(source, + """ + Line 2, column 9: Nested function may capture variables \ + by reference"""; + directory="src/Compiler/") + end + let + source = """ + function outer(n) + add_n = x -> x + n + multiply_n = x -> x * n + return add_n(5) + multiply_n(3) + end + """ + @test count_lint_errors(source; directory="src/Compiler/") == 2 + end + let + source = """ + function outer(x) + inner() = x + 1 + return inner() + end + """ + @test !lint_has_error_test(source; directory="src/API/") + end + let + source = """ + function standalone(x) + return x + 1 + end + """ + @test !lint_has_error_test(source; directory="src/Compiler/") + end + + let + source = """ + function outer(items) + return map(process_item, items) + end + """ + @test !lint_has_error_test(source; directory="src/Compiler/") + end + let + source = """ + function main(x) + f = let x = x + () -> x + 1 + end + if x < 0 + x = -x + end + return f() + end + """ + @test !lint_has_error_test(source; directory="src/Compiler/") + end + end + + @testset "Mutating ENV" begin + let + source = raw""" + function f(name) + println("Env var: ", ENV["HOME"]) + end + """ + @test !lint_has_error_test(source) + end + let + source = raw""" + function f(name) + ENV["HOME"] = "/tmp" + push!(ENV, "NEW_VAR" => "value") + delete!(ENV, "HOME") + setindex!(ENV, "/tmp", "HOME") + get!(ENV, "/tmp", "HOME") + pop!(ENV, "HOME") + end + """ + @test count_lint_errors(source) == 6 + end + end + +end diff --git a/test/runtests.jl b/test/runtests.jl index 6f0a94a..ccdb70d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,7 @@ include(joinpath(@__DIR__, "common.jl")) # include(joinpath(@__DIR__, "file_exclusion_tests.jl")) include(joinpath(@__DIR__, "fatal.jl")) include(joinpath(@__DIR__, "violations.jl")) +include(joinpath(@__DIR__, "recommendations.jl")) # include(joinpath(@__DIR__, "lint_context_tests.jl")) # include(joinpath(@__DIR__, "rai_rules_tests.jl")) From dd2674e6c3ae3ad939a61d1aa556b1048826fd67 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 17:40:35 +0200 Subject: [PATCH 32/62] Specify context; change testset name --- test/violations.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/violations.jl b/test/violations.jl index f4dc7f7..d3e8dd2 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -102,7 +102,8 @@ f = [] end """ - count_errors = count_lint_errors(source; directory = "src/Compiler/") + context = ReLint.LintContext([ReLint.VIOLATIONS["array with no specific type"]]) + count_errors = count_lint_errors(source; directory = "src/Compiler/", context) @test count_errors == 1 @test lint_test( source, @@ -555,7 +556,7 @@ end end - @testset "Non const global variables" begin + @testset "Non-const untyped global variables" begin let source = """ global counter = 0 From bdd205bb0ee51b6cbe360e1542f960cf41ca57f3 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 17:41:07 +0200 Subject: [PATCH 33/62] Remove unnecessary file --- test/runtests.jl | 2 - test/test_new_rules.jl | 317 ----------------------------------------- 2 files changed, 319 deletions(-) delete mode 100644 test/test_new_rules.jl diff --git a/test/runtests.jl b/test/runtests.jl index ccdb70d..fd5c8bc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,5 +12,3 @@ include(joinpath(@__DIR__, "recommendations.jl")) # include(joinpath(@__DIR__, "lint_context_tests.jl")) # include(joinpath(@__DIR__, "rai_rules_tests.jl")) -# include(joinpath(@__DIR__, "test_new_rules.jl")) - diff --git a/test/test_new_rules.jl b/test/test_new_rules.jl deleted file mode 100644 index a85df34..0000000 --- a/test/test_new_rules.jl +++ /dev/null @@ -1,317 +0,0 @@ -# Tests for newly implemented ReLint rules - -using Test -using ReLint - -@testset "ConstGlobalMissingTypeRule" begin - # Should trigger warning - untyped global - code1 = """ - global my_state = 0 - """ - # Test would go here when ReLint is properly set up - # @test check_violations(code1, ConstGlobalMissingTypeRule) > 0 - - # Should NOT trigger - typed global - code2 = """ - global my_state::Int = 0 - """ - # @test check_violations(code2, ConstGlobalMissingTypeRule) == 0 - - # Should NOT trigger - const - code3 = """ - const MY_CONSTANT = 42 - """ - # @test check_violations(code3, ConstGlobalMissingTypeRule) == 0 -end - -@testset "IsNothingPerformanceRule" begin - # Should trigger in Compiler directory - code1 = """ - # filename: src/Compiler/foo.jl - function process(x) - if isnothing(x) - return - end - end - """ - # Would need to set markers[:filename] appropriately - - # Should NOT trigger in normal code - code2 = """ - # filename: src/API/foo.jl - function process(x) - if isnothing(x) - return - end - end - """ - - # Preferred pattern - code3 = """ - # filename: src/Compiler/foo.jl - function process(x) - if x === nothing - return - end - end - """ -end - -@testset "NotFullyParameterizedConstructorRule" begin - # Only triggers in src/Compiler/ directory - - @testset "Should trigger - constructor in loop" begin - source = """ - function process(columns_list) - results = [] - for columns in columns_list - push!(results, ColumnarVector(columns)) - end - return results - end - """ - @test lint_test(source, - "Avoid not-fully-parameterized constructor `ColumnarVector(...)` in loops", - directory="src/Compiler/") - end - - @testset "Should trigger - Vector constructor in loop" begin - source = """ - function process(data_list) - outputs = [] - for data in data_list - push!(outputs, Vector(data)) - end - return outputs - end - """ - @test lint_test(source, - "Avoid not-fully-parameterized constructor `Vector(...)` in loops", - directory="src/Compiler/") - end - - @testset "Should trigger - Dict constructor in while loop" begin - source = """ - function build_dicts(items) - results = [] - i = 1 - while i <= length(items) - push!(results, Dict(items[i])) - i += 1 - end - return results - end - """ - @test lint_test(source, - "Avoid not-fully-parameterized constructor `Dict(...)` in loops", - directory="src/Compiler/") - end - - @testset "Should NOT trigger - fully parameterized constructor" begin - source = """ - function process(columns_list) - results = Vector{ColumnarVector{Int,Vector{Int}}}() - for columns in columns_list - push!(results, ColumnarVector{Int,Vector{Int}}(columns)) - end - return results - end - """ - @test !lint_has_error_test(source, directory="src/Compiler/") - end - - @testset "Should NOT trigger - maker function" begin - source = """ - function process(columns_list) - results = Any[] - for columns in columns_list - push!(results, make_columnar_vector(columns)) - end - return results - end - """ - @test !lint_has_error_test(source, directory="src/Compiler/") - end - - @testset "Should NOT trigger - constructor outside loop" begin - source = """ - function process(columns) - return ColumnarVector(columns) - end - """ - @test !lint_has_error_test(source, directory="src/Compiler/") - end - - @testset "Should NOT trigger - not in Compiler directory" begin - source = """ - function process(columns_list) - results = [] - for columns in columns_list - push!(results, ColumnarVector(columns)) - end - return results - end - """ - # Should not trigger outside src/Compiler/ - @test !lint_has_error_test(source, directory="src/API/") - end - - @testset "Should NOT trigger - lowercase function (not constructor)" begin - source = """ - function process(items) - results = Any[] - for item in items - push!(results, process_item(item)) - end - return results - end - """ - @test !lint_has_error_test(source, directory="src/Compiler/") - end -end - -@testset "ClosureCaptureByValueRule" begin - # Only triggers in src/Compiler/ directory - # This is a recommendation, not a violation - - @testset "Should NOT trigger - short-form nested function (not yet supported)" begin - # Short-form function definitions like `inner() = x + 1` are not - # currently detected due to AST structure complexity - source = """ - function outer(x) - inner() = x + 1 - return inner() - end - """ - # This test documents current limitation - @test !lint_has_error_test(source, directory="src/Compiler/") - end - - @testset "Should trigger - lambda function" begin - source = """ - function create_adder(n) - adder = x -> x + n - return adder - end - """ - @test lint_test(source, - "Nested function may capture variables by reference", - directory="src/Compiler/") - end - - @testset "Should trigger - anonymous function with do block" begin - source = """ - function process(data) - map(data) do item - item * 2 - end - end - """ - @test lint_test(source, - "Nested function may capture variables by reference", - directory="src/Compiler/") - end - - @testset "Should NOT trigger - full function definition (not yet supported)" begin - # Full function...end nested functions are not detected due to - # marker timing issues. They would need special handling. - source = """ - function outer(x, y) - function inner(z) - return x + y + z - end - return inner(10) - end - """ - # This test documents that we DON'T detect full function...end yet - @test !lint_has_error_test(source, directory="src/Compiler/") - end - - @testset "Should trigger - lambda closure with mutation" begin - source = """ - function main(x) - f = () -> x + 1 - if x < 0 - x = -x - end - return f() - end - """ - @test lint_test(source, - "Nested function may capture variables by reference", - directory="src/Compiler/") - end - - @testset "Should trigger - multiple nested functions" begin - source = """ - function outer(n) - add_n = x -> x + n - multiply_n = x -> x * n - return add_n(5) + multiply_n(3) - end - """ - # Should trigger for both closures (may be multiple times due to AST traversal) - @test count_lint_errors(source, directory="src/Compiler/") >= 2 - end - - @testset "Should NOT trigger - not in Compiler directory" begin - source = """ - function outer(x) - inner() = x + 1 - return inner() - end - """ - # Should not trigger outside src/Compiler/ - @test !lint_has_error_test(source, directory="src/API/") - end - - @testset "Should NOT trigger - no nested function (top-level)" begin - source = """ - function standalone(x) - return x + 1 - end - """ - @test !lint_has_error_test(source, directory="src/Compiler/") - end - - @testset "Should NOT trigger - function call (not definition)" begin - source = """ - function outer(items) - return map(process_item, items) - end - """ - @test !lint_has_error_test(source, directory="src/Compiler/") - end - - @testset "Recommended pattern - let binding" begin - # This still triggers because we can't detect let bindings yet, - # but documents the intended pattern - source = """ - function main(x) - f = let x = x - () -> x + 1 - end - if x < 0 - x = -x - end - return f() - end - """ - # Note: This will still trigger a warning because the rule is conservative - # It recommends let-binding but doesn't check if it's already there - @test lint_test(source, - "Nested function may capture variables by reference", - directory="src/Compiler/") - end -end - -# Integration test - verify rules are registered -@testset "Rule Registration" begin - # Check that new rules are in the rule list - all_rule_names = string.(nameof.(ReLint.all_rules())) - - @test "ConstGlobalMissingTypeRule" in all_rule_names - @test "IsNothingPerformanceRule" in all_rule_names - @test "NotFullyParameterizedConstructorRule" in all_rule_names - @test "ClosureCaptureByValueRule" in all_rule_names -end From 9aabfa5ecf9fd375241a12c337b788000a00c479 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 18:07:05 +0200 Subject: [PATCH 34/62] Minor changes --- src/linting/argus-rules.jl | 6 ++---- src/linting/extended_checks.jl | 8 +++++--- test/violations.jl | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl index 27a5c57..990d286 100644 --- a/src/linting/argus-rules.jl +++ b/src/linting/argus-rules.jl @@ -568,8 +568,8 @@ VIOLATIONS["not fully parametrised constructor"] = Rule( FATAL_VIOLATIONS = RuleGroup("fatal violations") # GeneratedRule -FATAL_VIOLATIONS["generated"] = Rule( - "generated", +FATAL_VIOLATIONS["@generated"] = Rule( + "@generated", "`@generated` should be used with extreme caution.", @pattern @generated {_}... ) @@ -670,8 +670,6 @@ FATAL_VIOLATIONS["unsafe-assert"] = Rule( end ) -# NonFrontShapeAPIUsageRule - # MustNotUseShow # # TODO: Template. diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl index 24c34e3..de5eecb 100644 --- a/src/linting/extended_checks.jl +++ b/src/linting/extended_checks.jl @@ -26,7 +26,9 @@ abstract type FatalLintRule <: ASTLintRule end include("text_lint_rules.jl") function all_rules() - return [values(RECOMMENDATIONS)..., - values(VIOLATIONS)..., - values(FATAL_VIOLATIONS)...] + return [ + values(RECOMMENDATIONS)..., + values(VIOLATIONS)..., + values(FATAL_VIOLATIONS)... + ] end diff --git a/test/violations.jl b/test/violations.jl index d3e8dd2..73095c7 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -340,7 +340,7 @@ end end - @testset "Checking string interpolation" begin + @testset "String interpolation" begin source_with_error = raw""" @INFO "($a.b.c)" @INFO "$a.b.c" From 9af79b8382c5581ec5bbb428fe412c919326d3c3 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 18:31:30 +0200 Subject: [PATCH 35/62] Add interface tests --- src/interface.jl | 2 +- test/common.jl | 3 +- test/interface.jl | 931 ++++++++++++++++ test/rai_rules_tests.jl | 2213 --------------------------------------- test/runtests.jl | 2 +- 5 files changed, 935 insertions(+), 2216 deletions(-) create mode 100644 test/interface.jl delete mode 100644 test/rai_rules_tests.jl diff --git a/src/interface.jl b/src/interface.jl index dfdba7c..331ff06 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -654,7 +654,7 @@ function generate_report( file_prefix_to_remove::String = "", analyze_all_file_found_locally::Bool = false, stream_workflowcommand::IO = stdout, - rules_to_run::Vector{DataType} = all_extended_rule_types[], + rules_to_run::Vector{Rule} = all_rules(), pre_commit_file::String = "", ) if isfile(output_filename) diff --git a/test/common.jl b/test/common.jl index f12d82f..2f67695 100644 --- a/test/common.jl +++ b/test/common.jl @@ -1,5 +1,6 @@ using ReLint: ReLint, run_lint_on_text, convert_offset_to_line, - convert_offset_to_line_from_lines, MarkdownFormat, PlainFormat + convert_offset_to_line_from_lines, MarkdownFormat, PlainFormat, + has_values using ReLint: LintResult, LintContext using Test diff --git a/test/interface.jl b/test/interface.jl new file mode 100644 index 0000000..f466d7d --- /dev/null +++ b/test/interface.jl @@ -0,0 +1,931 @@ +@testset "offset to line" begin + source = """ + function f() + return Threads.nthreads() + end + """ + @test_throws BoundsError convert_offset_to_line(-1, source) + @test_throws BoundsError convert_offset_to_line(length(source) + 2, source) + + @test convert_offset_to_line(10, source) == (1, 10, nothing) + @test convert_offset_to_line(20, source) == (2, 7, nothing) + @test convert_offset_to_line(42, source) == (2, 29, nothing) + @test convert_offset_to_line(46, source) == (3, 3, nothing) + +end + +@testset "Formatter" begin + source = """ + const x = Threads.nthreads() + function f() + return x + end + """ + + @testset "Plain 02" begin + io = IOBuffer() + run_lint_on_text(source; io=io) + result = String(take!(io)) + + expected = r""" + ---------- \H+ + Line 1, column 1: `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ + 1 potential threat is found: 0 fatal violation, 1 violation and 0 recommendation + ---------- + """ + @test !isnothing(match(expected, result)) + end + + @testset "Markdown 02" begin + io = IOBuffer() + run_lint_on_text(source; io=io, formatter=MarkdownFormat()) + result = String(take!(io)) + + expected = r""" + - \*\*Line 1, column 1:\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ + """ + @test !isnothing(match(expected, result)) + end + + @testset "Markdown 03 - with github information" begin + formatter = MarkdownFormat("axb-example-with-lint-errors", "RelationalAI/raicode") + io = IOBuffer() + + run_lint_on_text( + source; + io, + formatter, + directory="/src/Compiler/") + result = String(take!(io)) + + expected = r""" + - \*\*\[Line 1, column 1:\]\(https://github\.com/RelationalAI/raicode/blob/axb-example-with-lint-errors/\H+/src/Compiler/tmp_julia_file\.jl#L1\)\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ + """ + @test !isnothing(match(expected, result)) + end + + @testset "Markdown 04 - with github information" begin + formatter = MarkdownFormat("axb-example-with-lint-errors", "RelationalAI/raicode") + io = IOBuffer() + run_lint_on_text( + source; + io, + formatter, + directory="src/Compiler/") + result = String(take!(io)) + expected = r""" + - \*\*\[Line 1, column 1:\]\(https://github\.com/RelationalAI/raicode/blob/axb-example-with-lint-errors/\H+/src/Compiler/tmp_julia_file\.jl#L1\)\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ + """ + @test !isnothing(match(expected, result)) + end +end + +@testset "Run several times on same file" begin + mktempdir() do dir + open(joinpath(dir, "foo.jl"), "w") do io + write(io, "function f()\n @async 1 + 1\nend\n") + flush(io) + + @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) + @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) + @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) + @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) + end + end +end + +@testset "Linting multiple files" begin + @testset "No errors" begin + local result_is_empty = false + mktempdir() do dir + open(joinpath(dir, "foo.jl"), "w") do io1 + open(joinpath(dir, "bar.jl"), "w") do io2 + write(io1, "function f()\n @spawn 1 + 1\nend\n") + write(io2, "function g()\n @spawn 1 + 1\nend\n") + + flush(io1) + flush(io2) + + str = IOBuffer() + ReLint.run_lint(dir; io=str, formatter=ReLint.MarkdownFormat()) + result = String(take!(str)) + result_is_empty = isempty(result) + end + end + end + @test result_is_empty + end + + @testset "Two files with errors" begin + local result_matching = false + mktempdir() do dir + open(joinpath(dir, "foo.jl"), "w") do io1 + open(joinpath(dir, "bar.jl"), "w") do io2 + write(io1, "function f()\n @async 1 + 1\nend\n") + write(io2, "function g()\n @async 1 + 1\nend\n") + + flush(io1) + flush(io2) + + str = IOBuffer() + ReLint.run_lint(dir; io=str, formatter=ReLint.MarkdownFormat()) + + result = String(take!(str)) + + expected = r""" + - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ + - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ + """ + result_matching = !isnothing(match(expected, result)) + end + end + end + @test result_matching + end + + @testset "No splatting warning in tests" begin + local result_matching = false + mktempdir() do dir + open(joinpath(dir, "foo.jl"), "w") do io1 + open(joinpath(dir, "bar_test.jl"), "w") do io2 + write(io1, "function f()\n foo(x...)\nend\n") + write(io2, "\n\nfunction g()\n foo(x...)\nend\n") + + flush(io1) + flush(io2) + + str = IOBuffer() + ReLint.run_lint(dir; io=str, formatter=ReLint.MarkdownFormat()) + + result = String(take!(str)) + + expected = " - **Line 2, column 3:** Splatting (`...`) should be used with extreme caution." + result_matching = startswith(result, expected) + result_matching = result_matching && !contains(result, "bar_test.jl") + end + end + end + @test result_matching + end + + @testset "Report generation of two files with errors" begin + local result_matching = false + mktempdir() do dir + file1 = joinpath(dir, "foo.jl") + file2 = joinpath(dir, "bar.jl") + open(file1, "w") do io1 + open(file2, "w") do io2 + write(io1, "function f()\n @async 1 + 1\nend\n") + write(io2, + "function g()\n finalizer(\"hello\") do x nothing\nend\nend") + + flush(io1) + flush(io2) + + output_file = tempname() + json_output = IOBuffer() + stream_workflowcommand = IOBuffer() + ReLint.generate_report([file1, file2], output_file; json_output, stream_workflowcommand) + + # Checking the Workflow command + stream_workflowcommand_report = String(take!(stream_workflowcommand)) + wc_lines = split(stream_workflowcommand_report, "\n") + @test length(wc_lines) == 3 + @test isempty(wc_lines[end]) + @test contains(wc_lines[1], "foo.jl,line=2,col=3::Use `@spawn` instead of `@async`.") + + # Checking the JSON + json_report = JSON3.read(String(take!(json_output))) + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 2 + + @test json_report[:data][:violation_count] == 1 + @test json_report[:data][:recommandation_count] == 1 + @test json_report[:data][:fatalviolations_count] == 0 + + local result + open(output_file) do oo + result = read(oo, String) + end + + # First violations across files, then recommendations across files + expected = r""" + ## Static code analyzer report + \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + Report creation time \(UTC\): \H+ + - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ + + +
+ For PR Reviewer \(1\) + + - \*\*Line 2, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+\ + +
+ + 🚨\*\*In total, 0 fatal rule violation, 1 rule violation and 1 PR reviewer recommendation are found over 2 Julia files\*\*🚨 + """ + result_matching = !isnothing(match(expected, result)) + # DEBUG: + !result_matching && @info result + end + end + end + @test result_matching + end + + @testset "Report generation of two files with errors 02" begin + local result_matching = false + mktempdir() do dir + file1 = joinpath(dir, "foo.jl") + file2 = joinpath(dir, "bar.jl") + open(file1, "w") do io1 + open(file2, "w") do io2 + write(io1, "function g()\n @async 1 + 1\nend\n finalizer(\"hello\") do x nothing\nend\n") + write(io2, "function f()\n @async 1 + 1\nend\n finalizer(\"hello\") do x nothing\nend\n") + + flush(io1) + flush(io2) + + output_file = tempname() + json_output = IOBuffer() + ReLint.generate_report([file1, file2], output_file; json_output, stream_workflowcommand=devnull) + + json_report = JSON3.read(String(take!(json_output))) + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 2 + + @test json_report[:data][:recommandation_count] == 2 + @test json_report[:data][:violation_count] == 2 + + local result + open(output_file) do oo + result = read(oo, String) + end + + # First violations across files, then recommendations across files + expected = r""" + ## Static code analyzer report + \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + Report creation time \(UTC\): \H+ + - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ + - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ + +
+ For PR Reviewer \(2\) + + - \*\*Line 4, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+ + - \*\*Line 4, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+ + +
+ + 🚨\*\*In total, 0 fatal rule violation, 2 rule violations and 2 PR reviewer recommendations are found over 2 Julia files\*\*🚨 + """ + result_matching = !isnothing(match(expected, result)) + # DEBUG: + !result_matching && @info result + end + end + end + @test result_matching + end + + @testset "Report generation of two files with errors 02 - JSON report" begin + local result_matching = false + mktempdir() do dir + file1 = joinpath(dir, "foo.jl") + file2 = joinpath(dir, "bar.jl") + open(file1, "w") do io1 + open(file2, "w") do io2 + write(io1, "function g()\n @async 1 + 1\nend\n finalizer(\"hello\") do x nothing\nend\n") + write(io2, "function f()\n @async 1 + 1\nend\n finalizer(\"hello\") do x nothing\nend\n") + + flush(io1) + flush(io2) + + output_file = tempname() + json_filename = tempname() + @test !isfile(json_filename) + # json_io = IOBuffer() + ReLint.generate_report([file1, file2], output_file; json_filename, stream_workflowcommand=devnull) + + @test isfile(json_filename) + json_content = open(io->read(io, String), json_filename) + json_report = JSON3.read(json_content) + + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 2 + + @test json_report[:data][:recommandation_count] == 2 + @test json_report[:data][:violation_count] == 2 + + local result + open(output_file) do oo + result = read(oo, String) + end + + # First violations across files, then recommendations across files + expected = r""" + ## Static code analyzer report + \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + Report creation time \(UTC\): \H+ + - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ + - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ + +
+ For PR Reviewer \(2\) + + - \*\*Line 4, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+ + - \*\*Line 4, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+ + +
+ + 🚨\*\*In total, 0 fatal rule violation, 2 rule violations and 2 PR reviewer recommendations are found over 2 Julia files\*\*🚨 + """ + result_matching = !isnothing(match(expected, result)) + # DEBUG: + !result_matching && @info result + end + end + end + @test result_matching + end + + @testset "No modified julia file" begin + output_file = tempname() + json_output = IOBuffer() + ReLint.generate_report(String[], output_file; json_output, stream_workflowcommand=devnull) + + json_report = JSON3.read(String(take!(json_output))) + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 0 + + @test json_report[:data][:recommandation_count] == 0 + @test json_report[:data][:violation_count] == 0 + + + expected = r""" + ## Static code analyzer report + \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + Report creation time \(UTC\): \H+ + No Julia file is modified or added in this PR. + """ + result = open(io->read(io, String), output_file) + + result_matching = !isnothing(match(expected, result)) + @test result_matching + end + + @testset "No result with no julia file" begin + local result_matching = false + mktempdir() do dir + file1 = joinpath(dir, "foo.txt") + open(file1, "w") do io1 + write(io1, "Hello World\n") + flush(io1) + + output_file = tempname() + json_output = IOBuffer() + ReLint.generate_report([file1], output_file; json_output, stream_workflowcommand=devnull) + + json_report = JSON3.read(String(take!(json_output))) + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 0 + @test json_report[:data][:recommandation_count] == 0 + @test json_report[:data][:violation_count] == 0 + + local result + open(output_file) do oo + result = read(oo, String) + end + + + expected = r""" + ## Static code analyzer report + \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + Report creation time \(UTC\): \H+ + No Julia file is modified or added in this PR. + """ + result_matching = !isnothing(match(expected, result)) + end + end + @test result_matching + end + + @testset "Report generation of two files with no errors" begin + local result_matching = false + mktempdir() do dir + file1 = joinpath(dir, "foo.jl") + file2 = joinpath(dir, "bar.jl") + open(file1, "w") do io1 + open(file2, "w") do io2 + write(io1, "function f()\n @spawn 1 + 1\nend\n") + write(io2, "function g()\n @spawn 1 + 1\nend\n") + + flush(io1) + flush(io2) + + output_file = tempname() + json_output = IOBuffer() + ReLint.generate_report([file1, file2], output_file; json_output, stream_workflowcommand=devnull) + + json_report = JSON3.read(String(take!(json_output))) + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 2 + @test json_report[:data][:recommandation_count] == 0 + @test json_report[:data][:violation_count] == 0 + + local result + open(output_file) do oo + result = read(oo, String) + end + + expected = r""" + ## Static code analyzer report + \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + Report creation time \(UTC\): \H+ + 🎉No potential threats are found over 2 Julia files.👍 + """ + result_matching = !isnothing(match(expected, result)) + end + end + end + @test result_matching + end + + @testset "Report generation of 1 file with no errors" begin + local result_matching = false + mktempdir() do dir + file1 = joinpath(dir, "foo.jl") + open(file1, "w") do io1 + write(io1, "function f()\n @spawn 1 + 1\nend\n") + flush(io1) + + output_file = tempname() + json_output = IOBuffer() + ReLint.generate_report([file1], output_file; json_output, stream_workflowcommand=devnull) + + json_report = JSON3.read(String(take!(json_output))) + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 1 + + @test json_report[:data][:recommandation_count] == 0 + @test json_report[:data][:violation_count] == 0 + + local result + open(output_file) do oo + result = read(oo, String) + end + + expected = r""" + ## Static code analyzer report + \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + Report creation time \(UTC\): \H+ + 🎉No potential threats are found over 1 Julia file.👍 + """ + result_matching = !isnothing(match(expected, result)) + end + end + @test result_matching + end + + @testset "Report generation of 1 file with 1 error and github info" begin + mktempdir() do dir + file1 = joinpath(dir, "foo.jl") + open(file1, "w") do io1 + write(io1, "function f()\n @async 1 + 1\nend\n") + flush(io1) + + output_file = tempname() + json_io = IOBuffer() + ReLint.generate_report( + [file1], + output_file; + json_output=json_io, + github_repository="RelationalAI/raicode", + branch_name="axb-foo-bar", + file_prefix_to_remove="var/", + stream_workflowcommand=devnull) + + json_report = JSON3.read(String(take!(json_io))) + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 1 + + @test json_report[:data][:violation_count] == 1 + @test json_report[:data][:recommandation_count] == 0 + + local result + open(output_file) do oo + result = read(oo, String) + end + + # Remove the first folder to address an issue of GitHub Action + # (See MarkdownFormat for more information) + corrected_file_name = ReLint.remove_prefix_from_filename(file1, "var/") + + expected = " - **[Line 2, column 3:]" * + "(https://github.com/RelationalAI/raicode/blob/axb-foo-bar/$(corrected_file_name)" * + "#L2)** Use `@spawn` instead of `@async`." + if !occursin(expected, result) + @info "didn't match" expected result + end + @test occursin(expected, result) + end + end + end + + @testset "Report generation of all the folder" begin + # This is a slow test + local result_matching = false + mktempdir() do dir + file1 = joinpath(dir, "foo.jl") + open(file1, "w") do io1 + write(io1, "function f()\n @async 1 + 1\nend\n") + flush(io1) + + output_file = tempname() + json_io = IOBuffer() + ReLint.generate_report( + [file1], # Ignored because of analyze_all_file_found_locally + output_file; + json_output=json_io, + github_repository="RelationalAI/raicode", + branch_name="axb-foo-bar", + file_prefix_to_remove="var/", + analyze_all_file_found_locally=true, # OVERRIDE THE PROVIDED SET OF FILES + stream_workflowcommand=devnull + ) + + json_report = JSON3.read(String(take!(json_io))) + + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] >= 2 + @test json_report[:data][:violation_count] >= 0 + @test json_report[:data][:recommandation_count] >= 0 + @test json_report[:data][:fatalviolations_count] >= 0 + + local result + open(output_file) do oo + result = read(oo, String) + end + last_line = filter(!isempty, split(result, "\n"))[end] + @test last_line != "No Julia file is modified or added in this PR." + end + end + end + + @testset "Limiting report" begin + # this tests create a Julia file with 100 violations, the report should mention + # 100 violations, however only (an arbitrary) 30 are reported. + local result_matching = false + mktempdir() do dir + file1 = joinpath(dir, "foo.jl") + open(file1, "w") do io1 + write(io1, "function f()\n") + for _ in 1:100 + write(io1, " @async 1 + 1\n") + end + write(io1, "end\n") + flush(io1) + + output_file = tempname() + json_output = IOBuffer() + ReLint.generate_report([file1], output_file; json_output, stream_workflowcommand=devnull) + + json_report = JSON3.read(String(take!(json_output))) + @test json_report[:source] == "ReLint" + @test json_report[:data][:files_count] == 1 + @test json_report[:data][:recommandation_count] == 0 + @test json_report[:data][:violation_count] == 100 + + local result + open(output_file) do oo + result = read(oo, String) + end + all_lines = split(result, "\n") + lines_count = length(all_lines) + @test lines_count < 70 + + @test all_lines[end-3] == "⚠️Only a subset of the violations and recommandations are here reported⚠️" + @test all_lines[end-2] == "🚨**In total, 0 fatal rule violation, 100 rule violations and 0 PR reviewer recommendation are found over 1 Julia file**🚨" + @test all_lines[end] == "" + end + end + end + + @testset "Diamond between files" begin + mktempdir() do dir + open(joinpath(dir, "leaf.jl"), "w") do io + write(io, "function f()\n @async 1 + 1\nend\n") + end + + open(joinpath(dir, "bar.jl"), "w") do io + write(io, "include(\"leaf.jl\")\n") + end + + str = IOBuffer() + result = ReLint.run_lint(dir; io=str, formatter=ReLint.MarkdownFormat()) + @test result.files_count == 2 + @test result.violations_count == 1 + @test result.recommendations_count == 0 + end + end +end + +@testset "Running on a directory" begin + @testset "Non empty directory" begin + local r + r = LintResult() + + formatters = [ReLint.PlainFormat(), ReLint.MarkdownFormat()] + for formatter in formatters + mktempdir() do dir + open(joinpath(dir, "foo.jl"), "w") do io + write(io, "function f()\n @async 1 + 1\nend\n") + flush(io) + str = IOBuffer() + append!(r, ReLint.run_lint(dir; io=str, formatter)) + end + end + end + @test (r.violations_count + r.recommendations_count) == 2 + end + + @testset "Empty directory" begin + mktempdir() do dir + @test ReLint.run_lint(dir) == LintResult() + end + end +end + +@testset "Locally disabling lint" begin + @testset "lint-disable-next-line" begin + @test !lint_has_error_test(""" + function f() + # lint-disable-next-line + @async 1 + 2 + end + """) + @test !lint_has_error_test(""" + function f() + # lint-disable-next-line + @async 1 + 2 + end + """) + + @test !lint_has_error_test(""" + function f() + # lint-disable-next-line + @async 1 + 2 + end + """) + + @test lint_has_error_test(""" + function f() + # lint-disable-next-line + @async 1 + 2 + + @async 1 + 3 + end + """) + @test lint_has_error_test(""" + function f() + @async 1 + 2 + # lint-disable-next-line + + @async 1 + 3 + end + """) + @test lint_has_error_test(""" + function f() + @async 1 + 2 + # lint-disable-next-line + @async 1 + 3 + end + """) + + source = """ + function f() + # lint-disable-next-line + @async 1 + 2 + + @async 1 + 3 + end + """ + source_lines = split(source, "\n") + @test convert_offset_to_line_from_lines(46, source_lines) == (3, 4, "lint-disable-line") + @test convert_offset_to_line_from_lines(64, source_lines) == (5, 4, nothing) + end + + @testset "Locally disabling rule 01" begin + source = """ + function f() + # lint-disable-next-line: Use `@spawn` instead of `@async`. + @async 1 + 1 + end + """ + source_lines = split(source, "\n") + @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) + @test convert_offset_to_line_from_lines(91, source_lines) == (3, 14, "lint-disable-line: Use `@spawn` instead of `@async`.") + + @test !lint_has_error_test(source) + end + + @testset "Locally disabling rule 02" begin + source = """ + function f() + # lint-disable-next-line: Use `@spawn` instead of `@async`. + @async unsafe_foo(12) + end + """ + source_lines = split(source, "\n") + @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) + @test convert_offset_to_line_from_lines(91, source_lines) == (3, 14, "lint-disable-line: Use `@spawn` instead of `@async`.") + + @test lint_has_error_test(source) + @test lint_test(source, + "Line 3, column 12: An `unsafe_` function should be called only from an `unsafe_` function.") + end + + @testset "Locally disabling rule 03" begin + source = """ + function f() + # lint-disable-next-line: An `unsafe_` function + @async unsafe_foo(42) + end + """ + @test lint_has_error_test(source) + @test lint_test(source, + "Line 3, column 5: Use `@spawn` instead of `@async`.") + end + + @testset "Locally disabling rule 04" begin + source = """ + function f() + # lint-disable-next-line:Use `@spawn` instead of `@async`. + @async 1 + 1 + end + """ + source_lines = split(source, "\n") + @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) + @test convert_offset_to_line_from_lines(91, source_lines) == (3, 15, "lint-disable-line: Use `@spawn` instead of `@async`.") + + @test !lint_has_error_test(source) + end + + @testset "Locally disabling rule 05" begin + source = """ + function f() + # lint-disable-next-line: Use `@spawn` instead of `@async`. + @async 1 + 1 + end + """ + source_lines = split(source, "\n") + @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) + @test convert_offset_to_line_from_lines(91, source_lines) == (3, 13, "lint-disable-line: Use `@spawn` instead of `@async`.") + + @test !lint_has_error_test(source) + end +end + +@testset "Relaxing unused bindings" begin + # @test lint_test(""" + # function f(a::Int64, b, c) + # local x + # return 42 + # end + # """, "Line 2, column 11: Variable has been assigned but not used") + + @test !lint_has_error_test(""" + function f(a::Int64, b, c) + local _ + return 42 + end + """) + + @test !lint_has_error_test(""" + function f(a::Int64, b, c) + local _x + return 42 + end + """) +end + +@testset "Recommentation separated from violations" begin + source = """ + function f() + @async 1 + 1 + end + function g() + @lock Lock() begin + 1 + 1 + end + end + """ + io=IOBuffer() + run_lint_on_text(source; io) + + result = String(take!(io)) + expected = r""" + ---------- \H+ + Line 2, column 5: Use `@spawn` instead of `@async`\. \H+ + 1 potential threat is found: 0 fatal violation, 1 violation and 0 recommendation + ---------- + """ + @test !isnothing(match(expected, result)) +end + +@testset "Arithmetic LintResult" begin + l1 = LintResult() + l2 = LintResult(1, 2, 3) + l3 = LintResult(10, 20, 30) + l6 = LintResult(10, 20, 30, 40) + l4 = LintResult(10, 20, 30, 40, ["foo.jl"], 100, []) + l5 = LintResult(10, 20, 30, 40, ["foo2.jl"], 250) + + + @test l1 == l1 + @test l1 == LintResult() + # @test (l1 + l2) == l2 + # @test (l3 + l2) == LintResult(11, 22, 33) + @test l4 != l5 + @test l3 != l4 + @test l3 != l5 + + append!(l4, l5) + @test l4 == LintResult(20, 40, 60, 80, ["foo.jl", "foo2.jl"], 350) +end + +@testset "PreCommit format" begin + @testset "No fatal violation" begin + local result_matching = false + mktempdir() do dir + open(joinpath(dir, "foo.jl"), "w") do io1 + open(joinpath(dir, "bar.jl"), "w") do io2 + write(io1, "function f()\n @async 1 + 1\nend\n") + write(io2, "function g()\n @async 1 + 1\nend\n") + + flush(io1) + flush(io2) + + str = IOBuffer() + result = ReLint.run_lint(dir; io=str, formatter=ReLint.PreCommitFormat()) + ReLint.print_summary(ReLint.PreCommitFormat(), str, result) + + result = String(take!(str)) + + expected = r""" + 2 potential threats are found: 0 fatal violation, 2 violations and 0 recommendation + """ + result_matching = !isnothing(match(expected, result)) + end + end + end + @test result_matching + end + + @testset "With fatal violations" begin + local result_matching = false + mktempdir() do dir + open(joinpath(dir, "foo.jl"), "w") do io1 + open(joinpath(dir, "bar.jl"), "w") do io2 + write(io1, "function f()\n @async 1 + 1\n @warn \"blah\"\nend\n") + write(io2, """ + function g() + @async 1 + 1 + @info "blah" + end + """) + + flush(io1) + flush(io2) + + str = IOBuffer() + result = ReLint.run_lint(dir; io=str, formatter=ReLint.PreCommitFormat()) + ReLint.print_summary(ReLint.PreCommitFormat(), str, result) + + result = String(take!(str)) + + expected = r""" + Line 3, column 5: Unsafe logging statement\. You must enclose variables and strings with `@safe\(\.\.\.\)`\. \H+/bar.jl + Line 3, column 3: Unsafe logging statement\. You must enclose variables and strings with `@safe\(\.\.\.\)`\. \H+/foo.jl + 4 potential threats are found: 2 fatal violations, 2 violations and 0 recommendation + Note that the list above only show fatal violations + """ + result_matching = !isnothing(match(expected, result)) + end + end + end + @test result_matching + end +end + +@testset "Printing LintReport" begin + using ReLint: LintRuleReport, LintResult, print_report, PreCommitFormat, is_fatal + + result = LintResult() + lint_report = LintRuleReport(ReLint.FATAL_VIOLATIONS["@generated"], "error") + io = IOBuffer() + print_report(PreCommitFormat(), io, lint_report, result) + @test String(take!(io)) == "Line 0, column 0: error \n" +end diff --git a/test/rai_rules_tests.jl b/test/rai_rules_tests.jl deleted file mode 100644 index 31d938b..0000000 --- a/test/rai_rules_tests.jl +++ /dev/null @@ -1,2213 +0,0 @@ -# FOR FUTURE WORK -# @testset "string interpolation" begin -# source = raw"""$(@async 1 + 2)""" -# @test lint_has_error_test(source) -# end - -@testset "forbidden macros" begin - @testset "@async 01" begin - source = """ - function f() - @async 1 + 2 - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 2, column 5: Use `@spawn` instead of `@async`.") - end - - @testset "@async 02" begin - source = """ - function f() - Threads.@async 1 + 2 - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 2, column 5: Use `@spawn` instead of `@async`.") - end - - @testset "@cfunction" begin - source = """ - function f() - @cfunction(_readwrite_cb, Cvoid, (Ptr{Cvoid}, )) - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 2, column 5: Macro `@cfunction` should not be used.") - end - - @testset "@lock, @threads, @generated" begin - source = """ - function mark_transaction_as_database_creation!(kv::SpcsKV, transaction_id::String) - @lock kv.latch begin - push!(kv.create_database_transactions, transaction_id) - end - - - Threads.@threads for (e, v) in slots - @test e[] == v - free_slot!(pool, Blob{Nothing}(e)) - end - return nothing - end - - @generated function _empty_vector(::Type{T}) where {T} - vec = T[] - return vec - end - - try - @sync begin - @spawn_pager_bg_task RAI_PagerCore.add_to_cache!(conf, page) - @span_no_threshold "eot_rootmarker_write_to_blob" write_marker_to_cloud( - conf, page, rootinfo - ) - end - catch - Threads.@sync begin - 1 + 2 - end - nothing - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 7, column 5: `@threads` should be used with extreme caution.") - @test lint_test(source, - "Line 14, column 1: `@generated` should be used with extreme caution.") - @test lint_test(source, - "Line 20, column 5: `@sync` should be used with extreme caution.") - @test lint_test(source, - "Line 27, column 5: `@sync` should be used with extreme caution.") - end -end - -@testset "forbidden functions" begin - @testset "Initializing a const variable with functions" begin - source = """ - const x = Threads.nthreads() - const y = Deployment.is_local_deployment() - const z = is_local_deployment() - const u = foo() # Allowed - - function f() - if Deployment.is_local_deployment() - print("It is all good!") - end - return x + Threads.nthreads() - end - """ - @test count_lint_errors(source) == 3 - @test lint_test(source, - "Line 1, column 11: `Threads.nthreads()` should not be used in a constant variable.") - @test lint_test(source, - "Line 2, column 11: `Deployment.is_local_deployment()` should not be used in a constant variable.") - @test lint_test(source, - "Line 3, column 11: `is_local_deployment()` should not be used in a constant variable.") - end - - @testset "nthreads() not as a const" begin - source = """ - function f() - return Threads.nthreads() - end - """ - @test !lint_has_error_test(source) - end - - @testset "finalizer with do-end" begin - source = """ - function f(x) - ref = Ref(1) - x = ___MutableFoo(ref) - finalizer(x) do x - ref[] = 3 - end - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 4, column 5: `finalizer(_,_)` should not be used.") - - end - - @testset "finalizer without do-end" begin - source = """ - function f(x) - finalizer(q->nothing, x) - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 2, column 5: `finalizer(_,_)` should not be used.") - end - - @testset "finalizer with do-end 02" begin - source = """ - finalizer("hello") do x - println("hello ") - println("world") - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 1, column 1: `finalizer(_,_)` should not be used.") - end - - @testset "ccall" begin - source = """ - function rusage(who:: RUsageWho = RUSAGE_SELF) - ru = Vector{RUsage}(undef, 1) - ccall(:getrusage, Cint, (Cint, Ptr{Cvoid}), who, ru) - ccall(:jl_print_task_backtraces, Cvoid, ()) - return ru[1] - end - """ - @test count_lint_errors(source) == 2 - @test lint_test(source, - "Line 3, column 5: `ccall` should be used with extreme caution.") - @test lint_test(source, - "Line 4, column 5: `ccall` should be used with extreme caution.") - end - - @testset "ccall 02" begin - source = """ - function _pread_async!(fd::Integer, buffer::Ptr{UInt8}, count::Integer, offset::Integer)::UInt64 - uv_filesystem_request, uv_buffer_descriptor = _prepare_libuv_async_call(buffer, count) - - ccall(:uv_fs_read, Int32, - (Ptr{Cvoid}, Ptr{Cvoid}, Int32, Ptr{Cvoid}, UInt32, Int64, Ptr{Cvoid}), - Base.eventloop(), uv_filesystem_request, fd, uv_buffer_descriptor, UInt32(1), offset, - @cfunction(_readwrite_cb, Cvoid, (Ptr{Cvoid}, )) - ) - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 4, column 5: `ccall` should be used with extreme caution.") - end - - @testset "yield, sleep, map, Future, wait" begin - source = """ - function wait_for_cooldown(count::UInt64, counts::HistogramCounts) - while count != @atomic counts.total_observations - yield() - sleep(0.011) - end - end - - function use_of_map() - s = open("/tmp/mmap.bin") - m = read(s, Int) - n = read(s, Int) - A2 = mmap(s, Matrix{Int}, (m,n)) - A3 = Mmap.mmap(s, Matrix{Int}, (m,n)) - fut1 = Future{Any}() do f nothing end - fut2 = Future{Nothing}(()->nothing) - fut3 = Future(()->nothing) - - wait(TaskTreeJoin()) - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 3, column 9: `yield` should be used with extreme caution.") - @test lint_test(source, - "Line 4, column 9: `sleep` should be used with extreme caution.") - end - - @testset "@inbounds, remove_page, Channel, ErrorException" begin - source = """ - function f() - fut = Future{Any}() - r1 = fetch(fut) - - @inbounds begin - at_end(iter) && return 0 - i = 1 - set_from_tuple!(columns_tuple, 1, iter_tuple(iter)) - while next!(iter) && i < cap - i += 1 - set_from_tuple!(columns_tuple, i, iter_tuple(iter)) - end - return i - end - num_created1 = Threads.Atomic{Int}(0); - num_created2 = Atomic{Int}(0); - num_created3 = Atomic(0); - - pointer(page) == Ptr{Nothing}(0) && return - end - - function _clear_pager!(pager) - for (pid, _) in pager.owned_pages - remove_page(pager, pid) - end - end - - function foo() - ch1 = Channel() - ch2 = Channel(10) - - a() = sum(i for i in 1:1000); - b = Task(a); - - e = ErrorException("failure") - return (ch1, ch2) - end - - function bar(x) - throw(ErrorException("My error")) - end - - bar() = error("My fault") - """ - - @test lint_test(source, "Line 5, column 5: `@inbounds` should be used with extreme caution.") - - @test lint_test(source, "Line 24, column 9: `remove_page` should be used with extreme caution.") - - @test lint_test(source, "Line 33, column 9: `Task` should be used with extreme caution.") - - @test lint_test(source, "Line 40, column 11: Use custom exception instead of the generic `ErrorException`") - @test lint_test(source, "Line 43, column 9: Use custom exception instead of the generic `error()`") - - end - - @testset "Array with no specific type 01" begin - source = """ - function f() - x = [] - y = String[] - return vcat(x, y) - end - """ - # No error because this tmp file is not in the src/Compiler - @test !lint_has_error_test(source) - end - - @testset "Array with no specific type 02" begin - source = """ - function f() - x = [] - y = String[] - return vcat(x, y) - end - """ - @test lint_test( - source, - "Line 2, column 9: Need a specific Array type to be provided.", - directory = "src/Compiler/") - end - - @testset "Array with no specific type 03" begin - source = """ - function f() - @matchrule bindings_empty() = - Bindings([], _::Missing) => CoreBindings([]) - - @matchrule and_to_true() = - And([], annos) => BoolConstant(true, annos) - - @matchrule and_singleton() = - And([f], annos) => f - - @match CoreRelAbstract(bs2, [], as2) = e2 - - @matchrule and_to_true() = - And([], annos) => BoolConstant(true, annos) - - @matchrule exists_empty() = - Exists(e, _) where is_definitely_empty_expr(e) => - slice_to_false(input_val) - f = [] - end - """ - count_errors = count_lint_errors(source; directory = "src/Compiler/") - @test count_errors == 1 - @test lint_test( - source, - "Line 19, column 9: Need a specific Array type to be provided."; - directory = "src/Compiler") - end - - @testset "in, equal, haskey, uv_" begin - source = """ - function f() - x = 10 in [10] - y = in(10, [10]) - z = equal(10, "hello") - w = haskey(Dict(1=>1000), 1) - a = uv_foo(10, 20) - b = ∈(10, [10]) - c = 10 ∈ [10] - - # No error - d = [x for x in 1:10] - groupby_vars = [v for v in child_vars if !tin(v, sort_input.vars)] - end - """ - @test count_lint_errors(source) == 7 - @test lint_test(source, - "Line 2, column 9: Use `tin(item,collection)` instead of the Julia's `in`") - @test lint_test(source, - "Line 3, column 9: Use `tin(item,collection)` instead of the Julia's `in`") - @test lint_test(source, - "Line 4, column 9: Use `tequal(dict,key)` instead of the Julia's `equal`.") - @test lint_test(source, - "Line 5, column 9: Use `thaskey(dict,key)` instead of the Julia's `haskey`.") - @test lint_test(source, - "Line 6, column 9: `uv_` functions should be used with extreme caution.") - @test lint_test(source, - "Line 7, column 9: Use `tin(item,collection)` instead of the Julia's `in` or `∈`.") - @test lint_test(source, - "Line 8, column 9: Use `tin(item,collection)` instead of the Julia's `in` or `∈`.") - end - - @testset "Splatting" begin - @test lint_test("f(x...) + 10", - "Line 1, column 1: Splatting (`...`) should be used with extreme caution. Splatting from dynamically sized containers could result in severe performance degradation.") - - @test lint_test("hcat([f(x) for x in r]...)", - "Line 1, column 1: Splatting (`...`) should not be used with dynamically sized containers. This may result in performance degradation. See https://github.com/RelationalAI/RAIStyle#splatting for more information.") - - source = raw"""macro infov(verbosity::Int64, msg, exs...) - return quote - if $(esc(:($DebugLevels.@should_emit_log($Logging.Info, $verbosity)))) - $(Base.CoreLogging.logmsg_code((Base.CoreLogging.@_sourceinfo)..., :Info, msg, :(verbosity=$verbosity), exs...)) - end - end - """ - - @test count_lint_errors(source) == 0 - - @test count_lint_errors("""macro foo(x...)\nzork(x...)\nend""") == 0 - end -end - -@testset "Comparing AST to templates" begin - t(s1, s2) = comp(CSTParser.parse(s1), CSTParser.parse(s2)) - @test t("Threads.nthreads()", "Threads.nthreads()") - @test !t("QWEThreads.nthreads()", "Threads.nthreads()") - @test !t("Threads.nthreads()", "QWEThreads.nthreads()") - @test !t("Threads.nthreads()", "Threads.qwenthreads()") - - @test t("1 + 2", "1+2") - @test t("1 + 2", "1+hole_variable") - @test t("hole_variable + hole_variable", "1 + hole_variable") - @test t("hole_variable + 1", "1 + hole_variable") - - @test t("@async hole_variable", "@async begin 1 + 2 end") - - @test t("finalizer(y, x)", "finalizer(hole_variable, hole_variable)") - @test t("finalizer(q->nothing, x)", "finalizer(hole_variable, hole_variable)") - @test t("finalizer(x) do hole_variable hole_variable end", - "finalizer(x) do x - ref[] = 3 - end") - - @test !t("foo()", "foo(hole_variable)") - @test !t("foo()", "foo(x)") - - @test t("foo(x, y)", "foo(hole_variable, hole_variable)") - @test !t("foo(x, y, z)", "foo(hole_variable, hole_variable)") - @test t("foo(x, y, z)", "foo(hole_variable, hole_variable_star)") - - @test t("foo(x, y, z)", "foo(hole_variable_star)") - @test t("foo()", "foo(hole_variable_star)") - - @test t("Semaphore(10)", "Semaphore(hole_variable)") - # Ideally, the next line should pass. - # @test t("foo(x, y, z)", "foo(hole_variable, hole_variable, hole_variable, hole_variable_star)") - - source = """ - finalizer("hello") do x - println("hello ") - end - """ - @test t(source, "finalizer(hole_variable) do hole_variable hole_variable_star end") - - @test t("unlock(hole_variable)", "unlock(12)") - - # Generics - @test t("Future{Nothing}()", "Future{hole_variable}()") - @test t("Future{T}()", "Future{hole_variable}(hole_variable_star)") - @test t("Future{T}(()->nothing)", "Future{hole_variable}(hole_variable_star)") - @test !t("Future()", "Future{hole_variable}(hole_variable_star)") - @test !t("Future{Any}() do f nothing end", "Future{hole_variable}(hole_variable_star)") - - # Partial matching - @test t("foo", "foo") - @test t("fooQQQ", "foo") - @test t("QQQfoo", "foo") - @test t("QQQfooQQQ", "foo") - - @test !t("foo", "foobar") - @test t("fooQQQ", "foobar") - @test t("QQQfoo", "barfoo") - @test t("QQQfooQQQ", "barfoozork") - - @test !t("foo", "foo_bar") - @test t("fooQQQ", "foo_bar") - @test t("QQQfoo", "bar_foo") - @test t("QQQfooQQQ", "bar_foo_zork") - - @test t("foo(x, QQQzork)", "foo(x, zork)") - @test t("foo(x, QQQzork)", "foo(x, blah_zork)") - - # in keyword - @test t("in(hole_variable,hole_variable)", "in(x,y)") - @test t("x in y", "hole_variable in hole_variable") - - # Splatting - @test t("f(a...)", "hole_variable(hole_variable_star...)") - @test t("hcat([f(x) for x in r]...)", "hole_variable([hole_variable(hole_variable_star) for hole_variable in hole_variable]...)") - - # Named variable holes - @test t("1 + 2", "1+hole_variableA") - @test t("1 + 2", "1+hole_variableB") - @test t("1 + 2 + 2", "1+hole_variableA+hole_variableA") - @test !t("1 + 2 + 2", "1+hole_variableA+hole_variableB") - @test t("1 + 2 + 3", "1+hole_variableA+hole_variableB") - @test t("1 + 2 + 3 + 2", "1+hole_variableA+hole_variable +hole_variableA") - @test t("1 + 2 + 3 + 2 + 10", "1+hole_variableA+hole_variable +hole_variableA + hole_variable_star") - - @test !t(""" - if x == 1 - return 12 - elseif x== 2 - return "Reachable_branch" - end - """, """ - if hole_variableA == hole_variableB - hole_variable - elseif hole_variableA == hole_variableB - hole_variable - end - """) - - @test t(""" - if x == 1 - return 12 - elseif x== 1 - return "Reachable_branch" - end - """, """ - if hole_variableA == hole_variableB - hole_variable - elseif hole_variableA == hole_variableB - hole_variable - end - """) - - - @test t(""" - if x == 1 - println("hello") - return 12 - elseif x== 1 - println("world") - return "Reachable_branch" - end - """, """ - if hole_variableA == hole_variableB - hole_variable - elseif hole_variableA == hole_variableB - hole_variable - end - """) - - # LINT_STRING - @test t("\"LINT_STRING\"", "\"this is a string\"") - @test !t("\"LINT_STRING\"", "1 + 2") - @test t("\"this is a string\"", "\"LINT_STRING\"") - @test !t("1 + 2", "\"LINT_STRING\"") - @test t("\"1 + 2\"", "\"LINT_STRING\"") - - # LINT_STRING_WITH_INTERPOLATION - @test !t("\"1 + 2\"", "\"LINT_STRING_WITH_INTERPOLATION\"") - @test t(raw"\"foo $x\"", "\"LINT_STRING_WITH_INTERPOLATION\"") - @test t(raw"@warnv_safe_to_log 1 \"logged! $(a_variable)\"", "@warnv_safe_to_log hole_variable \"LINT_STRING_WITH_INTERPOLATION\"") - @test !t(raw"@warnv_safe_to_log 1 \"logged! (a_variable)\"", "@warnv_safe_to_log hole_variable \"LINT_STRING_WITH_INTERPOLATION\"") - - # @test t(raw"\"($x)\"", "\"LINT_STRING\"") - -end - -@testset "unsafe functions" begin - @testset "No error 01" begin - source = """ - function unsafe_f() - unsafe_g() - end - - function unsafe_g() - return 42 - end - """ - @test !lint_has_error_test(source) - end - - @testset "Some errors 01" begin - source = """ - function f() - unsafe_g() - end - - function unsafe_g() - return 42 - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 2, column 5: An `unsafe_` function should be called only from an `unsafe_` function.") - end - - @testset "Some errors 02" begin - source = """ - function f() - _unsafe_g() - end - - function _unsafe_g() - return 42 - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 2, column 5: An `unsafe_` function should be called only from an `unsafe_` function.") - end - - @testset "No error 02" begin - source = """ - function f() - # lint-disable-next-line - _unsafe_g() - end - - function _unsafe_g() - return 42 - end - """ - @test !lint_has_error_test(source) - end -end - -@testset "offset to line" begin - source = """ - function f() - return Threads.nthreads() - end - """ - @test_throws BoundsError convert_offset_to_line(-1, source) - @test_throws BoundsError convert_offset_to_line(length(source) + 2, source) - - @test convert_offset_to_line(10, source) == (1, 10, nothing) - @test convert_offset_to_line(20, source) == (2, 7, nothing) - @test convert_offset_to_line(42, source) == (2, 29, nothing) - @test convert_offset_to_line(46, source) == (3, 3, nothing) - -end - -@testset "Fetching values from AST" begin - @test fetch_value(CSTParser.parse("f"), :IDENTIFIER) == "f" - @test fetch_value(CSTParser.parse("f()"), :IDENTIFIER) == "f" - @test fetch_value(CSTParser.parse("begin f(g()) end"), :IDENTIFIER) == "f" - - source = """ - struct _SyncDict{Dict} - lock::Base.Threads.SpinLock - dict::Dict - - function _SyncDict{Dict}() where {Dict} - new{Dict}(Base.Threads.SpinLock(), Dict()) - end - end - """ - @test fetch_value(CSTParser.parse(source), :IDENTIFIER) == "_SyncDict" - -end - -@testset "Formatter" begin - source = """ - const x = Threads.nthreads() - function f() - return x - end - """ - - @testset "Plain 02" begin - io = IOBuffer() - run_lint_on_text(source; io=io) - result = String(take!(io)) - - expected = r""" - ---------- \H+ - Line 1, column 11: `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - 1 potential threat is found: 0 fatal violation, 1 violation and 0 recommendation - ---------- - """ - @test !isnothing(match(expected, result)) - end - - @testset "Markdown 02" begin - io = IOBuffer() - run_lint_on_text(source; io=io, formatter=MarkdownFormat()) - result = String(take!(io)) - - expected = r""" - - \*\*Line 1, column 11:\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - """ - @test !isnothing(match(expected, result)) - end - - @testset "Markdown 03 - with github information" begin - formatter = MarkdownFormat("axb-example-with-lint-errors", "RelationalAI/raicode") - io = IOBuffer() - - run_lint_on_text( - source; - io, - formatter, - directory="/src/Compiler/") - result = String(take!(io)) - - expected = r""" - - \*\*\[Line 1, column 11:\]\(https://github\.com/RelationalAI/raicode/blob/axb-example-with-lint-errors/\H+/src/Compiler/tmp_julia_file\.jl#L1\)\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - """ - @test !isnothing(match(expected, result)) - end - - @testset "Markdown 04 - with github information" begin - formatter = MarkdownFormat("axb-example-with-lint-errors", "RelationalAI/raicode") - io = IOBuffer() - run_lint_on_text( - source; - io, - formatter, - directory="src/Compiler/") - result = String(take!(io)) - expected = r""" - - \*\*\[Line 1, column 11:\]\(https://github\.com/RelationalAI/raicode/blob/axb-example-with-lint-errors/\H+/src/Compiler/tmp_julia_file\.jl#L1\)\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - """ - @test !isnothing(match(expected, result)) - end -end - -@testset "Run several times on same file" begin - mktempdir() do dir - open(joinpath(dir, "foo.jl"), "w") do io - write(io, "function f()\n @async 1 + 1\nend\n") - flush(io) - - @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) - @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) - @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) - @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) - end - end -end - -@testset "Linting multiple files" begin - @testset "No errors" begin - local result_is_empty = false - mktempdir() do dir - open(joinpath(dir, "foo.jl"), "w") do io1 - open(joinpath(dir, "bar.jl"), "w") do io2 - write(io1, "function f()\n @spawn 1 + 1\nend\n") - write(io2, "function g()\n @spawn 1 + 1\nend\n") - - flush(io1) - flush(io2) - - str = IOBuffer() - ReLint.run_lint(dir; io=str, formatter=ReLint.MarkdownFormat()) - result = String(take!(str)) - result_is_empty = isempty(result) - end - end - end - @test result_is_empty - end - - @testset "Two files with errors" begin - local result_matching = false - mktempdir() do dir - open(joinpath(dir, "foo.jl"), "w") do io1 - open(joinpath(dir, "bar.jl"), "w") do io2 - write(io1, "function f()\n @async 1 + 1\nend\n") - write(io2, "function g()\n @async 1 + 1\nend\n") - - flush(io1) - flush(io2) - - str = IOBuffer() - ReLint.run_lint(dir; io=str, formatter=ReLint.MarkdownFormat()) - - result = String(take!(str)) - - expected = r""" - - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - """ - result_matching = !isnothing(match(expected, result)) - end - end - end - @test result_matching - end - - @testset "No splatting warning in tests" begin - local result_matching = false - mktempdir() do dir - open(joinpath(dir, "foo.jl"), "w") do io1 - open(joinpath(dir, "bar_test.jl"), "w") do io2 - write(io1, "function f()\n foo(x...)\nend\n") - write(io2, "\n\nfunction g()\n foo(x...)\nend\n") - - flush(io1) - flush(io2) - - str = IOBuffer() - ReLint.run_lint(dir; io=str, formatter=ReLint.MarkdownFormat()) - - result = String(take!(str)) - - expected = " - **Line 2, column 3:** Splatting (`...`) should be used with extreme caution." - result_matching = startswith(result, expected) - result_matching = result_matching && !contains(result, "bar_test.jl") - end - end - end - @test result_matching - end - - @testset "Report generation of two files with errors" begin - local result_matching = false - mktempdir() do dir - file1 = joinpath(dir, "foo.jl") - file2 = joinpath(dir, "bar.jl") - open(file1, "w") do io1 - open(file2, "w") do io2 - write(io1, "function f()\n @async 1 + 1\nend\n") - write(io2, "function g()\n finalizer(\"hello\") do x nothing\nend\n") - - flush(io1) - flush(io2) - - output_file = tempname() - json_output = IOBuffer() - stream_workflowcommand = IOBuffer() - ReLint.generate_report([file1, file2], output_file; json_output, stream_workflowcommand) - - # Checking the Workflow command - stream_workflowcommand_report = String(take!(stream_workflowcommand)) - wc_lines = split(stream_workflowcommand_report, "\n") - @test length(wc_lines) == 3 - @test isempty(wc_lines[end]) - @test contains(wc_lines[1], "foo.jl,line=2,col=3::Use `@spawn` instead of `@async`.") - - # Checking the JSON - json_report = JSON3.read(String(take!(json_output))) - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 2 - - @test json_report[:data][:violation_count] == 1 - @test json_report[:data][:recommandation_count] == 1 - @test json_report[:data][:fatalviolations_count] == 0 - - local result - open(output_file) do oo - result = read(oo, String) - end - - # First violations across files, then recommendations across files - expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* - Report creation time \(UTC\): \H+ - - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - - -
- For PR Reviewer \(1\) - - - \*\*Line 2, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+\ - -
- - 🚨\*\*In total, 0 fatal rule violation, 1 rule violation and 1 PR reviewer recommendation are found over 2 Julia files\*\*🚨 - """ - result_matching = !isnothing(match(expected, result)) - # DEBUG: - !result_matching && @info result - end - end - end - @test result_matching - end - - @testset "Report generation of two files with errors 02" begin - local result_matching = false - mktempdir() do dir - file1 = joinpath(dir, "foo.jl") - file2 = joinpath(dir, "bar.jl") - open(file1, "w") do io1 - open(file2, "w") do io2 - write(io1, "function g()\n @async 1 + 1\nend\n finalizer(\"hello\") do x nothing\nend\n") - write(io2, "function f()\n @async 1 + 1\nend\n finalizer(\"hello\") do x nothing\nend\n") - - flush(io1) - flush(io2) - - output_file = tempname() - json_output = IOBuffer() - ReLint.generate_report([file1, file2], output_file; json_output, stream_workflowcommand=devnull) - - json_report = JSON3.read(String(take!(json_output))) - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 2 - - @test json_report[:data][:recommandation_count] == 2 - @test json_report[:data][:violation_count] == 2 - - local result - open(output_file) do oo - result = read(oo, String) - end - - # First violations across files, then recommendations across files - expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* - Report creation time \(UTC\): \H+ - - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - -
- For PR Reviewer \(2\) - - - \*\*Line 4, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+ - - \*\*Line 4, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+ - -
- - 🚨\*\*In total, 0 fatal rule violation, 2 rule violations and 2 PR reviewer recommendations are found over 2 Julia files\*\*🚨 - """ - result_matching = !isnothing(match(expected, result)) - # DEBUG: - !result_matching && @info result - end - end - end - @test result_matching - end - - @testset "Report generation of two files with errors 02 - JSON report" begin - local result_matching = false - mktempdir() do dir - file1 = joinpath(dir, "foo.jl") - file2 = joinpath(dir, "bar.jl") - open(file1, "w") do io1 - open(file2, "w") do io2 - write(io1, "function g()\n @async 1 + 1\nend\n finalizer(\"hello\") do x nothing\nend\n") - write(io2, "function f()\n @async 1 + 1\nend\n finalizer(\"hello\") do x nothing\nend\n") - - flush(io1) - flush(io2) - - output_file = tempname() - json_filename = tempname() - @test !isfile(json_filename) - # json_io = IOBuffer() - ReLint.generate_report([file1, file2], output_file; json_filename, stream_workflowcommand=devnull) - - @test isfile(json_filename) - json_content = open(io->read(io, String), json_filename) - json_report = JSON3.read(json_content) - - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 2 - - @test json_report[:data][:recommandation_count] == 2 - @test json_report[:data][:violation_count] == 2 - - local result - open(output_file) do oo - result = read(oo, String) - end - - # First violations across files, then recommendations across files - expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* - Report creation time \(UTC\): \H+ - - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - -
- For PR Reviewer \(2\) - - - \*\*Line 4, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+ - - \*\*Line 4, column 3:\*\* `finalizer\(_,_\)` should not be used\. \H+ - -
- - 🚨\*\*In total, 0 fatal rule violation, 2 rule violations and 2 PR reviewer recommendations are found over 2 Julia files\*\*🚨 - """ - result_matching = !isnothing(match(expected, result)) - # DEBUG: - !result_matching && @info result - end - end - end - @test result_matching - end - - @testset "No modified julia file" begin - output_file = tempname() - json_output = IOBuffer() - ReLint.generate_report(String[], output_file; json_output, stream_workflowcommand=devnull) - - json_report = JSON3.read(String(take!(json_output))) - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 0 - - @test json_report[:data][:recommandation_count] == 0 - @test json_report[:data][:violation_count] == 0 - - - expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* - Report creation time \(UTC\): \H+ - No Julia file is modified or added in this PR. - """ - result = open(io->read(io, String), output_file) - - result_matching = !isnothing(match(expected, result)) - @test result_matching - end - - @testset "No result with no julia file" begin - local result_matching = false - mktempdir() do dir - file1 = joinpath(dir, "foo.txt") - open(file1, "w") do io1 - write(io1, "Hello World\n") - flush(io1) - - output_file = tempname() - json_output = IOBuffer() - ReLint.generate_report([file1], output_file; json_output, stream_workflowcommand=devnull) - - json_report = JSON3.read(String(take!(json_output))) - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 0 - @test json_report[:data][:recommandation_count] == 0 - @test json_report[:data][:violation_count] == 0 - - local result - open(output_file) do oo - result = read(oo, String) - end - - - expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* - Report creation time \(UTC\): \H+ - No Julia file is modified or added in this PR. - """ - result_matching = !isnothing(match(expected, result)) - end - end - @test result_matching - end - - @testset "Report generation of two files with no errors" begin - local result_matching = false - mktempdir() do dir - file1 = joinpath(dir, "foo.jl") - file2 = joinpath(dir, "bar.jl") - open(file1, "w") do io1 - open(file2, "w") do io2 - write(io1, "function f()\n @spawn 1 + 1\nend\n") - write(io2, "function g()\n @spawn 1 + 1\nend\n") - - flush(io1) - flush(io2) - - output_file = tempname() - json_output = IOBuffer() - ReLint.generate_report([file1, file2], output_file; json_output, stream_workflowcommand=devnull) - - json_report = JSON3.read(String(take!(json_output))) - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 2 - @test json_report[:data][:recommandation_count] == 0 - @test json_report[:data][:violation_count] == 0 - - local result - open(output_file) do oo - result = read(oo, String) - end - - expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* - Report creation time \(UTC\): \H+ - 🎉No potential threats are found over 2 Julia files.👍 - """ - result_matching = !isnothing(match(expected, result)) - end - end - end - @test result_matching - end - - @testset "Report generation of 1 file with no errors" begin - local result_matching = false - mktempdir() do dir - file1 = joinpath(dir, "foo.jl") - open(file1, "w") do io1 - write(io1, "function f()\n @spawn 1 + 1\nend\n") - flush(io1) - - output_file = tempname() - json_output = IOBuffer() - ReLint.generate_report([file1], output_file; json_output, stream_workflowcommand=devnull) - - json_report = JSON3.read(String(take!(json_output))) - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 1 - - @test json_report[:data][:recommandation_count] == 0 - @test json_report[:data][:violation_count] == 0 - - local result - open(output_file) do oo - result = read(oo, String) - end - - expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* - Report creation time \(UTC\): \H+ - 🎉No potential threats are found over 1 Julia file.👍 - """ - result_matching = !isnothing(match(expected, result)) - end - end - @test result_matching - end - - @testset "Report generation of 1 file with 1 error and github info" begin - mktempdir() do dir - file1 = joinpath(dir, "foo.jl") - open(file1, "w") do io1 - write(io1, "function f()\n @async 1 + 1\nend\n") - flush(io1) - - output_file = tempname() - json_io = IOBuffer() - ReLint.generate_report( - [file1], - output_file; - json_output=json_io, - github_repository="RelationalAI/raicode", - branch_name="axb-foo-bar", - file_prefix_to_remove="var/", - stream_workflowcommand=devnull) - - json_report = JSON3.read(String(take!(json_io))) - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 1 - - @test json_report[:data][:violation_count] == 1 - @test json_report[:data][:recommandation_count] == 0 - - local result - open(output_file) do oo - result = read(oo, String) - end - - # Remove the first folder to address an issue of GitHub Action - # (See MarkdownFormat for more information) - corrected_file_name = ReLint.remove_prefix_from_filename(file1, "var/") - - expected = " - **[Line 2, column 3:]" * - "(https://github.com/RelationalAI/raicode/blob/axb-foo-bar/$(corrected_file_name)" * - "#L2)** Use `@spawn` instead of `@async`." - if !occursin(expected, result) - @info "didn't match" expected result - end - @test occursin(expected, result) - end - end - end - - @testset "Report generation of all the folder" begin - # This is a slow test - local result_matching = false - mktempdir() do dir - file1 = joinpath(dir, "foo.jl") - open(file1, "w") do io1 - write(io1, "function f()\n @async 1 + 1\nend\n") - flush(io1) - - output_file = tempname() - json_io = IOBuffer() - ReLint.generate_report( - [file1], # Ignored because of analyze_all_file_found_locally - output_file; - json_output=json_io, - github_repository="RelationalAI/raicode", - branch_name="axb-foo-bar", - file_prefix_to_remove="var/", - analyze_all_file_found_locally=true, # OVERRIDE THE PROVIDED SET OF FILES - stream_workflowcommand=devnull - ) - - json_report = JSON3.read(String(take!(json_io))) - - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] >= 2 - @test json_report[:data][:violation_count] >= 0 - @test json_report[:data][:recommandation_count] >= 0 - @test json_report[:data][:fatalviolations_count] >= 0 - - local result - open(output_file) do oo - result = read(oo, String) - end - last_line = filter(!isempty, split(result, "\n"))[end] - @test last_line != "No Julia file is modified or added in this PR." - end - end - end - - @testset "Limiting report" begin - # this tests create a Julia file with 100 violations, the report should mention - # 100 violations, however only (an arbitrary) 30 are reported. - local result_matching = false - mktempdir() do dir - file1 = joinpath(dir, "foo.jl") - open(file1, "w") do io1 - write(io1, "function f()\n") - for _ in 1:100 - write(io1, " @async 1 + 1\n") - end - write(io1, "end\n") - flush(io1) - - output_file = tempname() - json_output = IOBuffer() - ReLint.generate_report([file1], output_file; json_output, stream_workflowcommand=devnull) - - json_report = JSON3.read(String(take!(json_output))) - @test json_report[:source] == "ReLint" - @test json_report[:data][:files_count] == 1 - @test json_report[:data][:recommandation_count] == 0 - @test json_report[:data][:violation_count] == 100 - - local result - open(output_file) do oo - result = read(oo, String) - end - all_lines = split(result, "\n") - lines_count = length(all_lines) - @test lines_count < 70 - - @test all_lines[end-3] == "⚠️Only a subset of the violations and recommandations are here reported⚠️" - @test all_lines[end-2] == "🚨**In total, 0 fatal rule violation, 100 rule violations and 0 PR reviewer recommendation are found over 1 Julia file**🚨" - @test all_lines[end] == "" - end - end - end - - @testset "Diamond between files" begin - mktempdir() do dir - open(joinpath(dir, "leaf.jl"), "w") do io - write(io, "function f()\n @async 1 + 1\nend\n") - end - - open(joinpath(dir, "bar.jl"), "w") do io - write(io, "include(\"leaf.jl\")\n") - end - - str = IOBuffer() - result = ReLint.run_lint(dir; io=str, formatter=ReLint.MarkdownFormat()) - @test result.files_count == 2 - @test result.violations_count == 1 - @test result.recommendations_count == 0 - end - end -end - -@testset "Running on a directory" begin - @testset "Non empty directory" begin - local r - r = LintResult() - - formatters = [ReLint.PlainFormat(), ReLint.MarkdownFormat()] - for formatter in formatters - mktempdir() do dir - open(joinpath(dir, "foo.jl"), "w") do io - write(io, "function f()\n @async 1 + 1\nend\n") - flush(io) - str = IOBuffer() - append!(r, ReLint.run_lint(dir; io=str, formatter)) - end - end - end - @test (r.violations_count + r.recommendations_count) == 2 - end - - @testset "Empty directory" begin - mktempdir() do dir - @test ReLint.run_lint(dir) == LintResult() - end - end -end - -@testset "Locally disabling lint" begin - @testset "lint-disable-lint" begin - @test convert_offset_to_line(17, """ - function f() - @async 1 + 2 # lint-disable-line - end - """) == (2, 4, "lint-disable-line") - @test !lint_has_error_test(""" - function f() - @async 1 + 2 # lint-disable-line - end - """) - @test !lint_has_error_test(""" - function f() - @async 1 + 2 # lint-disable-line: Use `@spawn` instead of `@async`. - end - """) - @test !lint_has_error_test(""" - function f() - @async 1 + 2 #lint-disable-line: Use `@spawn` instead of `@async`. - end - """) - @test !lint_has_error_test(""" - function f() - @async 1 + 2 #lint-disable-line:Use `@spawn` instead of `@async`. - end - """) - @test lint_has_error_test(""" - function f() - @async 1 + 2 #lint-disable-line: not working `@spawn` instead of `@async`. - end - """) - @test !lint_has_error_test(""" - function f() - @async 1 + 2 #lint-disable-line - end - """) - - @test !lint_has_error_test(""" - function f() - @async 1 + 2 # lint-disable-line - end - """) - - @test lint_has_error_test(""" - function f() - @async 1 + 2 # lint-disable-line - @async 1 + 3 - end - """) - end - @testset "lint-disable-next-line" begin - @test !lint_has_error_test(""" - function f() - # lint-disable-next-line - @async 1 + 2 - end - """) - @test !lint_has_error_test(""" - function f() - # lint-disable-next-line - @async 1 + 2 - end - """) - - @test !lint_has_error_test(""" - function f() - # lint-disable-next-line - @async 1 + 2 - end - """) - - @test lint_has_error_test(""" - function f() - # lint-disable-next-line - @async 1 + 2 - - @async 1 + 3 - end - """) - @test lint_has_error_test(""" - function f() - @async 1 + 2 - # lint-disable-next-line - - @async 1 + 3 - end - """) - @test lint_has_error_test(""" - function f() - @async 1 + 2 - # lint-disable-next-line - @async 1 + 3 - end - """) - - source = """ - function f() - # lint-disable-next-line - @async 1 + 2 - - @async 1 + 3 - end - """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(46, source_lines) == (3, 4, "lint-disable-line") - @test convert_offset_to_line_from_lines(64, source_lines) == (5, 4, nothing) - end - - @testset "Locally disabling rule 01" begin - source = """ - function f() - # lint-disable-next-line: Use `@spawn` instead of `@async`. - @async 1 + 1 - end - """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) - @test convert_offset_to_line_from_lines(91, source_lines) == (3, 14, "lint-disable-line: Use `@spawn` instead of `@async`.") - - @test !lint_has_error_test(source) - end - - @testset "Locally disabling rule 02" begin - source = """ - function f() - # lint-disable-next-line: Use `@spawn` instead of `@async`. - @async unsafe_foo(12) - end - """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) - @test convert_offset_to_line_from_lines(91, source_lines) == (3, 14, "lint-disable-line: Use `@spawn` instead of `@async`.") - - @test lint_has_error_test(source) - @test lint_test(source, - "Line 3, column 12: An `unsafe_` function should be called only from an `unsafe_` function.") - end - - @testset "Locally disabling rule 03" begin - source = """ - function f() - # lint-disable-next-line: An `unsafe_` function - @async unsafe_foo(42) - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 3, column 5: Use `@spawn` instead of `@async`.") - end - - @testset "Locally disabling rule 04" begin - source = """ - function f() - # lint-disable-next-line:Use `@spawn` instead of `@async`. - @async 1 + 1 - end - """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) - @test convert_offset_to_line_from_lines(91, source_lines) == (3, 15, "lint-disable-line: Use `@spawn` instead of `@async`.") - - @test !lint_has_error_test(source) - end - - @testset "Locally disabling rule 05" begin - source = """ - function f() - # lint-disable-next-line: Use `@spawn` instead of `@async`. - @async 1 + 1 - end - """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) - @test convert_offset_to_line_from_lines(91, source_lines) == (3, 13, "lint-disable-line: Use `@spawn` instead of `@async`.") - - @test !lint_has_error_test(source) - end -end - -@testset "Relaxing unused bindings" begin - # @test lint_test(""" - # function f(a::Int64, b, c) - # local x - # return 42 - # end - # """, "Line 2, column 11: Variable has been assigned but not used") - - @test !lint_has_error_test(""" - function f(a::Int64, b, c) - local _ - return 42 - end - """) - - @test !lint_has_error_test(""" - function f(a::Int64, b, c) - local _x - return 42 - end - """) -end - -# This rule is disabled per default. To add it, need to modify reset_recommentation_dict!() -# @testset "IncorrectCallArgs" begin -# source = """ -# f(x) = 1 -# f(1, 2) -# """ -# @test lint_test(source, "Line 2, column 1: Possible method call error: f.") -# end - -@testset "Branch" begin - @testset "Reachable branches" begin - source = """ - function f(x) - if x == 1 - return 12 - elseif x== 2 - return "Reachable_branch" - end - end - """ - @test !lint_has_error_test(source) - end - - @testset "Unreachable branches" begin - source = """ - function f(x) - if x == 1 - return 12 - elseif x== 1 - return "Unreachable_branch" - end - end - """ - @test lint_test(source, "Line 2, column 5: Unreachable branch.") - end - - @testset "Unreachable branches 02" begin - source = """ - function f(x) - if x <= 1 - return 12 - elseif x <= 1 - return "Unreachable_branch" - end - end - """ - @test lint_test(source, "Line 2, column 5: Unreachable branch.") - end -end - -@testset "Resetting ReLint caches" begin - source = """ - function f() - @async 1 + 1 - end - function g() - @lock Lock() begin - 1 + 1 - end - end - """ - - run_lint_on_text(source; io=IOBuffer()) - - @test !isempty(ReLint.check_cache) - ReLint.reset_static_lint_caches() - @test isempty(ReLint.check_cache) -end - -@testset "Recommentation separated from violations" begin - source = """ - function f() - @async 1 + 1 - end - function g() - @lock Lock() begin - 1 + 1 - end - end - """ - io=IOBuffer() - run_lint_on_text(source; io) - - result = String(take!(io)) - expected = r""" - ---------- \H+ - Line 2, column 5: Use `@spawn` instead of `@async`\. \H+ - 1 potential threat is found: 0 fatal violation, 1 violation and 0 recommendation - ---------- - """ - @test !isnothing(match(expected, result)) -end - -@testset "Checking string interpolation" begin - - # ERRORS - source_with_error = raw""" - function f(conf) - @INFO "($conf.container.baseurl)" - end - """ - - source_with_error2 = raw""" - function f(conf) - @INFO "$conf.container.baseurl" - end - """ - - source_with_error3 = raw""" - function f(conf) - @INFO "this string contains an error $conf.container.baseurl indeed!" - end - """ - - source_with_error4 = raw""" - function f(conf) - @INFO "this string contains an error $conf .container.baseurl indeed!" - end - """ - - source_with_error5 = raw""" - function f(engine_name) - @INFO "Issuing delete request for engine $engine_name..." - end - """ - - source_with_error6 = raw""" - function f() - Source("model/$name", "model/$name", read(joinpath(@__DIR__, "models", "$name.rel"), String)) - end - """ - - source_with_error7 = raw""" - function f() - path = "$dir/$name.csv" - end - """ - - @test lint_test(source_with_error, raw"Line 2, column 11: Use $(x) instead of $x ") - @test lint_test(source_with_error2, raw"Line 2, column 11: Use $(x) instead of $x ") - @test lint_test(source_with_error3, raw"Line 2, column 11: Use $(x) instead of $x ") - @test lint_test(source_with_error4, raw"Line 2, column 11: Use $(x) instead of $x ") - @test lint_test(source_with_error5, raw"Line 2, column 11: Use $(x) instead of $x ") - - @test lint_test(source_with_error6, raw"Line 2, column 12: Use $(x) instead of $x ") - @test lint_test(source_with_error6, raw"Line 2, column 27: Use $(x) instead of $x ") - @test lint_test(source_with_error6, raw"Line 2, column 77: Use $(x) instead of $x ") - - @test lint_test(source_with_error7, raw"Line 2, column 12: Use $(x) instead of $x ") - - # NO ERROR - source_without_error = raw""" - function f(conf) - @INFO "$(conf.container.baseurl)" - end - """ - - source_without_error2 = raw""" - function f(conf) - @INFO "this string contains an error $(conf.container.baseurl) indeed!" - end - """ - - source_without_error3 = raw""" - function f() - _profile_filename = "profile-$(timestamp).pb.gz" - end - """ - - @test count_lint_errors(source_without_error) == 0 - @test count_lint_errors(source_without_error2) == 0 - @test count_lint_errors(source_without_error3) == 0 -end - -@testset "Arithmetic LintResult" begin - l1 = LintResult() - l2 = LintResult(1, 2, 3) - l3 = LintResult(10, 20, 30) - l6 = LintResult(10, 20, 30, 40) - l4 = LintResult(10, 20, 30, 40, ["foo.jl"], 100, []) - l5 = LintResult(10, 20, 30, 40, ["foo2.jl"], 250) - - - @test l1 == l1 - @test l1 == LintResult() - # @test (l1 + l2) == l2 - # @test (l3 + l2) == LintResult(11, 22, 33) - @test l4 != l5 - @test l3 != l4 - @test l3 != l5 - - append!(l4, l5) - @test l4 == LintResult(20, 40, 60, 80, ["foo.jl", "foo2.jl"], 350) -end - -@testset "RelPath Front-End" begin - source = """ - function rel_sig_from_relpath(path) - (name, types) = split_path(path) - return RelationSignature(name, types.elements) - end - - function interpret(x, y, path) - rest = drop_first(path) - return RelPath(rest.elements[2:end]) - end - - function reverse(decl::EdbDecl) - return relpath_from_signature(decl.signature) - end - - function use_path(x, y::RelPath, z) - return y.elements - end - """ - - @test count_lint_errors(source; directory="/src/Compiler/Front") == 5 - @test count_lint_errors(source; directory="") == 0 - - @test lint_test(source, - "Line 2, column 25: Usage of `RelPath` API method `split_path` is not allowed in this context."; - directory="/src/Compiler/Front" - ) -end - -# @testset "Check on @warnv_safe_to_log" begin -# source = raw""" -# function pm_check_mutable_pages(bytes::Int) -# pm = PAGER_MONITOR - -# max_pages = (@atomic pm.mutable_pages_running_max) -# max_bytes = (@atomic pm.mutable_bytes_running_max) -# if max_bytes >= bytes -# @warnv_safe_to_log 0 "[Pager] Too many mutable pages \ -# detected: $max_pages pages weighting $max_bytes bytes" - -# @ensure @warnv_safe_to_log 2 "[Pager] Too many mutable pages \ -# detected: $max_pages pages weighting $max_bytes bytes" - -# @ensure @warnv_safe_to_log 0 "no interpolation" - -# mutable_report = pm_generate_mutable_pages_report!() -# if !isnothing(mutable_report) -# @warnv_safe_to_log 0 mutable_report -# end -# return false -# end - -# return true -# end -# """ -# @test lint_test(source, "Line 7, column 9: Safe warning log has interpolation.") -# @test lint_test(source, "Line 10, column 17: Safe warning log has interpolation.") -# end - -@testset "Use of static threads" begin - source = raw""" - function f() - Threads.@threads :static for _ in 1:10 - println("foo") - end - - @threads :static for _ in 1:10 - println("foo") - end - - Threads.@threads :dynamic for _ in 1:10 - println("foo") - end - end - """ - @test lint_test(source, "Line 2, column 5: Use `Threads.@threads :dynamic` instead of `Threads.@threads :static`.") - @test lint_test(source, "Line 6, column 5: Use `Threads.@threads :dynamic` instead of `Threads.@threads :static`.") -end - -@testset "PreCommit format" begin - @testset "No fatal violation" begin - local result_matching = false - mktempdir() do dir - open(joinpath(dir, "foo.jl"), "w") do io1 - open(joinpath(dir, "bar.jl"), "w") do io2 - write(io1, "function f()\n @async 1 + 1\nend\n") - write(io2, "function g()\n @async 1 + 1\nend\n") - - flush(io1) - flush(io2) - - str = IOBuffer() - result = ReLint.run_lint(dir; io=str, formatter=ReLint.PreCommitFormat()) - ReLint.print_summary(ReLint.PreCommitFormat(), str, result) - - result = String(take!(str)) - - expected = r""" - 2 potential threats are found: 0 fatal violation, 2 violations and 0 recommendation - """ - result_matching = !isnothing(match(expected, result)) - end - end - end - @test result_matching - end - - @testset "With fatal violations" begin - local result_matching = false - mktempdir() do dir - open(joinpath(dir, "foo.jl"), "w") do io1 - open(joinpath(dir, "bar.jl"), "w") do io2 - write(io1, "function f()\n @async 1 + 1\n @warn \"blah\"\nend\n") - write(io2, """ - function g() - @async 1 + 1 - @info "blah" - end - """) - - flush(io1) - flush(io2) - - str = IOBuffer() - result = ReLint.run_lint(dir; io=str, formatter=ReLint.PreCommitFormat()) - ReLint.print_summary(ReLint.PreCommitFormat(), str, result) - - result = String(take!(str)) - - expected = r""" - Line 3, column 5: Unsafe logging statement\. You must enclose variables and strings with `@safe\(\.\.\.\)`\. \H+/bar.jl - Line 3, column 3: Unsafe logging statement\. You must enclose variables and strings with `@safe\(\.\.\.\)`\. \H+/foo.jl - 4 potential threats are found: 2 fatal violations, 2 violations and 0 recommendation - Note that the list above only show fatal violations - """ - result_matching = !isnothing(match(expected, result)) - end - end - end - @test result_matching - end -end - -@testset "Forbid @show" begin - source = """ - function f() - @show "foo" - @show 1 + 1 - end - """ - @test count_lint_errors(source) == 2 - @test lint_test(source, - "Line 2, column 5: Do not use `@show`, use `@info` instead.") - @test lint_test(source, - "Line 3, column 5: Do not use `@show`, use `@info` instead.") -end - -@testset "Forbid return in anonymous closure" begin - @testset "Anonymous function" begin - source = raw""" - function foo(f) - f() - return "foo" - end - - foo(() -> begin return "anonymous function" end) - foo() do ; return "anonymous function" end - foo() do - return "anonymous function" - end - - macro bar(x) - return :(($(x))()) - end - @bar(() -> begin return "anonymous function" end) - @bar() do - return "blah" - end - @bar() do ; return "blah" end - """ - @test count_lint_errors(source) == 6 - @test lint_test(source, - "Line 6, column 17: Anonymous function must not have `return` [Explanation](https") - @test lint_test(source, - "Line 7, column 12: Anonymous function must not have `return` [Explanation](https") - @test lint_test(source, - "Line 9, column 5: Anonymous function must not have `return` [Explanation](https") - @test lint_test(source, - "Line 15, column 18: Anonymous function must not have `return` [Explanation](https") - @test lint_test(source, - "Line 17, column 1: Anonymous function must not have `return` [Explanation](https") - @test lint_test(source, - "Line 19, column 13: Anonymous function must not have `return` [Explanation](https") - end - - @testset "make sure we restore the markers" begin - source = """ - @bar(() -> begin "anonymous function" end) - function f() - return 12 - end - """ - @test count_lint_errors(source) == 0 - end -end - -@testset "Forbid import" begin - source = """ - import foobar - import RelationalAI - import RelationalAI: rai_rules_tests - import RelationalAI: rai_rules_tests, rai_rules_tests2 - import RelationalAI: rai_rules_tests, rai_rules_tests2, rai_rules_tests3 - import RelationalAI: rai_rules_tests, rai_rules_tests2, rai_rules_tests3, rai_rules_tests4 - import RelationalAI: rai_rules_tests, rai_rules - - println("hello world") - """ - @test count_lint_errors(source) == 7 -end - -@testset "Forbid using RAICode" begin - source = """ - import RAICode - using RAICode - using RAICode: rai_rules_tests - - println("hello world") - """ - @test count_lint_errors(source) == 3 -end - -@testset "Forbid bare using" begin - @testset "bare using 01" begin - source = """ - using LinearAlgebra - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 1, column 1: Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`.") - end - - @testset "bare using 02" begin - source = """ - using Statistics - using LinearAlgebra - """ - @test count_lint_errors(source) == 2 - @test lint_test(source, - "Line 1, column 1: Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`.") - @test lint_test(source, - "Line 2, column 1: Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`.") - end - - @testset "explicit import with module name allowed" begin - source = """ - using LinearAlgebra: LinearAlgebra - """ - @test !lint_has_error_test(source) - end - - @testset "explicit import with function names allowed" begin - source = """ - using Statistics: mean, std - using LinearAlgebra: norm, dot - """ - @test !lint_has_error_test(source) - end - - @testset "mixed bare and explicit using" begin - source = """ - using LinearAlgebra - using Statistics: mean, std - using Base: show - """ - @test count_lint_errors(source) == 1 - @test lint_test(source, - "Line 1, column 1: Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`.") - end - - @testset "bare using in test file is allowed" begin - source = """ - using Test - using LinearAlgebra - """ - # Note: This will only pass when running on a file with "test" in the path - # In this test context, the markers[:filename] won't have "test/" so it will error - @test lint_has_error_test(source) - end -end - -@testset "Untyped array comprehensions" begin - @testset "untyped comprehension in Compiler" begin - source = """ - function f() - x = [i for i in 1:10] - return x - end - """ - @test lint_test(source, - "Line 2, column 9: Need a specific Array type to be provided."; - directory = "src/Compiler/") - end - - @testset "typed comprehension allowed" begin - source = """ - function f() - x = Int[i for i in 1:10] - return x - end - """ - @test count_lint_errors(source; directory = "src/Compiler/") == 0 - end - - @testset "untyped comprehension outside Compiler is ok" begin - source = """ - function f() - x = [i for i in 1:10] - return x - end - """ - @test count_lint_errors(source; directory = "src/Other/") == 0 - end - - @testset "multiple comprehensions mixed" begin - source = """ - function f() - x = [i for i in 1:10] - y = String[string(i) for i in 1:10] - return (x, y) - end - """ - @test count_lint_errors(source; directory = "src/Compiler/") == 1 - @test lint_test(source, - "Line 2, column 9: Need a specific Array type to be provided."; - directory = "src/Compiler/") - end -end - -@testset "Return type annotations" begin - @testset "function with return type annotation" begin - source = """ - function foo(x::Int)::String - return string(x) - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 1, column 1: Avoid return type annotations") - end - - @testset "function without return type annotation is ok" begin - source = """ - function foo(x::Int) - return string(x) - end - """ - @test !lint_has_error_test(source) - end - - @testset "multiple functions with annotations" begin - source = """ - function foo(x::Int)::String - return string(x) - end - - function bar(y::Float64)::Int - return round(Int, y) - end - - function baz(z) - return z + 1 - end - """ - @test count_lint_errors(source) == 2 - @test lint_test(source, - "Line 1, column 1: Avoid return type annotations") - @test lint_test(source, - "Line 5, column 1: Avoid return type annotations") - end - - @testset "one-liner with return type" begin - source = """ - foo(x::Int)::String = string(x) - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 1, column 1: Avoid return type annotations") - end - - @testset "one-liner without return type" begin - source = """ - foo(x::Int) = string(x) - """ - @test !lint_has_error_test(source) - end - - @testset "derived functions" begin - source = """ - @derived v=1 function _is_ivm_frame(rt::Runtime, path::RelPath)::Bool - return RAI_BackIR.has_attr(definition_of(rt, path), :frame) - end - """ - @test !lint_has_error_test(source) - end -end - -@testset "String concatenation with *" begin - @testset "string literal concatenation" begin - source = """ - function f() - x = "hello" * " world" - return x - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 2, column 9: Prefer string interpolation") - end - - @testset "variable concatenation with string" begin - source = """ - function f(name) - msg = "Hello " * name - return msg - end - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Line 2, column 11: Prefer string interpolation") - end - - @testset "string interpolation is ok" begin - source = raw""" - function f(name) - msg = "Hello $(name)" - return msg - end - """ - @test !lint_has_error_test(source) - end - - @testset "string() function is ok" begin - source = """ - function f(x, y) - msg = string(x, y) - return msg - end - """ - @test !lint_has_error_test(source) - end - - @testset "numeric multiplication is ok" begin - source = """ - function f() - x = 2 * 3 - return x - end - """ - @test !lint_has_error_test(source) - end -end - -@testset "No global variables" begin - @testset "global variable assignment" begin - source = """ - global counter = 0 - """ - @test lint_has_error_test(source) - @test lint_test(source, - "Global variable must have type annotation") - end - - @testset "const global is ok" begin - source = """ - const MAX_SIZE = 100 - """ - @test !lint_has_error_test(source) - end - - @testset "global in function is ok" begin - source = """ - function f() - x = 10 - return x - end - """ - @test !lint_has_error_test(source) - end - - @testset "multiple global assignments" begin - source = """ - global x = 1 - global y = 2 - const z = 3 - """ - @test count_lint_errors(source) == 2 - @test lint_test(source, - "Global variable must have type annotation") - @test lint_test(source, - "Global variable must have type annotation") - end -end - -@testset "Printing LintReport" begin - using ReLint: LintRuleReport, LintResult, GeneratedRule, print_report, PreCommitFormat, - is_fatal - - result = LintResult() - lint_report = LintRuleReport(GeneratedRule(), "error") - io = IOBuffer() - print_report(PreCommitFormat(), io, lint_report, result) - @test String(take!(io)) == "Line 0, column 0: error \n" -end - -@testset "Warning when modifying ENV" begin - @testset "Just reaching ENV" begin - source = raw""" - function f(name) - println("Env var: ", ENV["HOME"]) - end - """ - @test !lint_has_error_test(source) - end - - @testset "Modifying ENV" begin - source = raw""" - function f(name) - ENV["HOME"] = "/tmp" - push!(ENV, "NEW_VAR" => "value") - delete!(ENV, "HOME") - setindex!(ENV, "/tmp", "HOME") - get!(ENV, "/tmp", "HOME") - pop!(ENV, "HOME") - end - """ - @test count_lint_errors(source) == 6 - end -end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index fd5c8bc..fd413b4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -11,4 +11,4 @@ include(joinpath(@__DIR__, "violations.jl")) include(joinpath(@__DIR__, "recommendations.jl")) # include(joinpath(@__DIR__, "lint_context_tests.jl")) -# include(joinpath(@__DIR__, "rai_rules_tests.jl")) +include(joinpath(@__DIR__, "interface.jl")) From b9d9bd8037f282b4b39caea45e7e2a4f78a5683f Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Wed, 8 Apr 2026 18:36:01 +0200 Subject: [PATCH 36/62] Add file exclusion tests to test/interface.jl; reorder tests --- test/file_exclusion_tests.jl | 119 --------------- test/interface.jl | 275 +++++++++++++++++++++++------------ test/runtests.jl | 1 - 3 files changed, 186 insertions(+), 209 deletions(-) delete mode 100644 test/file_exclusion_tests.jl diff --git a/test/file_exclusion_tests.jl b/test/file_exclusion_tests.jl deleted file mode 100644 index f9c8f9f..0000000 --- a/test/file_exclusion_tests.jl +++ /dev/null @@ -1,119 +0,0 @@ -@testset "File exclusion" begin - using ReLint: extract_file_exclusions_from_precommit_file, LintFileExclusion - - filename = "precommit-config-fortesting.yaml" - precommit_full_path = joinpath(dirname(@__FILE__), filename) - if !isfile(precommit_full_path) - precommit_full_path = joinpath(dirname(@__FILE__), "test", filename) - end - @test isfile(precommit_full_path) - - @testset "Pre-commit file" begin - # Test the extraction of file exclusions from a pre-commit file. - expected_reg_exs = [ - "test.jl", - "src/version.jl", - "packages/jet_test_utils.jl", - "src/Test/jcompile-stats.jl", - "src/Test/integration.jl", - "build/.*", - ".github/.*", - "packages/RAI_Benchmarks/.*", - "test_cloud/.*", - "packages/Arroyo/bench/.*", - "contrib/.*", - "packages/RAI_Snoop/.*", - "packages/Salsa/examples/.*", - "test_spcs/.*", - "test/.*", - "bench/.*", - "scripts/.*", - "skaffold/.*", - ] - exclusions = extract_file_exclusions_from_precommit_file(precommit_full_path) - reg_exs = map(l->l.regex, exclusions) - @test all(l->l isa LintFileExclusion, exclusions) - @test reg_exs == expected_reg_exs - end - - @testset "Two files with errors" begin - local result_matching = false - mktempdir() do dir - open(joinpath(dir, "test.jl"), "w") do io1 - open(joinpath(dir, "bar.jl"), "w") do io2 - write(io1, "function f()\n @async 1 + 1\nend\n") - write(io2, "function g()\n @async 1 + 1\nend\n") - - flush(io1) - flush(io2) - - re = extract_file_exclusions_from_precommit_file(precommit_full_path) - context = LintContext(ReLint.all_rules(), re) - - # Run the linter on the directory - str = IOBuffer() - ReLint.run_lint( - dir; - io=str, - formatter=ReLint.MarkdownFormat(), - context, - ) - - result = String(take!(str)) - - # Only one of the files is linted - expected = r""" - - \*\*Line 2, column 7:\*\* Use `@spawn` instead of `@async`\. \H+ - """ - - result_matching = !isnothing(match(expected, result)) - result_matching || @info "DEBUG: $(result)" - end - end - end - @test result_matching - end - - @testset "Generating report" begin - local result_matching = false - - mktempdir() do dir - file1_name = joinpath(dir, "test.jl") - file2_name = joinpath(dir, "bar.jl") - - open(file1_name, "w") do io1 - open(file2_name, "w") do io2 - write(io1, "function f()\n @async 1 + 1\nend\n") - write(io2, "function g()\n @async 1 + 1\nend\n") - - flush(io1) - flush(io2) - - re = extract_file_exclusions_from_precommit_file(precommit_full_path) - context = LintContext(ReLint.all_extended_rule_types[], re) - - # Run the linter on the directory - output_file = tempname() - ReLint.generate_report( - [file1_name, file2_name], - output_file; - - json_filename=tempname(), - stream_workflowcommand=devnull, - pre_commit_file=precommit_full_path) - - result = open(output_file, "r") do io read(io, String) end - - # Only one of the files is linted - expected = r""" - - \*\*Line 2, column 7:\*\* Use `@spawn` instead of `@async`\. \H+ - """ - - result_matching = !isnothing(match(expected, result)) - result_matching || @info "DEBUG: $(result)" - end - end - end - @test result_matching - end -end \ No newline at end of file diff --git a/test/interface.jl b/test/interface.jl index f466d7d..2caab7f 100644 --- a/test/interface.jl +++ b/test/interface.jl @@ -14,72 +14,6 @@ end -@testset "Formatter" begin - source = """ - const x = Threads.nthreads() - function f() - return x - end - """ - - @testset "Plain 02" begin - io = IOBuffer() - run_lint_on_text(source; io=io) - result = String(take!(io)) - - expected = r""" - ---------- \H+ - Line 1, column 1: `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - 1 potential threat is found: 0 fatal violation, 1 violation and 0 recommendation - ---------- - """ - @test !isnothing(match(expected, result)) - end - - @testset "Markdown 02" begin - io = IOBuffer() - run_lint_on_text(source; io=io, formatter=MarkdownFormat()) - result = String(take!(io)) - - expected = r""" - - \*\*Line 1, column 1:\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - """ - @test !isnothing(match(expected, result)) - end - - @testset "Markdown 03 - with github information" begin - formatter = MarkdownFormat("axb-example-with-lint-errors", "RelationalAI/raicode") - io = IOBuffer() - - run_lint_on_text( - source; - io, - formatter, - directory="/src/Compiler/") - result = String(take!(io)) - - expected = r""" - - \*\*\[Line 1, column 1:\]\(https://github\.com/RelationalAI/raicode/blob/axb-example-with-lint-errors/\H+/src/Compiler/tmp_julia_file\.jl#L1\)\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - """ - @test !isnothing(match(expected, result)) - end - - @testset "Markdown 04 - with github information" begin - formatter = MarkdownFormat("axb-example-with-lint-errors", "RelationalAI/raicode") - io = IOBuffer() - run_lint_on_text( - source; - io, - formatter, - directory="src/Compiler/") - result = String(take!(io)) - expected = r""" - - \*\*\[Line 1, column 1:\]\(https://github\.com/RelationalAI/raicode/blob/axb-example-with-lint-errors/\H+/src/Compiler/tmp_julia_file\.jl#L1\)\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - """ - @test !isnothing(match(expected, result)) - end -end - @testset "Run several times on same file" begin mktempdir() do dir open(joinpath(dir, "foo.jl"), "w") do io @@ -658,6 +592,126 @@ end end end +@testset "File exclusion" begin + using ReLint: extract_file_exclusions_from_precommit_file, LintFileExclusion + + filename = "precommit-config-fortesting.yaml" + precommit_full_path = joinpath(dirname(@__FILE__), filename) + if !isfile(precommit_full_path) + precommit_full_path = joinpath(dirname(@__FILE__), "test", filename) + end + @test isfile(precommit_full_path) + + @testset "Pre-commit file" begin + # Test the extraction of file exclusions from a pre-commit file. + expected_reg_exs = [ + "test.jl", + "src/version.jl", + "packages/jet_test_utils.jl", + "src/Test/jcompile-stats.jl", + "src/Test/integration.jl", + "build/.*", + ".github/.*", + "packages/RAI_Benchmarks/.*", + "test_cloud/.*", + "packages/Arroyo/bench/.*", + "contrib/.*", + "packages/RAI_Snoop/.*", + "packages/Salsa/examples/.*", + "test_spcs/.*", + "test/.*", + "bench/.*", + "scripts/.*", + "skaffold/.*", + ] + exclusions = extract_file_exclusions_from_precommit_file(precommit_full_path) + reg_exs = map(l->l.regex, exclusions) + @test all(l->l isa LintFileExclusion, exclusions) + @test reg_exs == expected_reg_exs + end + + @testset "Two files with errors" begin + local result_matching = false + mktempdir() do dir + open(joinpath(dir, "test.jl"), "w") do io1 + open(joinpath(dir, "bar.jl"), "w") do io2 + write(io1, "function f()\n @async 1 + 1\nend\n") + write(io2, "function g()\n @async 1 + 1\nend\n") + + flush(io1) + flush(io2) + + re = extract_file_exclusions_from_precommit_file(precommit_full_path) + context = LintContext(ReLint.all_rules(), re) + + # Run the linter on the directory + str = IOBuffer() + ReLint.run_lint( + dir; + io=str, + formatter=ReLint.MarkdownFormat(), + context, + ) + + result = String(take!(str)) + + # Only one of the files is linted + expected = r""" + - \*\*Line 2, column 7:\*\* Use `@spawn` instead of `@async`\. \H+ + """ + + result_matching = !isnothing(match(expected, result)) + result_matching || @info "DEBUG: $(result)" + end + end + end + @test result_matching + end + + @testset "Generating report" begin + local result_matching = false + + mktempdir() do dir + file1_name = joinpath(dir, "test.jl") + file2_name = joinpath(dir, "bar.jl") + + open(file1_name, "w") do io1 + open(file2_name, "w") do io2 + write(io1, "function f()\n @async 1 + 1\nend\n") + write(io2, "function g()\n @async 1 + 1\nend\n") + + flush(io1) + flush(io2) + + re = extract_file_exclusions_from_precommit_file(precommit_full_path) + context = LintContext(ReLint.all_rules(), re) + + # Run the linter on the directory + output_file = tempname() + ReLint.generate_report( + [file1_name, file2_name], + output_file; + + json_filename=tempname(), + stream_workflowcommand=devnull, + pre_commit_file=precommit_full_path) + + result = open(output_file, "r") do io read(io, String) end + + # Only one of the files is linted + expected = r""" + - \*\*Line 2, column 7:\*\* Use `@spawn` instead of `@async`\. \H+ + """ + + result_matching = !isnothing(match(expected, result)) + result_matching || @info "DEBUG: $(result)" + end + end + end + @test result_matching + end +end + @testset "Locally disabling lint" begin @testset "lint-disable-next-line" begin @test !lint_has_error_test(""" @@ -788,29 +842,6 @@ end end end -@testset "Relaxing unused bindings" begin - # @test lint_test(""" - # function f(a::Int64, b, c) - # local x - # return 42 - # end - # """, "Line 2, column 11: Variable has been assigned but not used") - - @test !lint_has_error_test(""" - function f(a::Int64, b, c) - local _ - return 42 - end - """) - - @test !lint_has_error_test(""" - function f(a::Int64, b, c) - local _x - return 42 - end - """) -end - @testset "Recommentation separated from violations" begin source = """ function f() @@ -929,3 +960,69 @@ end print_report(PreCommitFormat(), io, lint_report, result) @test String(take!(io)) == "Line 0, column 0: error \n" end + +@testset "Formatter" begin + source = """ + const x = Threads.nthreads() + function f() + return x + end + """ + + @testset "Plain 02" begin + io = IOBuffer() + run_lint_on_text(source; io=io) + result = String(take!(io)) + + expected = r""" + ---------- \H+ + Line 1, column 1: `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ + 1 potential threat is found: 0 fatal violation, 1 violation and 0 recommendation + ---------- + """ + @test !isnothing(match(expected, result)) + end + + @testset "Markdown 02" begin + io = IOBuffer() + run_lint_on_text(source; io=io, formatter=MarkdownFormat()) + result = String(take!(io)) + + expected = r""" + - \*\*Line 1, column 1:\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ + """ + @test !isnothing(match(expected, result)) + end + + @testset "Markdown 03 - with github information" begin + formatter = MarkdownFormat("axb-example-with-lint-errors", "RelationalAI/raicode") + io = IOBuffer() + + run_lint_on_text( + source; + io, + formatter, + directory="/src/Compiler/") + result = String(take!(io)) + + expected = r""" + - \*\*\[Line 1, column 1:\]\(https://github\.com/RelationalAI/raicode/blob/axb-example-with-lint-errors/\H+/src/Compiler/tmp_julia_file\.jl#L1\)\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ + """ + @test !isnothing(match(expected, result)) + end + + @testset "Markdown 04 - with github information" begin + formatter = MarkdownFormat("axb-example-with-lint-errors", "RelationalAI/raicode") + io = IOBuffer() + run_lint_on_text( + source; + io, + formatter, + directory="src/Compiler/") + result = String(take!(io)) + expected = r""" + - \*\*\[Line 1, column 1:\]\(https://github\.com/RelationalAI/raicode/blob/axb-example-with-lint-errors/\H+/src/Compiler/tmp_julia_file\.jl#L1\)\*\* `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ + """ + @test !isnothing(match(expected, result)) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index fd413b4..a715436 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,7 +5,6 @@ using ReLint: convert_offset_to_line_from_lines include(joinpath(@__DIR__, "common.jl")) # include(joinpath(@__DIR__, "line_rules_tests.jl")) -# include(joinpath(@__DIR__, "file_exclusion_tests.jl")) include(joinpath(@__DIR__, "fatal.jl")) include(joinpath(@__DIR__, "violations.jl")) include(joinpath(@__DIR__, "recommendations.jl")) From abff960d6d9f43626e44eb133a37b5f19026e3d1 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 9 Apr 2026 13:23:48 +0200 Subject: [PATCH 37/62] Clean up dependencies --- Project.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 09e2856..c5ce431 100644 --- a/Project.toml +++ b/Project.toml @@ -4,17 +4,14 @@ version = "0.9.0" [deps] Argus = "e39743ec-b2cc-45ef-a990-b666fc093538" -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -Git = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [compat] Argus = "0.1.0" -BenchmarkTools = "1.6.3" -Git = "1.5.0" +Dates = "1.11.0" JuliaSyntax = "1.0.2" julia = "1" From 3c63971ddd6c51fe619ae26543d85ea519c98c56 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 9 Apr 2026 13:55:01 +0200 Subject: [PATCH 38/62] Reorganise --- scripts/run_julia_lint.sh | 2 +- src/ReLint.jl | 12 +- src/interface.jl | 674 +++++++++++++++++---------------- src/linting/extended_checks.jl | 34 -- src/linting/text_lint_rules.jl | 3 +- test/common.jl | 2 +- test/interface.jl | 28 +- 7 files changed, 378 insertions(+), 377 deletions(-) delete mode 100644 src/linting/extended_checks.jl diff --git a/scripts/run_julia_lint.sh b/scripts/run_julia_lint.sh index 5b02c5e..966314d 100755 --- a/scripts/run_julia_lint.sh +++ b/scripts/run_julia_lint.sh @@ -112,7 +112,7 @@ while [[ $ATTEMPT -lt $MAX_RETRIES ]]; do end using ReLint: ReLint, LintContext - result = ReLint.LintResult() + result = ReLint.LintGlobalReport() all_files_tmp=split(open(io->read(io, String), \"$FILES_TO_RUN\", \"r\")) all_files=map(string, all_files_tmp) all_files=filter(f->isfile(f) || isdir(f), all_files) diff --git a/src/ReLint.jl b/src/ReLint.jl index 00afcda..fd82f4b 100644 --- a/src/ReLint.jl +++ b/src/ReLint.jl @@ -3,17 +3,17 @@ module ReLint using Argus using JuliaSyntax -mutable struct LintMeta - error - LintMeta() = new(nothing) - LintMeta(v) = new(v) -end +all_rules() = [ + values(RECOMMENDATIONS)..., + values(VIOLATIONS)..., + values(FATAL_VIOLATIONS)... +] -include("linting/extended_checks.jl") include("interface.jl") function __init__() include(joinpath(@__DIR__, "linting/argus-rules.jl")) + include(joinpath(@__DIR__, "linting/text_lint_rules.jl")) end end diff --git a/src/interface.jl b/src/interface.jl index 331ff06..d0c33a7 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -3,6 +3,12 @@ using JSON3 global MAX_REPORTED_ERRORS = 60 # 1_000_000 +# Lint report +# =========== + +# Individual rule report +# ---------------------- + # Each individual rule violation report mutable struct LintRuleReport rule::Rule @@ -34,17 +40,73 @@ is_recommendation(r::LintRuleReport) = is_recommendation(r.rule) is_violation(r::LintRuleReport) = is_violation(r.rule) is_fatal(r::LintRuleReport) = is_fatal(r.rule) +# Global report +# ------------- + +# Global result of executing Lint on files and folders +mutable struct LintGlobalReport + files_count::Integer + violations_count::Integer + recommendations_count::Integer + fatalviolations_count::Integer + linted_files::Vector{String} + printout_count::Integer + lintrule_reports::Vector{LintRuleReport} + branch::String # The branch on which we got the result + + LintGlobalReport(args...) = new(args...) +end + +LintGlobalReport() = LintGlobalReport(0, 0, 0) +LintGlobalReport(a, b, c) = LintGlobalReport(a, b, c, 0) +LintGlobalReport(a, b, c, d) = LintGlobalReport(a, b, c, d, String[]) +LintGlobalReport(a, b, c, d, e) = LintGlobalReport(a, b, c, d, e, 0) +LintGlobalReport(a, b, c, d, e, f) = LintGlobalReport(a, b, c, d, e, f, LintRuleReport[]) +LintGlobalReport(a, b, c, d, e, f, g) = LintGlobalReport(a, b, c, d, e, f, LintRuleReport[], "master") + + +function Base.append!(l1::LintGlobalReport, l2::LintGlobalReport) + l1.files_count += l2.files_count + l1.violations_count += l2.violations_count + l1.recommendations_count += l2.recommendations_count + l1.fatalviolations_count += l2.fatalviolations_count + Base.append!(l1.linted_files, l2.linted_files) + Base.append!(l1.lintrule_reports, l2.lintrule_reports) + + return l1.printout_count += l2.printout_count +end + +function Base.:(==)(l1::LintGlobalReport, l2::LintGlobalReport) + return l1.files_count == l2.files_count && + l1.violations_count == l2.violations_count && + l1.recommendations_count == l2.recommendations_count && + l1.fatalviolations_count == l2.fatalviolations_count && + l1.linted_files == l2.linted_files && + l1.printout_count == l2.printout_count && + l1.lintrule_reports == l2.lintrule_reports +end + +function is_already_linted(l::LintGlobalReport, filename) + return filename in l.linted_files +end + +function has_values(l::LintGlobalReport, a, b, c) + return l.files_count == a && + l.violations_count == b && + l.recommendations_count == c +end + +# Precommit config +# ================ + # File exclusion struct LintFileExclusion regex::String end -function should_be_excluded(lfe::LintFileExclusion, filename::String) - # isdefined(Main, :Infiltrator) && Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) - return !isnothing(match(Regex(lfe.regex), filename)) -end -function should_be_excluded(lfes::Vector{LintFileExclusion}, filename::String) - return any(lfe -> should_be_excluded(lfe, filename), lfes) -end +should_be_excluded(lfe::LintFileExclusion, filename::String) = + !isnothing(match(Regex(lfe.regex), filename)) +should_be_excluded(lfes::Vector{LintFileExclusion}, filename::String) = + any(lfe -> should_be_excluded(lfe, filename), lfes) # For a given pre-commit file configuration, the function extract the regex rules # that are used to exclude files from the linting process. @@ -90,61 +152,59 @@ function extract_file_exclusions_from_precommit_file(pre_commit_file::String) return file_exclusions end +# Formatting +# ========== -# Global result of executing Lint on files and folders -mutable struct LintResult - files_count::Integer - violations_count::Integer - recommendations_count::Integer - fatalviolations_count::Integer - linted_files::Vector{String} - printout_count::Integer - lintrule_reports::Vector{LintRuleReport} - branch::String # The branch on which we got the result - - LintResult(args...) = new(args...) -end +abstract type AbstractFormatter end +struct PlainFormat <: AbstractFormatter end -LintResult() = LintResult(0, 0, 0) -LintResult(a, b, c) = LintResult(a, b, c, 0) -LintResult(a, b, c, d) = LintResult(a, b, c, d, String[]) -LintResult(a, b, c, d, e) = LintResult(a, b, c, d, e, 0) -LintResult(a, b, c, d, e, f) = LintResult(a, b, c, d, e, f, LintRuleReport[]) -LintResult(a, b, c, d, e, f, g) = LintResult(a, b, c, d, e, f, LintRuleReport[], "master") +# MarkdownFormat can optionally contains github information. This is useful when a +# report is generated which contains Markdown links. +# file_prefix_to_remove corresponds to a prefix files will be removed when generating the +# report. This is useful because GitHub Action clones a repository in a folder of the same +# name. In our case, GHA will create /home/runner/work/raicode/raicode so we need to remove +# one "raicode" from the fullname. +struct MarkdownFormat <: AbstractFormatter + github_branch_name::String + github_repository_name::String + file_prefix_to_remove::String + stream_workflowcommand::IO + MarkdownFormat() = new("", "", "", devnull) + MarkdownFormat( + branch::String, + repo::String, + prefix::String, + stream_workflowcommand::IO + ) = new(branch, repo, prefix, stream_workflowcommand) + MarkdownFormat(branch::String, repo::String) = new(branch, repo, "", devnull) +end -function Base.append!(l1::LintResult, l2::LintResult) - l1.files_count += l2.files_count - l1.violations_count += l2.violations_count - l1.recommendations_count += l2.recommendations_count - l1.fatalviolations_count += l2.fatalviolations_count - Base.append!(l1.linted_files, l2.linted_files) - Base.append!(l1.lintrule_reports, l2.lintrule_reports) +# Only show the fatal violations and a summary +struct PreCommitFormat <: AbstractFormatter end - return l1.printout_count += l2.printout_count -end +# Linting +# ======= -function Base.:(==)(l1::LintResult, l2::LintResult) - return l1.files_count == l2.files_count && - l1.violations_count == l2.violations_count && - l1.recommendations_count == l2.recommendations_count && - l1.fatalviolations_count == l2.fatalviolations_count && - l1.linted_files == l2.linted_files && - l1.printout_count == l2.printout_count && - l1.lintrule_reports == l2.lintrule_reports -end +struct LintContext + rules_to_run::Vector{Rule} + regex_exclusions #::Vector{LintFileExclusion} -function is_already_linted(l::LintResult, filename) - return filename in l.linted_files -end + function LintContext(dts_as_str::Vector{String}) + dt = DataType[] + for dt_as_str in dts_as_str + ind = findfirst(t -> nameof(t) == Symbol(dt_as_str), all_rules()) + isnothing(ind) && error("Non-existing rule: $(dt_as_str)") + push!(dt, all_rules()[ind]) + end + return new(dt, []) + end -function has_values(l::LintResult, a, b, c) - return l.files_count == a && - l.violations_count == b && - l.recommendations_count == c + LintContext(s::Vector{Rule}) = new(s, []) + LintContext() = new(all_rules(), []) + LintContext(a, b) = new(a, b) end - """ lint_file(rootpath, context) @@ -204,162 +264,9 @@ function lint_text(file_content_string::AbstractString; filename = "", c end get_all_lines_from_filename(filename::String) = open(io -> readlines(io), filename) -struct ReLintDisabler <: CommentDisabler end -const relint_disabler = ReLintDisabler() - -relint_disabler(line::AbstractString) = is_disable_all_comment(line) -function relint_disabler(rule::Rule, line::AbstractString) - is_disable_comment(line) || return false - is_disable_all_comment(line) && return true - is_disable_rule_comment(line, rule) && return true - return false -end -is_disable_comment(str::AbstractString) = - startswith(str, "# lint-disable-next-line") -is_disable_all_comment(str::AbstractString) = str == "# lint-disable-next-line" -function is_disable_rule_comment(str::AbstractString, rule::Rule) - is_disable_comment(str) || return false - split_command_from_rules = split(str, ":") - length(split_command_from_rules) == 2 || return false - rules_list = strip.(split(split_command_from_rules[2], ",")) - rule.name in rules_list && return true - # Allow disabling by rule description as well. - return startswith(rule.description, lstrip(split_command_from_rules[2])) -end - -# Return (index_line, index_column, annotation) for a given offset in a source -# Useful in tests? -function convert_offset_to_line(offset::Integer, source::String) - return convert_offset_to_line_from_lines(offset, split(source, "\n")) -end - -# Return the lint next-line annotation, if there is one, at the end of `line`. -# Return -# * `nothing` if there is no `lint-disable-next-line` annotation. -# * ""::SubString if the end of the line is "lint-disable-next-line". -# * s::SubString if the end of the line is "lint-disable_next_line: $s" -function annotation_for_next_line(line::AbstractString) - if endswith(line, "lint-disable-next-line") - return "" - end - # An annotation must be in a comment and not contain any `#` or `"` characters. - m = match(r"# lint-disable-next-line: *([^\"#]+)$", line) - return isnothing(m) ? nothing : m[1] -end - -function annotation_for_this_line(line::AbstractString) - if endswith(line, "lint-disable-line") - return "" - end - # An annotation must be in a comment and not contain any `#` or `"` characters. - m = match(r"#\h*lint-disable-line: *([^\"#]+)$", line) - return isnothing(m) ? nothing : m[1] -end - -# Return a triple: (line::Int, column::Int, annotation::Option(String)) -# Return (index_line, index_column, annotation) for a given offset in a source -# -# `annotation` could be either `nothing`, "lint-disable-line", or -# `"lint-disable-line: $ERROR_MSG_TO_IGNORE"` -# -# Note: `offset` is measured in codepoints. The returned `column` is a character -# offset, not a codepoint offset. -function convert_offset_to_line_from_lines(offset::Integer, all_lines) - offset < 0 && throw(BoundsError("source", offset)) - - current_codepoint = 1 - # In these annotations, "" means "lint-disable-line", a nonempty string `s` means - # "lint_disable_line: $s", and nothing means there's no applicable annotation. - prev_annotation::Union{Nothing, SubString} = nothing - this_annotation::Union{Nothing, SubString} = nothing - for (line_number, line) in enumerate(all_lines) - this_annotation = annotation_for_this_line(line) - # current_codepoint + sizeof(line) is possibly pointing at the newline that isn't - # actually stored in `line`. - if offset in current_codepoint:(current_codepoint + sizeof(line)) - index_in_line = offset - current_codepoint + 1 # possibly off the end by 1. - if !isnothing(this_annotation) - annotation = this_annotation - elseif !isnothing(prev_annotation) - annotation = prev_annotation - else - annotation = nothing - end - if !isnothing(annotation) - if annotation == "" - annotation = "lint-disable-line" - else - annotation = "lint-disable-line: " * annotation - end - end - if index_in_line == sizeof(line) + 1 - return line_number, length(line) + 1, annotation - else - return line_number, length(line, 1, index_in_line), annotation - end - end - prev_annotation = annotation_for_next_line(line) - current_codepoint += sizeof(line) + 1 # 1 is for the newline - end - throw(BoundsError("source", offset)) -end - -abstract type AbstractFormatter end -struct PlainFormat <: AbstractFormatter end - -# MarkdownFormat can optionally contains github information. This is useful when a -# report is generated which contains Markdown links. -# file_prefix_to_remove corresponds to a prefix files will be removed when generating the -# report. This is useful because GitHub Action clones a repository in a folder of the same -# name. In our case, GHA will create /home/runner/work/raicode/raicode so we need to remove -# one "raicode" from the fullname. -struct MarkdownFormat <: AbstractFormatter - github_branch_name::String - github_repository_name::String - file_prefix_to_remove::String - stream_workflowcommand::IO - - MarkdownFormat() = new("", "", "", devnull) - MarkdownFormat( - branch::String, - repo::String, - prefix::String, - stream_workflowcommand::IO - ) = new(branch, repo, prefix, stream_workflowcommand) - MarkdownFormat(branch::String, repo::String) = new(branch, repo, "", devnull) -end - -# Only show the fatal violations and a summary -struct PreCommitFormat <: AbstractFormatter end - -function print_header(::PreCommitFormat, io::IO, rootpath::String) - # printstyled(io, "-" ^ 10 * " $(rootpath)\n", color=:blue) - # printstyled(io, "**List of Fatal violations, please address them to commit these files**\n", color=:red) -end - -print_footer(::PreCommitFormat, io::IO) = nothing -function print_summary(::PreCommitFormat, io::IO, result::LintResult) - print_summary(PlainFormat(), io, result) - return printstyled(io, "Note that the list above only show fatal violations\n", color = :red) -end - -function print_report(::PreCommitFormat, io::IO, lint_report::LintRuleReport, result::LintResult) - should_print_report(result) || return - # Do not print anything if it is not a fatal violation - is_fatal(lint_report) || return - printstyled(io, "Line $(lint_report.line), column $(lint_report.column):", color = :green) - print(io, " ") - print(io, lint_report.msg) - print(io, " ") - println(io, lint_report.file) - return result.printout_count += 1 -end - -should_print_report(result) = result.printout_count <= MAX_REPORTED_ERRORS - function _run_lint_on_dir( rootpath::String; - result::LintResult = LintResult(), + result::LintGlobalReport = LintGlobalReport(), io::Union{IO, Nothing} = stdout, io_violations::Union{IO, Nothing} = nothing, io_recommendations::Union{IO, Nothing} = nothing, @@ -384,93 +291,13 @@ function _run_lint_on_dir( end end return result -end - -function print_header(::PlainFormat, io::IO, rootpath::String) - return printstyled(io, "-"^10 * " $(rootpath)\n", color = :blue) -end - -function print_report(::PlainFormat, io::IO, lint_report::LintRuleReport, result::LintResult) - should_print_report(result) || return - printstyled(io, "Line $(lint_report.line), column $(lint_report.column):", color = :green) - print(io, " ") - print(io, lint_report.msg) - print(io, " ") - println(io, lint_report.file) - return result.printout_count += 1 - -end - -function print_summary( - ::PlainFormat, - io::IO, - result::LintResult - ) - nb_rulereports = result.violations_count + result.recommendations_count + result.fatalviolations_count - return if iszero(nb_rulereports) - printstyled(io, "No potential threats were found.\n", color = :green) - else - plural = nb_rulereports > 1 ? "s are" : " is" - plural_vio = result.violations_count > 1 ? "s" : "" - plural_fatal = result.fatalviolations_count > 1 ? "s" : "" - plural_rec = result.recommendations_count > 1 ? "s" : "" - printstyled(io, "$(nb_rulereports) potential threat$(plural) found: ", color = :red) - printstyled(io, "$(result.fatalviolations_count) fatal violation$(plural_fatal), $(result.violations_count) violation$(plural_vio) and $(result.recommendations_count) recommendation$(plural_rec)\n", color = :red) - end -end - -function print_footer(::PlainFormat, io::IO) - return printstyled(io, "-"^10 * "\n\n", color = :blue) -end - -print_header(::MarkdownFormat, io::IO, rootpath::String) = nothing -print_footer(::MarkdownFormat, io::IO) = nothing - -# Remove a leading '/' if the file starts with one. This is necessary to build the URL -# Remove the prefix mentioned in the Markdown from the file_name -function remove_prefix_from_filename(file_name::String, file_prefix_to_remove::String) - corrected_file_name = first(file_name) == '/' ? file_name[2:end] : file_name - if startswith(corrected_file_name, file_prefix_to_remove) - corrected_file_name = corrected_file_name[(length(file_prefix_to_remove) + 1):end] - end - return corrected_file_name -end - -function remove_prefix_from_filename(file_name::String, format::MarkdownFormat) - return remove_prefix_from_filename(file_name, format.file_prefix_to_remove) -end - -function print_report(format::MarkdownFormat, io::IO, lint_report::LintRuleReport, result::LintResult) - should_print_report(result) || return - - corrected_file_name = remove_prefix_from_filename(lint_report.file, format) - - coordinates = "Line $(lint_report.line), column $(lint_report.column):" - if !isempty(format.github_branch_name) && !isempty(format.github_repository_name) - extended_coordinates = "[$(coordinates)](https://github.com/$(format.github_repository_name)/blob/$(format.github_branch_name)/$(corrected_file_name)#L$(lint_report.line))" - print(io, " - **$(extended_coordinates)** $(lint_report.msg) $(lint_report.file)\n") - else - print(io, " - **$(coordinates)** $(lint_report.msg) $(lint_report.file)\n") - end - - # Produce workflow command to see results in the PR file changed tab: - # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#example-setting-an-error-message - println(format.stream_workflowcommand, "::error file=$(corrected_file_name),line=$(lint_report.line),col=$(lint_report.column)::$(lint_report.msg)") - return result.printout_count += 1 -end - -print_summary( - ::MarkdownFormat, - io::IO, - result::LintResult -) = nothing - +end """ run_lint(rootpath::String; io::IO=stdout, io_violations::Union{IO,Nothing}, io_recommendations::Union{IO,Nothing}) Run lint rules on a file `rootpath`, which must be an existing non-folder file. Return a -LintResult. +LintGlobalReport. Example of use: import ReLint @@ -478,7 +305,7 @@ Example of use: """ function run_lint( rootpath::String; - result::LintResult = LintResult(), + result::LintGlobalReport = LintGlobalReport(), io::Union{IO, Nothing} = stdout, io_violations::Union{IO, Nothing} = nothing, io_recommendations::Union{IO, Nothing} = nothing, @@ -527,7 +354,7 @@ function run_lint( end # We run Lint on a single file. - append!(result, LintResult(1, count_violations, count_recommendations, count_fatalviolations, [rootpath], 0, lint_reports)) + append!(result, LintGlobalReport(1, count_violations, count_recommendations, count_fatalviolations, [rootpath], 0, lint_reports)) return result end @@ -539,7 +366,7 @@ useful to test some rules that depends on the filename. """ function run_lint_on_text( source::String; - result::LintResult = LintResult(), + result::LintGlobalReport = LintGlobalReport(), io::Union{IO, Nothing} = stdout, formatter::AbstractFormatter = PlainFormat(), directory::String = "", # temporary directory to be created. If empty, let Julia decide @@ -581,34 +408,6 @@ function run_lint_on_text( end end -function print_datadog_report( - json_output::IO, - report_as_string::String, - files_count::Integer, - violation_count::Integer, - recommandation_count::Integer, - fatalviolations_count::Integer, - branch::String, - rules_count::Integer, - ) - event = Dict( - :source => "ReLint", - :specversion => "1.1", - :type => "result", - :time => string(now(UTC)), #Dates.format(now(UTC), "yyyy-mm-ddTHH:MM:SSZ"), # RFC3339 format - :data => Dict( - :report_as_string => report_as_string, - :files_count => files_count, - :violation_count => violation_count, - :recommandation_count => recommandation_count, - :fatalviolations_count => fatalviolations_count, - :branch => branch, - :rules_count => rules_count, # Added in specversion 1.1 - ) - ) - return println(json_output, JSON3.write(event)) -end - """ generate_report(filenames::Vector{String}, output_filename::String;...) @@ -674,7 +473,7 @@ function generate_report( local julia_filenames = filter(n -> endswith(n, ".jl"), filenames) # Result of the whole analysis - lint_result = LintResult() + lint_result = LintGlobalReport() lint_result.branch = branch_name # If analyze_all_file_found_locally is set to true, we discard all the provided files @@ -787,3 +586,238 @@ function generate_report( # In that case, we need to close the stream at the end. return isnothing(json_filename) || close(json_output) end + +struct ReLintDisabler <: CommentDisabler end +const relint_disabler = ReLintDisabler() + +relint_disabler(line::AbstractString) = is_disable_all_comment(line) +function relint_disabler(rule::Rule, line::AbstractString) + is_disable_comment(line) || return false + is_disable_all_comment(line) && return true + is_disable_rule_comment(line, rule) && return true + return false +end +is_disable_comment(str::AbstractString) = + startswith(str, "# lint-disable-next-line") +is_disable_all_comment(str::AbstractString) = str == "# lint-disable-next-line" +function is_disable_rule_comment(str::AbstractString, rule::Rule) + is_disable_comment(str) || return false + split_command_from_rules = split(str, ":") + length(split_command_from_rules) == 2 || return false + rules_list = strip.(split(split_command_from_rules[2], ",")) + rule.name in rules_list && return true + # Allow disabling by rule description as well. + return startswith(rule.description, lstrip(split_command_from_rules[2])) +end + +# Utils +# ----- + +# Return (index_line, index_column, annotation) for a given offset in a source +# Useful in tests? +function convert_offset_to_line(offset::Integer, source::String) + return convert_offset_to_line_from_lines(offset, split(source, "\n")) +end + +# Return the lint next-line annotation, if there is one, at the end of `line`. +# Return +# * `nothing` if there is no `lint-disable-next-line` annotation. +# * ""::SubString if the end of the line is "lint-disable-next-line". +# * s::SubString if the end of the line is "lint-disable_next_line: $s" +function annotation_for_next_line(line::AbstractString) + if endswith(line, "lint-disable-next-line") + return "" + end + # An annotation must be in a comment and not contain any `#` or `"` characters. + m = match(r"# lint-disable-next-line: *([^\"#]+)$", line) + return isnothing(m) ? nothing : m[1] +end + +function annotation_for_this_line(line::AbstractString) + if endswith(line, "lint-disable-line") + return "" + end + # An annotation must be in a comment and not contain any `#` or `"` characters. + m = match(r"#\h*lint-disable-line: *([^\"#]+)$", line) + return isnothing(m) ? nothing : m[1] +end + +# Return a triple: (line::Int, column::Int, annotation::Option(String)) +# Return (index_line, index_column, annotation) for a given offset in a source +# +# `annotation` could be either `nothing`, "lint-disable-line", or +# `"lint-disable-line: $ERROR_MSG_TO_IGNORE"` +# +# Note: `offset` is measured in codepoints. The returned `column` is a character +# offset, not a codepoint offset. +function convert_offset_to_line_from_lines(offset::Integer, all_lines) + offset < 0 && throw(BoundsError("source", offset)) + + current_codepoint = 1 + # In these annotations, "" means "lint-disable-line", a nonempty string `s` means + # "lint_disable_line: $s", and nothing means there's no applicable annotation. + prev_annotation::Union{Nothing, SubString} = nothing + this_annotation::Union{Nothing, SubString} = nothing + for (line_number, line) in enumerate(all_lines) + this_annotation = annotation_for_this_line(line) + # current_codepoint + sizeof(line) is possibly pointing at the newline that isn't + # actually stored in `line`. + if offset in current_codepoint:(current_codepoint + sizeof(line)) + index_in_line = offset - current_codepoint + 1 # possibly off the end by 1. + if !isnothing(this_annotation) + annotation = this_annotation + elseif !isnothing(prev_annotation) + annotation = prev_annotation + else + annotation = nothing + end + if !isnothing(annotation) + if annotation == "" + annotation = "lint-disable-line" + else + annotation = "lint-disable-line: " * annotation + end + end + if index_in_line == sizeof(line) + 1 + return line_number, length(line) + 1, annotation + else + return line_number, length(line, 1, index_in_line), annotation + end + end + prev_annotation = annotation_for_next_line(line) + current_codepoint += sizeof(line) + 1 # 1 is for the newline + end + throw(BoundsError("source", offset)) +end + +function print_header(::PreCommitFormat, io::IO, rootpath::String) + # printstyled(io, "-" ^ 10 * " $(rootpath)\n", color=:blue) + # printstyled(io, "**List of Fatal violations, please address them to commit these files**\n", color=:red) +end + +print_footer(::PreCommitFormat, io::IO) = nothing +function print_summary(::PreCommitFormat, io::IO, result::LintGlobalReport) + print_summary(PlainFormat(), io, result) + return printstyled(io, "Note that the list above only show fatal violations\n", color = :red) +end + +function print_report(::PreCommitFormat, io::IO, lint_report::LintRuleReport, result::LintGlobalReport) + should_print_report(result) || return + # Do not print anything if it is not a fatal violation + is_fatal(lint_report) || return + printstyled(io, "Line $(lint_report.line), column $(lint_report.column):", color = :green) + print(io, " ") + print(io, lint_report.msg) + print(io, " ") + println(io, lint_report.file) + return result.printout_count += 1 +end + +should_print_report(result) = result.printout_count <= MAX_REPORTED_ERRORS + +function print_header(::PlainFormat, io::IO, rootpath::String) + return printstyled(io, "-"^10 * " $(rootpath)\n", color = :blue) +end + +function print_report(::PlainFormat, io::IO, lint_report::LintRuleReport, result::LintGlobalReport) + should_print_report(result) || return + printstyled(io, "Line $(lint_report.line), column $(lint_report.column):", color = :green) + print(io, " ") + print(io, lint_report.msg) + print(io, " ") + println(io, lint_report.file) + return result.printout_count += 1 + +end + +function print_summary( + ::PlainFormat, + io::IO, + result::LintGlobalReport + ) + nb_rulereports = result.violations_count + result.recommendations_count + result.fatalviolations_count + return if iszero(nb_rulereports) + printstyled(io, "No potential threats were found.\n", color = :green) + else + plural = nb_rulereports > 1 ? "s are" : " is" + plural_vio = result.violations_count > 1 ? "s" : "" + plural_fatal = result.fatalviolations_count > 1 ? "s" : "" + plural_rec = result.recommendations_count > 1 ? "s" : "" + printstyled(io, "$(nb_rulereports) potential threat$(plural) found: ", color = :red) + printstyled(io, "$(result.fatalviolations_count) fatal violation$(plural_fatal), $(result.violations_count) violation$(plural_vio) and $(result.recommendations_count) recommendation$(plural_rec)\n", color = :red) + end +end + +function print_footer(::PlainFormat, io::IO) + return printstyled(io, "-"^10 * "\n\n", color = :blue) +end + +print_header(::MarkdownFormat, io::IO, rootpath::String) = nothing +print_footer(::MarkdownFormat, io::IO) = nothing + +# Remove a leading '/' if the file starts with one. This is necessary to build the URL +# Remove the prefix mentioned in the Markdown from the file_name +function remove_prefix_from_filename(file_name::String, file_prefix_to_remove::String) + corrected_file_name = first(file_name) == '/' ? file_name[2:end] : file_name + if startswith(corrected_file_name, file_prefix_to_remove) + corrected_file_name = corrected_file_name[(length(file_prefix_to_remove) + 1):end] + end + return corrected_file_name +end + +function remove_prefix_from_filename(file_name::String, format::MarkdownFormat) + return remove_prefix_from_filename(file_name, format.file_prefix_to_remove) +end + +function print_report(format::MarkdownFormat, io::IO, lint_report::LintRuleReport, result::LintGlobalReport) + should_print_report(result) || return + + corrected_file_name = remove_prefix_from_filename(lint_report.file, format) + + coordinates = "Line $(lint_report.line), column $(lint_report.column):" + if !isempty(format.github_branch_name) && !isempty(format.github_repository_name) + extended_coordinates = "[$(coordinates)](https://github.com/$(format.github_repository_name)/blob/$(format.github_branch_name)/$(corrected_file_name)#L$(lint_report.line))" + print(io, " - **$(extended_coordinates)** $(lint_report.msg) $(lint_report.file)\n") + else + print(io, " - **$(coordinates)** $(lint_report.msg) $(lint_report.file)\n") + end + + # Produce workflow command to see results in the PR file changed tab: + # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#example-setting-an-error-message + println(format.stream_workflowcommand, "::error file=$(corrected_file_name),line=$(lint_report.line),col=$(lint_report.column)::$(lint_report.msg)") + return result.printout_count += 1 +end + +print_summary( + ::MarkdownFormat, + io::IO, + result::LintGlobalReport +) = nothing + +function print_datadog_report( + json_output::IO, + report_as_string::String, + files_count::Integer, + violation_count::Integer, + recommandation_count::Integer, + fatalviolations_count::Integer, + branch::String, + rules_count::Integer, + ) + event = Dict( + :source => "ReLint", + :specversion => "1.1", + :type => "result", + :time => string(now(UTC)), #Dates.format(now(UTC), "yyyy-mm-ddTHH:MM:SSZ"), # RFC3339 format + :data => Dict( + :report_as_string => report_as_string, + :files_count => files_count, + :violation_count => violation_count, + :recommandation_count => recommandation_count, + :fatalviolations_count => fatalviolations_count, + :branch => branch, + :rules_count => rules_count, # Added in specversion 1.1 + ) + ) + return println(json_output, JSON3.write(event)) +end diff --git a/src/linting/extended_checks.jl b/src/linting/extended_checks.jl deleted file mode 100644 index de5eecb..0000000 --- a/src/linting/extended_checks.jl +++ /dev/null @@ -1,34 +0,0 @@ -struct LintContext - rules_to_run::Vector{Rule} - regex_exclusions #::Vector{LintFileExclusion} - - function LintContext(dts_as_str::Vector{String}) - dt = DataType[] - for dt_as_str in dts_as_str - ind = findfirst(t -> nameof(t) == Symbol(dt_as_str), all_rules()) - isnothing(ind) && error("Non-existing rule: $(dt_as_str)") - push!(dt, all_rules()[ind]) - end - return new(dt, []) - end - - LintContext(s::Vector{Rule}) = new(s, []) - LintContext() = new(all_rules(), []) - LintContext(a, b) = new(a, b) -end - -abstract type LintRule end -abstract type ASTLintRule <: LintRule end -abstract type RecommendationLintRule <: ASTLintRule end -abstract type ViolationLintRule <: ASTLintRule end -abstract type FatalLintRule <: ASTLintRule end - -include("text_lint_rules.jl") - -function all_rules() - return [ - values(RECOMMENDATIONS)..., - values(VIOLATIONS)..., - values(FATAL_VIOLATIONS)... - ] -end diff --git a/src/linting/text_lint_rules.jl b/src/linting/text_lint_rules.jl index 906fcc2..fc50585 100644 --- a/src/linting/text_lint_rules.jl +++ b/src/linting/text_lint_rules.jl @@ -1,3 +1,4 @@ +abstract type LintRule end abstract type LineLintRule <: LintRule end abstract type LineRecommendationLintRule <: LineLintRule end abstract type LineViolationLintRule <: LineLintRule end @@ -17,4 +18,4 @@ function check(t::TodoJiraIssueRule, line::String, markers::Dict{Symbol,String}) (contains(line, "TODO") && !contains(line, r"TODO\s?\(RAI-\d+\)")) && return true, "`TODO` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue." return false, "" -end \ No newline at end of file +end diff --git a/test/common.jl b/test/common.jl index 2f67695..fc6e511 100644 --- a/test/common.jl +++ b/test/common.jl @@ -2,7 +2,7 @@ using ReLint: ReLint, run_lint_on_text, convert_offset_to_line, convert_offset_to_line_from_lines, MarkdownFormat, PlainFormat, has_values -using ReLint: LintResult, LintContext +using ReLint: LintGlobalReport, LintContext using Test using JSON3 diff --git a/test/interface.jl b/test/interface.jl index 2caab7f..1e220e1 100644 --- a/test/interface.jl +++ b/test/interface.jl @@ -569,7 +569,7 @@ end @testset "Running on a directory" begin @testset "Non empty directory" begin local r - r = LintResult() + r = LintGlobalReport() formatters = [ReLint.PlainFormat(), ReLint.MarkdownFormat()] for formatter in formatters @@ -587,7 +587,7 @@ end @testset "Empty directory" begin mktempdir() do dir - @test ReLint.run_lint(dir) == LintResult() + @test ReLint.run_lint(dir) == LintGlobalReport() end end end @@ -866,25 +866,25 @@ end @test !isnothing(match(expected, result)) end -@testset "Arithmetic LintResult" begin - l1 = LintResult() - l2 = LintResult(1, 2, 3) - l3 = LintResult(10, 20, 30) - l6 = LintResult(10, 20, 30, 40) - l4 = LintResult(10, 20, 30, 40, ["foo.jl"], 100, []) - l5 = LintResult(10, 20, 30, 40, ["foo2.jl"], 250) +@testset "Arithmetic LintGlobalReport" begin + l1 = LintGlobalReport() + l2 = LintGlobalReport(1, 2, 3) + l3 = LintGlobalReport(10, 20, 30) + l6 = LintGlobalReport(10, 20, 30, 40) + l4 = LintGlobalReport(10, 20, 30, 40, ["foo.jl"], 100, []) + l5 = LintGlobalReport(10, 20, 30, 40, ["foo2.jl"], 250) @test l1 == l1 - @test l1 == LintResult() + @test l1 == LintGlobalReport() # @test (l1 + l2) == l2 - # @test (l3 + l2) == LintResult(11, 22, 33) + # @test (l3 + l2) == LintGlobalReport(11, 22, 33) @test l4 != l5 @test l3 != l4 @test l3 != l5 append!(l4, l5) - @test l4 == LintResult(20, 40, 60, 80, ["foo.jl", "foo2.jl"], 350) + @test l4 == LintGlobalReport(20, 40, 60, 80, ["foo.jl", "foo2.jl"], 350) end @testset "PreCommit format" begin @@ -952,9 +952,9 @@ end end @testset "Printing LintReport" begin - using ReLint: LintRuleReport, LintResult, print_report, PreCommitFormat, is_fatal + using ReLint: LintRuleReport, LintGlobalReport, print_report, PreCommitFormat, is_fatal - result = LintResult() + result = LintGlobalReport() lint_report = LintRuleReport(ReLint.FATAL_VIOLATIONS["@generated"], "error") io = IOBuffer() print_report(PreCommitFormat(), io, lint_report, result) From 7fd0b55196912c6917ddef3d9b51742baff40234 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 9 Apr 2026 14:11:02 +0200 Subject: [PATCH 39/62] Remove annotation-specific utils (not relevant anymore) --- src/interface.jl | 77 ----------------------------------------------- test/common.jl | 4 +-- test/interface.jl | 46 ---------------------------- test/runtests.jl | 1 - 4 files changed, 1 insertion(+), 127 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index d0c33a7..d9cdef0 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -613,83 +613,6 @@ end # Utils # ----- -# Return (index_line, index_column, annotation) for a given offset in a source -# Useful in tests? -function convert_offset_to_line(offset::Integer, source::String) - return convert_offset_to_line_from_lines(offset, split(source, "\n")) -end - -# Return the lint next-line annotation, if there is one, at the end of `line`. -# Return -# * `nothing` if there is no `lint-disable-next-line` annotation. -# * ""::SubString if the end of the line is "lint-disable-next-line". -# * s::SubString if the end of the line is "lint-disable_next_line: $s" -function annotation_for_next_line(line::AbstractString) - if endswith(line, "lint-disable-next-line") - return "" - end - # An annotation must be in a comment and not contain any `#` or `"` characters. - m = match(r"# lint-disable-next-line: *([^\"#]+)$", line) - return isnothing(m) ? nothing : m[1] -end - -function annotation_for_this_line(line::AbstractString) - if endswith(line, "lint-disable-line") - return "" - end - # An annotation must be in a comment and not contain any `#` or `"` characters. - m = match(r"#\h*lint-disable-line: *([^\"#]+)$", line) - return isnothing(m) ? nothing : m[1] -end - -# Return a triple: (line::Int, column::Int, annotation::Option(String)) -# Return (index_line, index_column, annotation) for a given offset in a source -# -# `annotation` could be either `nothing`, "lint-disable-line", or -# `"lint-disable-line: $ERROR_MSG_TO_IGNORE"` -# -# Note: `offset` is measured in codepoints. The returned `column` is a character -# offset, not a codepoint offset. -function convert_offset_to_line_from_lines(offset::Integer, all_lines) - offset < 0 && throw(BoundsError("source", offset)) - - current_codepoint = 1 - # In these annotations, "" means "lint-disable-line", a nonempty string `s` means - # "lint_disable_line: $s", and nothing means there's no applicable annotation. - prev_annotation::Union{Nothing, SubString} = nothing - this_annotation::Union{Nothing, SubString} = nothing - for (line_number, line) in enumerate(all_lines) - this_annotation = annotation_for_this_line(line) - # current_codepoint + sizeof(line) is possibly pointing at the newline that isn't - # actually stored in `line`. - if offset in current_codepoint:(current_codepoint + sizeof(line)) - index_in_line = offset - current_codepoint + 1 # possibly off the end by 1. - if !isnothing(this_annotation) - annotation = this_annotation - elseif !isnothing(prev_annotation) - annotation = prev_annotation - else - annotation = nothing - end - if !isnothing(annotation) - if annotation == "" - annotation = "lint-disable-line" - else - annotation = "lint-disable-line: " * annotation - end - end - if index_in_line == sizeof(line) + 1 - return line_number, length(line) + 1, annotation - else - return line_number, length(line, 1, index_in_line), annotation - end - end - prev_annotation = annotation_for_next_line(line) - current_codepoint += sizeof(line) + 1 # 1 is for the newline - end - throw(BoundsError("source", offset)) -end - function print_header(::PreCommitFormat, io::IO, rootpath::String) # printstyled(io, "-" ^ 10 * " $(rootpath)\n", color=:blue) # printstyled(io, "**List of Fatal violations, please address them to commit these files**\n", color=:red) diff --git a/test/common.jl b/test/common.jl index fc6e511..b3be81d 100644 --- a/test/common.jl +++ b/test/common.jl @@ -1,6 +1,4 @@ -using ReLint: ReLint, run_lint_on_text, convert_offset_to_line, - convert_offset_to_line_from_lines, MarkdownFormat, PlainFormat, - has_values +using ReLint: ReLint, run_lint_on_text, MarkdownFormat, PlainFormat, has_values using ReLint: LintGlobalReport, LintContext using Test diff --git a/test/interface.jl b/test/interface.jl index 1e220e1..c58ba67 100644 --- a/test/interface.jl +++ b/test/interface.jl @@ -1,19 +1,3 @@ -@testset "offset to line" begin - source = """ - function f() - return Threads.nthreads() - end - """ - @test_throws BoundsError convert_offset_to_line(-1, source) - @test_throws BoundsError convert_offset_to_line(length(source) + 2, source) - - @test convert_offset_to_line(10, source) == (1, 10, nothing) - @test convert_offset_to_line(20, source) == (2, 7, nothing) - @test convert_offset_to_line(42, source) == (2, 29, nothing) - @test convert_offset_to_line(46, source) == (3, 3, nothing) - -end - @testset "Run several times on same file" begin mktempdir() do dir open(joinpath(dir, "foo.jl"), "w") do io @@ -738,7 +722,6 @@ end function f() # lint-disable-next-line @async 1 + 2 - @async 1 + 3 end """) @@ -746,7 +729,6 @@ end function f() @async 1 + 2 # lint-disable-next-line - @async 1 + 3 end """) @@ -757,18 +739,6 @@ end @async 1 + 3 end """) - - source = """ - function f() - # lint-disable-next-line - @async 1 + 2 - - @async 1 + 3 - end - """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(46, source_lines) == (3, 4, "lint-disable-line") - @test convert_offset_to_line_from_lines(64, source_lines) == (5, 4, nothing) end @testset "Locally disabling rule 01" begin @@ -778,10 +748,6 @@ end @async 1 + 1 end """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) - @test convert_offset_to_line_from_lines(91, source_lines) == (3, 14, "lint-disable-line: Use `@spawn` instead of `@async`.") - @test !lint_has_error_test(source) end @@ -792,10 +758,6 @@ end @async unsafe_foo(12) end """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) - @test convert_offset_to_line_from_lines(91, source_lines) == (3, 14, "lint-disable-line: Use `@spawn` instead of `@async`.") - @test lint_has_error_test(source) @test lint_test(source, "Line 3, column 12: An `unsafe_` function should be called only from an `unsafe_` function.") @@ -820,10 +782,6 @@ end @async 1 + 1 end """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) - @test convert_offset_to_line_from_lines(91, source_lines) == (3, 15, "lint-disable-line: Use `@spawn` instead of `@async`.") - @test !lint_has_error_test(source) end @@ -834,10 +792,6 @@ end @async 1 + 1 end """ - source_lines = split(source, "\n") - @test convert_offset_to_line_from_lines(30, source_lines) == (2, 17, nothing) - @test convert_offset_to_line_from_lines(91, source_lines) == (3, 13, "lint-disable-line: Use `@spawn` instead of `@async`.") - @test !lint_has_error_test(source) end end diff --git a/test/runtests.jl b/test/runtests.jl index a715436..65bb33e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,5 @@ using ReLint using Test -using ReLint: convert_offset_to_line_from_lines include(joinpath(@__DIR__, "common.jl")) From 6bd96ed751716930e9a9f9954281b9b821187c76 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 9 Apr 2026 14:21:51 +0200 Subject: [PATCH 40/62] Remove comments; rewrite some short form functions; add comment sections --- src/interface.jl | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index d9cdef0..3eeca3a 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -64,6 +64,7 @@ LintGlobalReport(a, b, c, d, e) = LintGlobalReport(a, b, c, d, e, 0) LintGlobalReport(a, b, c, d, e, f) = LintGlobalReport(a, b, c, d, e, f, LintRuleReport[]) LintGlobalReport(a, b, c, d, e, f, g) = LintGlobalReport(a, b, c, d, e, f, LintRuleReport[], "master") +# Base overwrites function Base.append!(l1::LintGlobalReport, l2::LintGlobalReport) l1.files_count += l2.files_count @@ -86,15 +87,14 @@ function Base.:(==)(l1::LintGlobalReport, l2::LintGlobalReport) l1.lintrule_reports == l2.lintrule_reports end -function is_already_linted(l::LintGlobalReport, filename) - return filename in l.linted_files -end +# Utils -function has_values(l::LintGlobalReport, a, b, c) - return l.files_count == a && - l.violations_count == b && - l.recommendations_count == c -end +is_already_linted(l::LintGlobalReport, filename) = filename in l.linted_files + +has_values(l::LintGlobalReport, a, b, c) = + l.files_count == a && + l.violations_count == b && + l.recommendations_count == c # Precommit config # ================ @@ -124,21 +124,16 @@ function extract_file_exclusions_from_precommit_file(pre_commit_file::String) # We are outside the ReLint repo entry if contains(line, "lint-fatal-checks") # We are inside the ReLint repo entry - # @info :inside_repo_entry state = :inside_repo_entry continue end elseif state == :inside_repo_entry - # @info :inside_repo_entry - if contains(line, "(?x)^(") # We are inside the ReLint repo entry state = :inside_exclusion_entry continue end elseif state == :inside_exclusion_entry - # @info :inside_exclusion_entry - # We are leaving the exclusion portion if contains(line, ")") state = :outside_repo_entry @@ -610,13 +605,10 @@ function is_disable_rule_comment(str::AbstractString, rule::Rule) return startswith(rule.description, lstrip(split_command_from_rules[2])) end -# Utils -# ----- +# Printing +# -------- -function print_header(::PreCommitFormat, io::IO, rootpath::String) - # printstyled(io, "-" ^ 10 * " $(rootpath)\n", color=:blue) - # printstyled(io, "**List of Fatal violations, please address them to commit these files**\n", color=:red) -end +print_header(::PreCommitFormat, io::IO, rootpath::String) = nothing print_footer(::PreCommitFormat, io::IO) = nothing function print_summary(::PreCommitFormat, io::IO, result::LintGlobalReport) @@ -731,7 +723,7 @@ function print_datadog_report( :source => "ReLint", :specversion => "1.1", :type => "result", - :time => string(now(UTC)), #Dates.format(now(UTC), "yyyy-mm-ddTHH:MM:SSZ"), # RFC3339 format + :time => string(now(UTC)), :data => Dict( :report_as_string => report_as_string, :files_count => files_count, @@ -739,7 +731,7 @@ function print_datadog_report( :recommandation_count => recommandation_count, :fatalviolations_count => fatalviolations_count, :branch => branch, - :rules_count => rules_count, # Added in specversion 1.1 + :rules_count => rules_count, ) ) return println(json_output, JSON3.write(event)) From dfd746f4398c0dc606dd1299bd35583721fb523c Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 9 Apr 2026 14:28:10 +0200 Subject: [PATCH 41/62] Specific imports --- src/interface.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 3eeca3a..4924024 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,5 +1,5 @@ -using Dates -using JSON3 +import JSON3 +using Dates: now, UTC global MAX_REPORTED_ERRORS = 60 # 1_000_000 From 56a5b967968feab4b0b42f00df4e7e47a756001b Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 9 Apr 2026 14:37:51 +0200 Subject: [PATCH 42/62] Reorganise rules --- src/ReLint.jl | 9 +- src/linting/argus-rules.jl | 826 ---------------------- src/rules/fatal.jl | 259 +++++++ src/rules/recommendations.jl | 177 +++++ src/rules/rules.jl | 13 + src/{linting => rules}/text_lint_rules.jl | 0 src/rules/utils.jl | 26 + src/rules/violations.jl | 347 +++++++++ 8 files changed, 823 insertions(+), 834 deletions(-) delete mode 100644 src/linting/argus-rules.jl create mode 100644 src/rules/fatal.jl create mode 100644 src/rules/recommendations.jl create mode 100644 src/rules/rules.jl rename src/{linting => rules}/text_lint_rules.jl (100%) create mode 100644 src/rules/utils.jl create mode 100644 src/rules/violations.jl diff --git a/src/ReLint.jl b/src/ReLint.jl index fd82f4b..c34bb6a 100644 --- a/src/ReLint.jl +++ b/src/ReLint.jl @@ -3,17 +3,10 @@ module ReLint using Argus using JuliaSyntax -all_rules() = [ - values(RECOMMENDATIONS)..., - values(VIOLATIONS)..., - values(FATAL_VIOLATIONS)... -] - include("interface.jl") function __init__() - include(joinpath(@__DIR__, "linting/argus-rules.jl")) - include(joinpath(@__DIR__, "linting/text_lint_rules.jl")) + include(joinpath(@__DIR__, "rules/rules.jl")) end end diff --git a/src/linting/argus-rules.jl b/src/linting/argus-rules.jl deleted file mode 100644 index 990d286..0000000 --- a/src/linting/argus-rules.jl +++ /dev/null @@ -1,826 +0,0 @@ -using Argus - -# Rule metadata -# ============= - -# TODO: `string_literal` syntax class -@define_rule_hook :exclude_files begin - args = @pattern [{files}...] - - pre_check = @check [:files] begin - file_names = map(s -> s.children[1].val, files.src) - if any(contains.(current_file(), file_names)) - skip_match() - end - end - - post_check = nothing -end - -@define_rule_hook :only_in_dirs begin - args = @pattern [{dirs}...] - - pre_check = @check [:dirs] begin - dir_names = map(s -> s.children[1].val, dirs.src) - if !any(contains.(current_file(), dir_names)) - skip_match() - end - end - - post_check = nothing -end - -# Recommendation rules -# ==================== - -RECOMMENDATIONS = RuleGroup("recommendations") - -# CcallRule -RECOMMENDATIONS["ccall"] = Rule( - "ccall", - "`ccall` should be used with extreme caution.", - @pattern ccall({_}...) -) - -# FinalizerRule -RECOMMENDATIONS["finalizer"] = Rule( - "finalizer", - "`finalizer(_,_)` should not be used.", - @pattern finalizer({_}...) -) - -# CFunctionRule -RECOMMENDATIONS["@cfunction"] = Rule( - "@cfunction", - "`@cfunction` should not be used.", - @pattern @cfunction({_}...) -) - -# UnlockRule -RECOMMENDATIONS["unlock"] = Rule( - "unlock", - "`unlock` should be used with extreme caution.", - @pattern unlock({_}) -) - -# YieldRule -RECOMMENDATIONS["yield"] = Rule( - "yield", - "`yield` should be used with extreme caution.", - @pattern yield() -) - -# SleepRule -RECOMMENDATIONS["sleep"] = Rule( - "sleep", - "`sleep` should be used with extreme caution.", - @pattern sleep({_}) -) - -# InboundsRule -RECOMMENDATIONS["@inbounds"] = Rule( - "@inbounds", - "`@inbounds` should be used with extreme caution.", - @pattern @inbounds({_}) -) - -# ThreadsRule -RECOMMENDATIONS["@threads"] = Rule( - "@threads", - "`@threads` should be used with extreme caution.", - @pattern ~or(@threads({_}), Threads.@threads({_})) -) - -# SyncRule -RECOMMENDATIONS["@sync"] = Rule( - "@sync", - "`@sync` should be used with extreme caution.", - @pattern ~or(@sync({_}), Threads.@sync({_})) -) - -# SplattingRule -RECOMMENDATIONS["splatting"] = Rule( - "splatting", - """ - Splatting (`...`) should be used with extreme caution. Splatting from dynamically \ - sized containers could result in severe performance degradation. Splatting from \ - statically-sized tuples is usually okay.""", - (@pattern ~and( - {_}(@esc(({_}...)..., 1)), - ~not(~inside({_:::macrodef})) - )), - Dict( - :exclude_files => ["test.jl", "tests.jl"] - ) -) - -# InterpolationInSafeLogRule -RECOMMENDATIONS["interpolation in `@warnv_safe_to_log`"] = Rule( - "interpolation in `@warnv_safe_to_log`", - "Safe warning log has interpolation.", - @pattern @warnv_safe_to_log {_} "$({_}...)$({x:::identifier})$({_}...)" -) - -# ReturnTypeAnnotationRule -RECOMMENDATIONS["return type annotation"] = Rule( - "return type annotation", - """ - Avoid return type annotations `function foo()::Type`. \ - Return type annotations can hurt performance by forcing type conversions.""", - (@pattern ~and( - ~or( - function ({f}({args}...)::{ret_type}) {_}... end, - {_:::funcall}::{ret_type} = {_} - ), - ~not(~inside(@derived {_}...)) - )), - Dict( - :exclude_files => ["test/", "test.jl"] - ) -) - -# StringConcatenationRule -RECOMMENDATIONS["string concatenation"] = Rule( - "string concatenation", - """ - Prefer string interpolation or `string()` over `*` for string concatenation. \ - Use `\"\$(a)\$(b)\"` instead of `a * b`.""", - (@pattern ~or( - {_:::string} * {_}, - {_} * {_:::string} - )), - Dict( - :exclude_files => ["test/", "test.jl"] - ) -) - -# NoGlobalVariablesRule -# -# Removed because it is too similar to "non-const untyped global" in `VIOLATIONS`. - -# IsNothingPerformanceRule -RECOMMENDATIONS["isnothing"] = Rule( - "isnothing", - """ - In performance-critical code, prefer `x === nothing` or `x isa Nothing` over \ - `isnothing(x)`""", - (@pattern isnothing({_})), - Dict( - :only_in_dirs => ["src/Compiler/", "packages/Salsa/"], - :exclude_files => ["test/", "test.jl"] - ) -) - -# ClosureCaptureByValueRule -RECOMMENDATIONS["closure capture by reference"] = Rule( - "closure capture by reference", - """ - Nested function may capture variables by reference, causing boxing and type \ - instability. Consider using `let x = x` to capture by value for better performance.""", - (@pattern ~and( - ~or( - ({_}...) -> {_}, # Anonymous function. - {_}({_}...) do {x}... # `do` block. - {_}... - end, - {_:::fundef} # Function definition - ), - ~inside({_:::fundef}), - ~not(~inside(let {_}... - {_}... - end)) - )), - Dict( - :only_in_dirs => ["src/Compiler/"], - :exclude_files => ["test/", "test.jl"] - ) -) - -# AccessingENVRuleq -RECOMMENDATIONS["mutating ENV"] = Rule( - "mutating ENV", - """ - Mutating `ENV` is not thread-safe, only do so in a single-threaded context.""", - @pattern ~or( - ENV[{_}] = {_}, - ~and( - {mutating_fun}(ENV, {_}...), - ~when([:mutating_fun], - mutating_fun.name in ["pop!", "delete!", "setindex!", "get!", "push!"]) - ) - ) -) - -# Violation rules -# =============== - -VIOLATIONS = RuleGroup("violations") - -# AsyncRule -# -# TODO: Template for `@spawn`. -VIOLATIONS["@async"] = Rule( - "@async", - "Use `@spawn` instead of `@async`.", - @pattern ~or(@async({_}), Threads.@async({_})) -) - -# InitializingWithFunctionRule -# -# TODO: Pattern variables in rule messages. -VIOLATIONS["initializing with `nthreads`"] = Rule( - "initializing with `nthreads`", - "`Threads.nthreads()` should not be used in a constant variable.", - @pattern const {_} = Threads.nthreads() -) -VIOLATIONS["initializing with `is_local_deployment`"] = Rule( - "initializing with `is_local_deployment`", - "`is_local_deployment()` should not be used in a constant variable.", - @pattern ~or( - :(const {_} = is_local_deployment()), - :(const {_} = Deployment.is_local_deployment()) - ) -) - -# ArrayWithNoTypeRule -VIOLATIONS["array with no specific type"] = Rule( - "array with no specific type", - "Need a specific Array type to be provided.", - (@pattern ~and( - [], - ~not(~inside(~and( - {m:::macrocall}, - ~when([:m], m.name in ["@match", "@matchrule"]) - ))) - )), - Dict( - :only_in_dirs => ["src/Compiler"] - ) -) - -# RemovePageRule -VIOLATIONS["remove_page"] = Rule( - "remove_page", - "`remove_page` should be used with extreme caution.", - @pattern remove_page({_}, {_}) -) - -# TaskRule -VIOLATIONS["Task"] = Rule( - "Task", - "`Task` should be used with extreme caution.", - @pattern Task({_}) -) - -# ErrorExceptionRule -VIOLATIONS["ErrorException"] = Rule( - "ErrorException", - "Use custom exception instead of the generic `ErrorException`.", - (@pattern ErrorException({_}...)), - Dict( - :exclude_files => ["test.jl", "tests.jl", "bench/", "Vectorized/Test"] - ) -) - -# ErrorRule -VIOLATIONS["error"] = Rule( - "error", - "Use custom exception instead of the generic `error()`.", - (@pattern error({_}...)), - Dict( - :exclude_files => ["test.jl", "tests.jl", "bench/", "Vectorized/Test"] - ) -) - -# UnsafeRule -register_syntax_class!(:unsafe_funcall, SyntaxClass( - "`unsafe_*` function call", - [ - @pattern begin - {_f:::funcall} - @when [:_f] startswith(_f.fun_name.name, r"_?unsafe_") - end - ] -)) -VIOLATIONS["unsafe_ function"] = Rule( - "unsafe_ function", - "An `unsafe_` function should be called only from an `unsafe_` function.", - @pattern ~and( - {_:::unsafe_funcall}, - ~not(~inside(~or( - {_:::unsafe_funcall} = {_}, - function ({_:::unsafe_funcall}) {_}... end - ))) - ) -) - -# InRule -VIOLATIONS["in"] = Rule( - "in", - "Use `tin(item,collection)` instead of the Julia's `in` or `∈`.", - @pattern ~or( - in({_}, {_}), - ∈({_}, {_}) - ) -) - -# HasKeyRule -VIOLATIONS["haskey"] = Rule( - "haskey", - "Use `thaskey(dict,key)` instead of the Julia's `haskey`.", - @pattern haskey({_}, {_}) -) - -# EqualRule -VIOLATIONS["equal"] = Rule( - "equal", - "Use `tequal(dict,key)` instead of the Julia's `equal`.", - @pattern equal({_}, {_}) -) - -# UvRule -VIOLATIONS["uv"] = Rule( - "uv", - "`uv_` functions should be used with extreme caution.", - @pattern ~and( - {f:::funcall}, - ~when([:f], startswith(f.fun_name.name, "uv_")) - ) -) - -# UnreachableBranchRule -# -# TODO: Find a way to express this in its most general form. Something like: -# -# @pattern ~or( -# if {cond} -# {_}... -# (elseif {_} {_}...)... -# elseif {cond} -# {_}... -# end, -# if {cond} -# {_}... -# (elseif {_} {_}...)... -# elseif {cond} -# {_}... -# else -# {_}... -# end -VIOLATIONS["unreachable branch"] = Rule( - "unreachable branch", - "Unreachable branch.", - @pattern ~or( - if {cond} - {_}... - elseif {cond} - {_}... - end, - if {cond} - {_}... - elseif {cond} - {_}... - else - {_}... - end, - if {cond} - {_}... - elseif {_} - {_}... - elseif {cond} - {_}... - end, - if {cond} - {_}... - elseif {_} - {_}... - elseif {cond} - {_}... - else - {_}... - end - ) -) - -# StringInterpolationRule -VIOLATIONS["string interpolation"] = Rule( - "string interpolation", - "Use \$(x) instead of \$x.", - @pattern ~and( - "$({_}...)$({x:::identifier})$({_}...)", - ~when([:x], !inside_parens(x.src)) - ) -) - -# RelPathAPIUsageRule -VIOLATIONS["RelPath"] = Rule( - "RelPath", - "Usage of type `RelPath` is not allowed in this context.", - (@pattern ~or( - {_}::RelPath, - RelPath({_}), - RelPath({_}, {_}) - )), - Dict( - :only_in_dirs => ["src/Compiler/Front"] - ) -) -VIOLATIONS["RelPath split_path"] = Rule( - "RelPath split_path", - "Usage of `RelPath` API method `split_path` is not allowed in this context.", - (@pattern split_path({_})), - Dict( - :only_in_dirs => ["src/Compiler/Front"] - ) -) -VIOLATIONS["RelPath drop_first"] = Rule( - "RelPath drop_first", - "Usage of `RelPath` API method `drop_first` is not allowed in this context.", - (@pattern drop_first({_})), - Dict( - :only_in_dirs => ["src/Compiler/Front"] - ) -) -VIOLATIONS["RelPath relpath_from_signature"] = Rule( - "RelPath relpath_from_signature", - """ - Usage of `RelPath` API method `relpath_from_signature` is not allowed \ - in this context.""", - (@pattern relpath_from_signature({_})), - Dict( - :only_in_dirs => ["src/Compiler/Front"] - ) -) - -# UseOfStaticThreads -VIOLATIONS["static threads"] = Rule( - "static threads", - """ - Use `Threads.@threads :dynamic` instead of `Threads.@threads :static`. Static threads \ - must not be used as generated tasks will not be able to migrate across threads.""", - @pattern ~or( - (@threads :static {_}...), - Threads.@threads :static {_}... - ) -) - -# NoImportRule -VIOLATIONS["no import"] = Rule( - "no import", - "Imports must be specified with `using` and not `import`.", - @pattern {_:::import} -) - -# NotImportingRAICodeRule -VIOLATIONS["no import RAICode"] = Rule( - "no import RAICode", - "Importing RAICode should be avoided (when possible).", - @pattern begin - {u:::using} - @when [:u] u.module.module_name == "RAICode" - end -) - -# BareUsingRule -VIOLATIONS["bare using"] = Rule( - "bare using", - "Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`.", - (@pattern begin - {u:::using} - @when [:u] isempty(u.ids.src) - end), - Dict( - :exclude_files => ["test/", "test.jl"] - ) -) - -# UntypedArrayComprehensionRule -VIOLATIONS["untyped array comprehension"] = Rule( - "untyped array comprehension", - """ - Need a specific Array type to be provided. \ - Use `T[x for x in xs]` instead of `[x for x in xs]`.""", - (@pattern [{_} for {_} in {_}]), - Dict( - :only_in_dirs => ["src/Compiler/"] - ) -) - -# ConstGlobalMissingTypeRule -register_syntax_class!(:const, SyntaxClass( - "const assignment", - [ - (@pattern const {_} = {_}), - (@pattern const global {_} = {_}) - ] -)) -VIOLATIONS["non-const untyped global"] = Rule( - "non-const untyped global", - """ - Global variable must have type annotation: `global x::Type = value`. \ - Use `const` for immutable globals.""", - (@pattern ~and( - :(global {_:::identifier} = {_}), - ~not(~inside({_:::const}, 1)) - )), - Dict( - :exclude_files => ["test/", "test.jl"] - ) -) - -# NotFullyParameterizedConstructorRule -register_syntax_class!(:loop, SyntaxClass( - "`for` or `while` loop", - [ - (@pattern (for {_} in {_} - {_}... - end)), - (@pattern (for {_} = {_} - {_}... - end)), - (@pattern (for {_} ∈ {_} - {_}... - end)), - (@pattern while {_} - {_}... - end) - ] -)) -VIOLATIONS["not fully parametrised constructor"] = Rule( - "not fully parametrised constructor", - """ - Avoid not-fully-parameterized constructor in loops. \ - Use a maker function instead for better performance.""", - (@pattern ~and( - {id:::identifier}({_}...), - ~inside({_:::loop}), - ~when([:id], startswith(id.name, r"[A-Z]")) - )), - Dict( - :only_in_dirs => ["src/Compiler"], - :exclude_files => ["test/", "test.jl"] - ) -) - -# Fatal rules -# =========== - -FATAL_VIOLATIONS = RuleGroup("fatal violations") - -# GeneratedRule -FATAL_VIOLATIONS["@generated"] = Rule( - "@generated", - "`@generated` should be used with extreme caution.", - @pattern @generated {_}... -) - -# LogStatementsMustBeSafe -# -# TODO: Syntax classes with parameters (e.g. macrocall(r"@info|@warn|@error|@debug")). -register_syntax_class!(:log_macro, SyntaxClass( - "log macro", - [ - @pattern begin - {m:::macrocall} - @when [:m] begin - log_macro_names = r"@info|@warn|@error|@debug" - startswith(m.name, log_macro_names) - end - end - ] -)) - -register_syntax_class!(:safe_macro_arg, SyntaxClass( - "safe macro argument", - [ - # Safe literal. - (@pattern begin - {arg} - @when [:arg] begin - s = arg.src - k = kind(s) - # Strings are not safe. - (k == K"string" || endswith(string(k), "String")) && return false - # `nothing` is safe. - !isnothing(s.data.val) && s.data.val == :nothing && return true - # Chars and literals other than strings are safe. - (JuliaSyntax.is_literal(s) || k == K"char") && return true - # Anything else is not safe. - return false - end - end), - # Argument wrapped in `@safe`. - (@pattern ~or(@safe({args}...), - SafeLogging.@safe({args}...), - $SafeLogging.@safe({args}...))), - # Assignment with safe rhs. - (@pattern {_} = {arg:::safe_macro_arg}) - ] -)) - -FATAL_VIOLATIONS["unsafe-logging"] = Rule( - "unsafe-logging", - """ - Unsafe logging statement. \ - You must enclose variables and strings with `@safe(...)`.""", - @pattern begin - {log_macro:::log_macro} - @when [:log_macro] begin - for s in log_macro.args - c = Argus.SYNTAX_CLASS_REGISTRY[:safe_macro_arg] - is_successful(syntax_match(c, s)) || return true - end - return false - end - end -) - -# AssertionStatementsMustBeSafe -# -# TODO: Syntax classes with parameters (e.g. macrocall(r"@assert|@dassert")). -register_syntax_class!(:assert_macro, SyntaxClass( - "assert macro", - [ - @pattern begin - {m:::macrocall} - @when [:m] begin - assert_macro_names = r"@assert|@dassert" - startswith(m.name, assert_macro_names) - end - end - ] -)) -FATAL_VIOLATIONS["unsafe-assert"] = Rule( - "unsafe-assert", - """ - Unsafe assertion statement. \ - You must enclose the message with `@safe(...)`.""", - @pattern begin - {assert_macro:::assert_macro} - @when [:assert_macro] begin - # Assert macro call with one argument is safe. - length(assert_macro.args) == 1 && return false - for s in assert_macro.args[2:end] - c = Argus.SYNTAX_CLASS_REGISTRY[:safe_macro_arg] - is_successful(syntax_match(c, s)) || return true - end - # All assert macro call arguments are safe. - return false - end - end -) - -# MustNotUseShow -# -# TODO: Template. -FATAL_VIOLATIONS["show"] = Rule( - "show", - "Do not use `@show`, use `@info` instead.", - @pattern @show {_}... -) - -# NoinlineAndLiteralRule -register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( - "@nonline call with non lit or id args", - [ - # Call or dotcall. - (@pattern begin - ~or( - @noinline(({_})({args}...)), - @noinline(({_}).({args}...)), - ) - @when [:args] begin - for s in args.src - k = kind(s) - k == K"parameters" && break - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return true - end - return false - end - end), - # Call or dotcall with kwargs. - (@pattern begin - ~or( - @noinline(({_})({args}...; {kwargs}...)), - @noinline(({_}).({args}...; {kwargs}...)) - ) - @when [:args, :kwargs] begin - for s in args.src - k = kind(s) - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return true - end - for kws in kwargs.src - k = kind(kws) - JuliaSyntax.is_literal(kws) || - JuliaSyntax.is_identifier(kws) || - k == K"char" || - k == K"string" || - k == K"..." || - k == K"=" && (JuliaSyntax.is_literal(kws.children[2]) || - JuliaSyntax.is_identifier(kws.children[2])) || - return true - end - return false - end - end), - # Macrocall. - (@pattern begin - @noinline({c:::macrocall}) - @when [:c] begin - for s in c.args - k = kind(s) - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == JuliaSyntax.K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return true - end - return false - end - end), - # Infix call. - (@pattern begin - ~or(@noinline({i:::infix_call}), - @noinline({i:::infix_dotcall})) - @when [:i] begin - # Check lhs. - s = i.lhs.src - k = kind(s) - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return true - # Check rhs. - s = i.rhs.src - k = kind(s) - JuliaSyntax.is_literal(s) || - JuliaSyntax.is_identifier(s) || - k == K"char" || - k == K"string" && all(c -> isa(c.data.val, String), s.children) || - k == K"..." || - return true - return false - end - end) - ] -)) -# TODO: Support for allowing match if the source and pattern differ only by a flag -# (e.g. INFIX_FLAG). -FATAL_VIOLATIONS["noinline with non-literal/identifier args"] = Rule( - "noinline with non-literal/identifier args", - """ - For call-site `@noinline` call, all args must be literals or identifiers only. \ - Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086). - """, - @pattern {n:::noinline_with_non_lit_or_id_args} -) - -# NoReturnInAnonymousFunctionRule -register_syntax_class!(:do_call_with_return, SyntaxClass( - "do call with explicit `return`", - [ - (@pattern begin - {f:::funcall} - @when [:f] begin - isempty(f.args.src) && return false - do_node = f.args.src[end] - return kind(do_node) == K"do" && - kind(do_node.children[2].children[end]) == K"return" - end - end), - (@pattern begin - {m:::macrocall} - @when [:m] begin - isempty(m.args) && return false - do_node = m.args[end] - return kind(do_node) == K"do" && - kind(do_node.children[2].children[end]) == K"return" - end - end) - ] -)) - -FATAL_VIOLATIONS["return in anonymous function"] = Rule( - "return in anonymous function", - "Anonymous function must not have `return` [Explanation](https://github.com/RelationalAI/RAIStyle#returning-from-a-closure).", - @pattern ~or( - ({_}...) -> begin - {_}... - return {_}... - end, - {_:::do_call_with_return} - ) -) diff --git a/src/rules/fatal.jl b/src/rules/fatal.jl new file mode 100644 index 0000000..6962e42 --- /dev/null +++ b/src/rules/fatal.jl @@ -0,0 +1,259 @@ +FATAL_VIOLATIONS = RuleGroup("fatal violations") + +# GeneratedRule +FATAL_VIOLATIONS["@generated"] = Rule( + "@generated", + "`@generated` should be used with extreme caution.", + @pattern @generated {_}... +) + +# LogStatementsMustBeSafe +# +# TODO: Syntax classes with parameters (e.g. macrocall(r"@info|@warn|@error|@debug")). +register_syntax_class!(:log_macro, SyntaxClass( + "log macro", + [ + @pattern begin + {m:::macrocall} + @when [:m] begin + log_macro_names = r"@info|@warn|@error|@debug" + startswith(m.name, log_macro_names) + end + end + ] +)) + +register_syntax_class!(:safe_macro_arg, SyntaxClass( + "safe macro argument", + [ + # Safe literal. + (@pattern begin + {arg} + @when [:arg] begin + s = arg.src + k = kind(s) + # Strings are not safe. + (k == K"string" || endswith(string(k), "String")) && return false + # `nothing` is safe. + !isnothing(s.data.val) && s.data.val == :nothing && return true + # Chars and literals other than strings are safe. + (JuliaSyntax.is_literal(s) || k == K"char") && return true + # Anything else is not safe. + return false + end + end), + # Argument wrapped in `@safe`. + (@pattern ~or(@safe({args}...), + SafeLogging.@safe({args}...), + $SafeLogging.@safe({args}...))), + # Assignment with safe rhs. + (@pattern {_} = {arg:::safe_macro_arg}) + ] +)) + +FATAL_VIOLATIONS["unsafe-logging"] = Rule( + "unsafe-logging", + """ + Unsafe logging statement. \ + You must enclose variables and strings with `@safe(...)`.""", + @pattern begin + {log_macro:::log_macro} + @when [:log_macro] begin + for s in log_macro.args + c = Argus.SYNTAX_CLASS_REGISTRY[:safe_macro_arg] + is_successful(syntax_match(c, s)) || return true + end + return false + end + end +) + +# AssertionStatementsMustBeSafe +# +# TODO: Syntax classes with parameters (e.g. macrocall(r"@assert|@dassert")). +register_syntax_class!(:assert_macro, SyntaxClass( + "assert macro", + [ + @pattern begin + {m:::macrocall} + @when [:m] begin + assert_macro_names = r"@assert|@dassert" + startswith(m.name, assert_macro_names) + end + end + ] +)) +FATAL_VIOLATIONS["unsafe-assert"] = Rule( + "unsafe-assert", + """ + Unsafe assertion statement. \ + You must enclose the message with `@safe(...)`.""", + @pattern begin + {assert_macro:::assert_macro} + @when [:assert_macro] begin + # Assert macro call with one argument is safe. + length(assert_macro.args) == 1 && return false + for s in assert_macro.args[2:end] + c = Argus.SYNTAX_CLASS_REGISTRY[:safe_macro_arg] + is_successful(syntax_match(c, s)) || return true + end + # All assert macro call arguments are safe. + return false + end + end +) + +# MustNotUseShow +# +# TODO: Template. +FATAL_VIOLATIONS["show"] = Rule( + "show", + "Do not use `@show`, use `@info` instead.", + @pattern @show {_}... +) + +# NoinlineAndLiteralRule +register_syntax_class!(:noinline_with_non_lit_or_id_args, SyntaxClass( + "@nonline call with non lit or id args", + [ + # Call or dotcall. + (@pattern begin + ~or( + @noinline(({_})({args}...)), + @noinline(({_}).({args}...)), + ) + @when [:args] begin + for s in args.src + k = kind(s) + k == K"parameters" && break + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return true + end + return false + end + end), + # Call or dotcall with kwargs. + (@pattern begin + ~or( + @noinline(({_})({args}...; {kwargs}...)), + @noinline(({_}).({args}...; {kwargs}...)) + ) + @when [:args, :kwargs] begin + for s in args.src + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return true + end + for kws in kwargs.src + k = kind(kws) + JuliaSyntax.is_literal(kws) || + JuliaSyntax.is_identifier(kws) || + k == K"char" || + k == K"string" || + k == K"..." || + k == K"=" && (JuliaSyntax.is_literal(kws.children[2]) || + JuliaSyntax.is_identifier(kws.children[2])) || + return true + end + return false + end + end), + # Macrocall. + (@pattern begin + @noinline({c:::macrocall}) + @when [:c] begin + for s in c.args + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == JuliaSyntax.K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return true + end + return false + end + end), + # Infix call. + (@pattern begin + ~or(@noinline({i:::infix_call}), + @noinline({i:::infix_dotcall})) + @when [:i] begin + # Check lhs. + s = i.lhs.src + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return true + # Check rhs. + s = i.rhs.src + k = kind(s) + JuliaSyntax.is_literal(s) || + JuliaSyntax.is_identifier(s) || + k == K"char" || + k == K"string" && all(c -> isa(c.data.val, String), s.children) || + k == K"..." || + return true + return false + end + end) + ] +)) +# TODO: Support for allowing match if the source and pattern differ only by a flag +# (e.g. INFIX_FLAG). +FATAL_VIOLATIONS["noinline with non-literal/identifier args"] = Rule( + "noinline with non-literal/identifier args", + """ + For call-site `@noinline` call, all args must be literals or identifiers only. \ + Pull complex args out to top-level. [RAI-35086](https://relationalai.atlassian.net/browse/RAI-35086). + """, + @pattern {n:::noinline_with_non_lit_or_id_args} +) + +# NoReturnInAnonymousFunctionRule +register_syntax_class!(:do_call_with_return, SyntaxClass( + "do call with explicit `return`", + [ + (@pattern begin + {f:::funcall} + @when [:f] begin + isempty(f.args.src) && return false + do_node = f.args.src[end] + return kind(do_node) == K"do" && + kind(do_node.children[2].children[end]) == K"return" + end + end), + (@pattern begin + {m:::macrocall} + @when [:m] begin + isempty(m.args) && return false + do_node = m.args[end] + return kind(do_node) == K"do" && + kind(do_node.children[2].children[end]) == K"return" + end + end) + ] +)) + +FATAL_VIOLATIONS["return in anonymous function"] = Rule( + "return in anonymous function", + "Anonymous function must not have `return` [Explanation](https://github.com/RelationalAI/RAIStyle#returning-from-a-closure).", + @pattern ~or( + ({_}...) -> begin + {_}... + return {_}... + end, + {_:::do_call_with_return} + ) +) diff --git a/src/rules/recommendations.jl b/src/rules/recommendations.jl new file mode 100644 index 0000000..6de7388 --- /dev/null +++ b/src/rules/recommendations.jl @@ -0,0 +1,177 @@ +RECOMMENDATIONS = RuleGroup("recommendations") + +# CcallRule +RECOMMENDATIONS["ccall"] = Rule( + "ccall", + "`ccall` should be used with extreme caution.", + @pattern ccall({_}...) +) + +# FinalizerRule +RECOMMENDATIONS["finalizer"] = Rule( + "finalizer", + "`finalizer(_,_)` should not be used.", + @pattern finalizer({_}...) +) + +# CFunctionRule +RECOMMENDATIONS["@cfunction"] = Rule( + "@cfunction", + "`@cfunction` should not be used.", + @pattern @cfunction({_}...) +) + +# UnlockRule +RECOMMENDATIONS["unlock"] = Rule( + "unlock", + "`unlock` should be used with extreme caution.", + @pattern unlock({_}) +) + +# YieldRule +RECOMMENDATIONS["yield"] = Rule( + "yield", + "`yield` should be used with extreme caution.", + @pattern yield() +) + +# SleepRule +RECOMMENDATIONS["sleep"] = Rule( + "sleep", + "`sleep` should be used with extreme caution.", + @pattern sleep({_}) +) + +# InboundsRule +RECOMMENDATIONS["@inbounds"] = Rule( + "@inbounds", + "`@inbounds` should be used with extreme caution.", + @pattern @inbounds({_}) +) + +# ThreadsRule +RECOMMENDATIONS["@threads"] = Rule( + "@threads", + "`@threads` should be used with extreme caution.", + @pattern ~or(@threads({_}), Threads.@threads({_})) +) + +# SyncRule +RECOMMENDATIONS["@sync"] = Rule( + "@sync", + "`@sync` should be used with extreme caution.", + @pattern ~or(@sync({_}), Threads.@sync({_})) +) + +# SplattingRule +RECOMMENDATIONS["splatting"] = Rule( + "splatting", + """ + Splatting (`...`) should be used with extreme caution. Splatting from dynamically \ + sized containers could result in severe performance degradation. Splatting from \ + statically-sized tuples is usually okay.""", + (@pattern ~and( + {_}(@esc(({_}...)..., 1)), + ~not(~inside({_:::macrodef})) + )), + Dict( + :exclude_files => ["test.jl", "tests.jl"] + ) +) + +# InterpolationInSafeLogRule +RECOMMENDATIONS["interpolation in `@warnv_safe_to_log`"] = Rule( + "interpolation in `@warnv_safe_to_log`", + "Safe warning log has interpolation.", + @pattern @warnv_safe_to_log {_} "$({_}...)$({x:::identifier})$({_}...)" +) + +# ReturnTypeAnnotationRule +RECOMMENDATIONS["return type annotation"] = Rule( + "return type annotation", + """ + Avoid return type annotations `function foo()::Type`. \ + Return type annotations can hurt performance by forcing type conversions.""", + (@pattern ~and( + ~or( + function ({f}({args}...)::{ret_type}) {_}... end, + {_:::funcall}::{ret_type} = {_} + ), + ~not(~inside(@derived {_}...)) + )), + Dict( + :exclude_files => ["test/", "test.jl"] + ) +) + +# StringConcatenationRule +RECOMMENDATIONS["string concatenation"] = Rule( + "string concatenation", + """ + Prefer string interpolation or `string()` over `*` for string concatenation. \ + Use `\"\$(a)\$(b)\"` instead of `a * b`.""", + (@pattern ~or( + {_:::string} * {_}, + {_} * {_:::string} + )), + Dict( + :exclude_files => ["test/", "test.jl"] + ) +) + +# NoGlobalVariablesRule +# +# Removed because it is too similar to "non-const untyped global" in `VIOLATIONS`. + +# IsNothingPerformanceRule +RECOMMENDATIONS["isnothing"] = Rule( + "isnothing", + """ + In performance-critical code, prefer `x === nothing` or `x isa Nothing` over \ + `isnothing(x)`""", + (@pattern isnothing({_})), + Dict( + :only_in_dirs => ["src/Compiler/", "packages/Salsa/"], + :exclude_files => ["test/", "test.jl"] + ) +) + +# ClosureCaptureByValueRule +RECOMMENDATIONS["closure capture by reference"] = Rule( + "closure capture by reference", + """ + Nested function may capture variables by reference, causing boxing and type \ + instability. Consider using `let x = x` to capture by value for better performance.""", + (@pattern ~and( + ~or( + ({_}...) -> {_}, # Anonymous function. + {_}({_}...) do {x}... # `do` block. + {_}... + end, + {_:::fundef} # Function definition + ), + ~inside({_:::fundef}), + ~not(~inside(let {_}... + {_}... + end)) + )), + Dict( + :only_in_dirs => ["src/Compiler/"], + :exclude_files => ["test/", "test.jl"] + ) +) + +# AccessingENVRuleq +RECOMMENDATIONS["mutating ENV"] = Rule( + "mutating ENV", + """ + Mutating `ENV` is not thread-safe, only do so in a single-threaded context.""", + @pattern ~or( + ENV[{_}] = {_}, + ~and( + {mutating_fun}(ENV, {_}...), + ~when([:mutating_fun], + mutating_fun.name in ["pop!", "delete!", "setindex!", "get!", "push!"]) + ) + ) +) diff --git a/src/rules/rules.jl b/src/rules/rules.jl new file mode 100644 index 0000000..355422c --- /dev/null +++ b/src/rules/rules.jl @@ -0,0 +1,13 @@ +using Argus + +include("utils.jl") +include("recommendations.jl") +include("violations.jl") +include("fatal.jl") +include("text_lint_rules.jl") + +all_rules() = [ + values(RECOMMENDATIONS)..., + values(VIOLATIONS)..., + values(FATAL_VIOLATIONS)... +] diff --git a/src/linting/text_lint_rules.jl b/src/rules/text_lint_rules.jl similarity index 100% rename from src/linting/text_lint_rules.jl rename to src/rules/text_lint_rules.jl diff --git a/src/rules/utils.jl b/src/rules/utils.jl new file mode 100644 index 0000000..6b74b13 --- /dev/null +++ b/src/rules/utils.jl @@ -0,0 +1,26 @@ +# TODO: `string_literal` syntax class +@define_rule_hook :exclude_files begin + args = @pattern [{files}...] + + pre_check = @check [:files] begin + file_names = map(s -> s.children[1].val, files.src) + if any(contains.(current_file(), file_names)) + skip_match() + end + end + + post_check = nothing +end + +@define_rule_hook :only_in_dirs begin + args = @pattern [{dirs}...] + + pre_check = @check [:dirs] begin + dir_names = map(s -> s.children[1].val, dirs.src) + if !any(contains.(current_file(), dir_names)) + skip_match() + end + end + + post_check = nothing +end diff --git a/src/rules/violations.jl b/src/rules/violations.jl new file mode 100644 index 0000000..4ea5ad7 --- /dev/null +++ b/src/rules/violations.jl @@ -0,0 +1,347 @@ +VIOLATIONS = RuleGroup("violations") + +# AsyncRule +# +# TODO: Template for `@spawn`. +VIOLATIONS["@async"] = Rule( + "@async", + "Use `@spawn` instead of `@async`.", + @pattern ~or(@async({_}), Threads.@async({_})) +) + +# InitializingWithFunctionRule +# +# TODO: Pattern variables in rule messages. +VIOLATIONS["initializing with `nthreads`"] = Rule( + "initializing with `nthreads`", + "`Threads.nthreads()` should not be used in a constant variable.", + @pattern const {_} = Threads.nthreads() +) +VIOLATIONS["initializing with `is_local_deployment`"] = Rule( + "initializing with `is_local_deployment`", + "`is_local_deployment()` should not be used in a constant variable.", + @pattern ~or( + :(const {_} = is_local_deployment()), + :(const {_} = Deployment.is_local_deployment()) + ) +) + +# ArrayWithNoTypeRule +VIOLATIONS["array with no specific type"] = Rule( + "array with no specific type", + "Need a specific Array type to be provided.", + (@pattern ~and( + [], + ~not(~inside(~and( + {m:::macrocall}, + ~when([:m], m.name in ["@match", "@matchrule"]) + ))) + )), + Dict( + :only_in_dirs => ["src/Compiler"] + ) +) + +# RemovePageRule +VIOLATIONS["remove_page"] = Rule( + "remove_page", + "`remove_page` should be used with extreme caution.", + @pattern remove_page({_}, {_}) +) + +# TaskRule +VIOLATIONS["Task"] = Rule( + "Task", + "`Task` should be used with extreme caution.", + @pattern Task({_}) +) + +# ErrorExceptionRule +VIOLATIONS["ErrorException"] = Rule( + "ErrorException", + "Use custom exception instead of the generic `ErrorException`.", + (@pattern ErrorException({_}...)), + Dict( + :exclude_files => ["test.jl", "tests.jl", "bench/", "Vectorized/Test"] + ) +) + +# ErrorRule +VIOLATIONS["error"] = Rule( + "error", + "Use custom exception instead of the generic `error()`.", + (@pattern error({_}...)), + Dict( + :exclude_files => ["test.jl", "tests.jl", "bench/", "Vectorized/Test"] + ) +) + +# UnsafeRule +register_syntax_class!(:unsafe_funcall, SyntaxClass( + "`unsafe_*` function call", + [ + @pattern begin + {_f:::funcall} + @when [:_f] startswith(_f.fun_name.name, r"_?unsafe_") + end + ] +)) +VIOLATIONS["unsafe_ function"] = Rule( + "unsafe_ function", + "An `unsafe_` function should be called only from an `unsafe_` function.", + @pattern ~and( + {_:::unsafe_funcall}, + ~not(~inside(~or( + {_:::unsafe_funcall} = {_}, + function ({_:::unsafe_funcall}) {_}... end + ))) + ) +) + +# InRule +VIOLATIONS["in"] = Rule( + "in", + "Use `tin(item,collection)` instead of the Julia's `in` or `∈`.", + @pattern ~or( + in({_}, {_}), + ∈({_}, {_}) + ) +) + +# HasKeyRule +VIOLATIONS["haskey"] = Rule( + "haskey", + "Use `thaskey(dict,key)` instead of the Julia's `haskey`.", + @pattern haskey({_}, {_}) +) + +# EqualRule +VIOLATIONS["equal"] = Rule( + "equal", + "Use `tequal(dict,key)` instead of the Julia's `equal`.", + @pattern equal({_}, {_}) +) + +# UvRule +VIOLATIONS["uv"] = Rule( + "uv", + "`uv_` functions should be used with extreme caution.", + @pattern ~and( + {f:::funcall}, + ~when([:f], startswith(f.fun_name.name, "uv_")) + ) +) + +# UnreachableBranchRule +# +# TODO: Find a way to express this in its most general form. Something like: +# +# @pattern ~or( +# if {cond} +# {_}... +# (elseif {_} {_}...)... +# elseif {cond} +# {_}... +# end, +# if {cond} +# {_}... +# (elseif {_} {_}...)... +# elseif {cond} +# {_}... +# else +# {_}... +# end +VIOLATIONS["unreachable branch"] = Rule( + "unreachable branch", + "Unreachable branch.", + @pattern ~or( + if {cond} + {_}... + elseif {cond} + {_}... + end, + if {cond} + {_}... + elseif {cond} + {_}... + else + {_}... + end, + if {cond} + {_}... + elseif {_} + {_}... + elseif {cond} + {_}... + end, + if {cond} + {_}... + elseif {_} + {_}... + elseif {cond} + {_}... + else + {_}... + end + ) +) + +# StringInterpolationRule +VIOLATIONS["string interpolation"] = Rule( + "string interpolation", + "Use \$(x) instead of \$x.", + @pattern ~and( + "$({_}...)$({x:::identifier})$({_}...)", + ~when([:x], !inside_parens(x.src)) + ) +) + +# RelPathAPIUsageRule +VIOLATIONS["RelPath"] = Rule( + "RelPath", + "Usage of type `RelPath` is not allowed in this context.", + (@pattern ~or( + {_}::RelPath, + RelPath({_}), + RelPath({_}, {_}) + )), + Dict( + :only_in_dirs => ["src/Compiler/Front"] + ) +) +VIOLATIONS["RelPath split_path"] = Rule( + "RelPath split_path", + "Usage of `RelPath` API method `split_path` is not allowed in this context.", + (@pattern split_path({_})), + Dict( + :only_in_dirs => ["src/Compiler/Front"] + ) +) +VIOLATIONS["RelPath drop_first"] = Rule( + "RelPath drop_first", + "Usage of `RelPath` API method `drop_first` is not allowed in this context.", + (@pattern drop_first({_})), + Dict( + :only_in_dirs => ["src/Compiler/Front"] + ) +) +VIOLATIONS["RelPath relpath_from_signature"] = Rule( + "RelPath relpath_from_signature", + """ + Usage of `RelPath` API method `relpath_from_signature` is not allowed \ + in this context.""", + (@pattern relpath_from_signature({_})), + Dict( + :only_in_dirs => ["src/Compiler/Front"] + ) +) + +# UseOfStaticThreads +VIOLATIONS["static threads"] = Rule( + "static threads", + """ + Use `Threads.@threads :dynamic` instead of `Threads.@threads :static`. Static threads \ + must not be used as generated tasks will not be able to migrate across threads.""", + @pattern ~or( + (@threads :static {_}...), + Threads.@threads :static {_}... + ) +) + +# NoImportRule +VIOLATIONS["no import"] = Rule( + "no import", + "Imports must be specified with `using` and not `import`.", + @pattern {_:::import} +) + +# NotImportingRAICodeRule +VIOLATIONS["no import RAICode"] = Rule( + "no import RAICode", + "Importing RAICode should be avoided (when possible).", + @pattern begin + {u:::using} + @when [:u] u.module.module_name == "RAICode" + end +) + +# BareUsingRule +VIOLATIONS["bare using"] = Rule( + "bare using", + "Use `using Foo: Foo` or `using Foo: specific_function` instead of bare `using Foo`.", + (@pattern begin + {u:::using} + @when [:u] isempty(u.ids.src) + end), + Dict( + :exclude_files => ["test/", "test.jl"] + ) +) + +# UntypedArrayComprehensionRule +VIOLATIONS["untyped array comprehension"] = Rule( + "untyped array comprehension", + """ + Need a specific Array type to be provided. \ + Use `T[x for x in xs]` instead of `[x for x in xs]`.""", + (@pattern [{_} for {_} in {_}]), + Dict( + :only_in_dirs => ["src/Compiler/"] + ) +) + +# ConstGlobalMissingTypeRule +register_syntax_class!(:const, SyntaxClass( + "const assignment", + [ + (@pattern const {_} = {_}), + (@pattern const global {_} = {_}) + ] +)) +VIOLATIONS["non-const untyped global"] = Rule( + "non-const untyped global", + """ + Global variable must have type annotation: `global x::Type = value`. \ + Use `const` for immutable globals.""", + (@pattern ~and( + :(global {_:::identifier} = {_}), + ~not(~inside({_:::const}, 1)) + )), + Dict( + :exclude_files => ["test/", "test.jl"] + ) +) + +# NotFullyParameterizedConstructorRule +register_syntax_class!(:loop, SyntaxClass( + "`for` or `while` loop", + [ + (@pattern (for {_} in {_} + {_}... + end)), + (@pattern (for {_} = {_} + {_}... + end)), + (@pattern (for {_} ∈ {_} + {_}... + end)), + (@pattern while {_} + {_}... + end) + ] +)) +VIOLATIONS["not fully parametrised constructor"] = Rule( + "not fully parametrised constructor", + """ + Avoid not-fully-parameterized constructor in loops. \ + Use a maker function instead for better performance.""", + (@pattern ~and( + {id:::identifier}({_}...), + ~inside({_:::loop}), + ~when([:id], startswith(id.name, r"[A-Z]")) + )), + Dict( + :only_in_dirs => ["src/Compiler"], + :exclude_files => ["test/", "test.jl"] + ) +) From 4f04b97d70e4f90ec946badd52d6bdb5e0e3e76e Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 9 Apr 2026 18:02:50 +0200 Subject: [PATCH 43/62] Document; clean up; reorganise --- src/interface.jl | 773 +++++++++++++++++++++---------------- src/rules/rules.jl | 41 ++ src/rules/utils.jl | 3 + test/common.jl | 7 +- test/interface.jl | 61 +-- test/lint_context_tests.jl | 16 +- 6 files changed, 530 insertions(+), 371 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 4924024..4f487b5 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -9,7 +9,11 @@ global MAX_REPORTED_ERRORS = 60 # 1_000_000 # Individual rule report # ---------------------- -# Each individual rule violation report +""" + LintRuleReport + +Report of the result of linting one file with one rule. +""" mutable struct LintRuleReport rule::Rule msg::String @@ -22,13 +26,18 @@ mutable struct LintRuleReport end LintRuleReport(rule::Rule, msg::String) = LintRuleReport(rule, msg, "", "", 0, 0, false, 0) -Argus_result_to_LintRuleReport(rule::Rule, bindings::BindingSet) = +""" + Argus_result_to_LintRuleReport(rule::Rule, match_result::BindingSet) + +Convert a rule match result to a `LintRuleReport`. +""" +Argus_result_to_LintRuleReport(rule::Rule, match_result::BindingSet) = LintRuleReport(rule, rule.description, "", - JuliaSyntax.filename(bindings), - JuliaSyntax.source_location(bindings)[1], - JuliaSyntax.source_location(bindings)[2], + JuliaSyntax.filename(match_result), + JuliaSyntax.source_location(match_result)[1], + JuliaSyntax.source_location(match_result)[2], false, 0) @@ -43,12 +52,16 @@ is_fatal(r::LintRuleReport) = is_fatal(r.rule) # Global report # ------------- -# Global result of executing Lint on files and folders +""" + LintGlobalReport + +Report of the result of linting multiple files and directories with a set of rules. +""" mutable struct LintGlobalReport files_count::Integer violations_count::Integer recommendations_count::Integer - fatalviolations_count::Integer + fatal_violations_count::Integer linted_files::Vector{String} printout_count::Integer lintrule_reports::Vector{LintRuleReport} @@ -70,7 +83,7 @@ function Base.append!(l1::LintGlobalReport, l2::LintGlobalReport) l1.files_count += l2.files_count l1.violations_count += l2.violations_count l1.recommendations_count += l2.recommendations_count - l1.fatalviolations_count += l2.fatalviolations_count + l1.fatal_violations_count += l2.fatal_violations_count Base.append!(l1.linted_files, l2.linted_files) Base.append!(l1.lintrule_reports, l2.lintrule_reports) @@ -81,66 +94,68 @@ function Base.:(==)(l1::LintGlobalReport, l2::LintGlobalReport) return l1.files_count == l2.files_count && l1.violations_count == l2.violations_count && l1.recommendations_count == l2.recommendations_count && - l1.fatalviolations_count == l2.fatalviolations_count && + l1.fatal_violations_count == l2.fatal_violations_count && l1.linted_files == l2.linted_files && l1.printout_count == l2.printout_count && l1.lintrule_reports == l2.lintrule_reports end -# Utils - -is_already_linted(l::LintGlobalReport, filename) = filename in l.linted_files - -has_values(l::LintGlobalReport, a, b, c) = - l.files_count == a && - l.violations_count == b && - l.recommendations_count == c - # Precommit config # ================ -# File exclusion +""" + LintFileExclusion + +Wrapper for a regex matching the names of the files that should be excluded when linting +during pre-commit. +""" struct LintFileExclusion regex::String end + should_be_excluded(lfe::LintFileExclusion, filename::String) = !isnothing(match(Regex(lfe.regex), filename)) should_be_excluded(lfes::Vector{LintFileExclusion}, filename::String) = any(lfe -> should_be_excluded(lfe, filename), lfes) -# For a given pre-commit file configuration, the function extract the regex rules -# that are used to exclude files from the linting process. +""" + extract_file_exclusions_from_precommit_file(pre_commit_file::String) + +For a given pre-commit file configuration, extract the regex rules used to exclude files +from the linting process. +""" function extract_file_exclusions_from_precommit_file(pre_commit_file::String) # 3 states: - # - outside the ReLint repo entry (initial state) - # - inside the exclusion entry - # - inside the ReLint repo entry and we are in the exclude section + # - outside the ReLint repo entry (initial state); + # - inside the exclusion entry; + # - inside the ReLint repo entry and we are in the exclude section. state = :outside_repo_entry file_exclusions = [] open(pre_commit_file, "r") do io for line in eachline(io) if state == :outside_repo_entry - # We are outside the ReLint repo entry + # We are outside the ReLint repo entry. if contains(line, "lint-fatal-checks") - # We are inside the ReLint repo entry + # We are inside the ReLint repo entry. state = :inside_repo_entry continue end elseif state == :inside_repo_entry if contains(line, "(?x)^(") - # We are inside the ReLint repo entry + # We are inside the ReLint repo entry. state = :inside_exclusion_entry continue end elseif state == :inside_exclusion_entry - # We are leaving the exclusion portion + # We are leaving the exclusion portion. if contains(line, ")") state = :outside_repo_entry continue end regex_rule = strip(line, ['{', '}', '\n', ' ', '|']) - !isempty(regex_rule) && push!(file_exclusions, LintFileExclusion(regex_rule)) + !isempty(regex_rule) && + push!(file_exclusions, LintFileExclusion(regex_rule)) end end end @@ -150,7 +165,18 @@ end # Formatting # ========== +""" + AbstractFormatter + +Supertype for all lint report formatters. +""" abstract type AbstractFormatter end + +""" + PlainFormat <: AbstractFormatter + +Default lint report formatter. +""" struct PlainFormat <: AbstractFormatter end # MarkdownFormat can optionally contains github information. This is useful when a @@ -159,6 +185,12 @@ struct PlainFormat <: AbstractFormatter end # report. This is useful because GitHub Action clones a repository in a folder of the same # name. In our case, GHA will create /home/runner/work/raicode/raicode so we need to remove # one "raicode" from the fullname. +""" + MarkdownFormat <: AbstractFormatter + +Markdown lint report formatter which contains GitHub information such as branch and +repository. It is useful when the generated report contains Markdown links. +""" struct MarkdownFormat <: AbstractFormatter github_branch_name::String github_repository_name::String @@ -175,59 +207,59 @@ struct MarkdownFormat <: AbstractFormatter MarkdownFormat(branch::String, repo::String) = new(branch, repo, "", devnull) end -# Only show the fatal violations and a summary +""" + PreCommitFormat <: AbstractFormatter + +Pre-commit lint report formatter. Only shows fatal violations and a summary. +""" struct PreCommitFormat <: AbstractFormatter end # Linting # ======= +""" + LintContext + +The lint context containing the set of rules to run and the set of `LintFileExclusion`s, +if any. +""" struct LintContext - rules_to_run::Vector{Rule} - regex_exclusions #::Vector{LintFileExclusion} - - function LintContext(dts_as_str::Vector{String}) - dt = DataType[] - for dt_as_str in dts_as_str - ind = findfirst(t -> nameof(t) == Symbol(dt_as_str), all_rules()) - isnothing(ind) && error("Non-existing rule: $(dt_as_str)") - push!(dt, all_rules()[ind]) - end - return new(dt, []) - end + rules::Vector{Rule} + file_exclusions::Vector{LintFileExclusion} - LintContext(s::Vector{Rule}) = new(s, []) + LintContext(rules::Vector{Rule}) = new(rules, []) LintContext() = new(all_rules(), []) LintContext(a, b) = new(a, b) end """ - lint_file(rootpath, context) - + lint_file(rootpath[, context::LintContext]) +Lint the file found at `rootpath`. If a lint context is not specified, use all rules +defined in ReLint. """ -lint_file(rootpath, context::LintContext = LintContext()) = +lint_file(rootpath, context::LintContext=LintContext()) = lint_text(read(rootpath, String); context, filename = rootpath) """ - lint_text(file_content_string; filename="", context) + lint_text(text::String; filename="", context::LintContext=LintContext()) -Runs lint checks on `text`, lints will be reported as comming from -`file`. +Runs lint checks on `text`. Lints will be reported as comming from `filename`. """ -function lint_text(file_content_string::AbstractString; filename = "", context = LintContext()) - ast = JuliaSyntax.parseall(SyntaxNode, file_content_string; filename=filename) +function lint_text(text::String; filename="", context::LintContext=LintContext()) + ast = JuliaSyntax.parseall(SyntaxNode, text; filename=filename) ast = Argus._normalise!(ast) - all_lines = split(file_content_string, "\n") + all_lines = split(text, "\n") lint_rule_reports = [] # AST rules - match_results = rules_match(context.rules_to_run, ast; disabler=relint_disabler) + match_results = rules_match(context.rules, ast; disabler=relint_disabler) # Remove refactorings. for (rule_name, match_result) in match_results successful_match_results = map(m -> m[1], match_result.matches) rule = - context.rules_to_run[findfirst(r -> r.name == rule_name, context.rules_to_run)] + context.rules[findfirst(r -> r.name == rule_name, context.rules)] for m in successful_match_results push!(lint_rule_reports, Argus_result_to_LintRuleReport(rule, m)) end @@ -257,102 +289,130 @@ function lint_text(file_content_string::AbstractString; filename = "", c return lint_rule_reports end -get_all_lines_from_filename(filename::String) = open(io -> readlines(io), filename) -function _run_lint_on_dir( - rootpath::String; - result::LintGlobalReport = LintGlobalReport(), - io::Union{IO, Nothing} = stdout, - io_violations::Union{IO, Nothing} = nothing, - io_recommendations::Union{IO, Nothing} = nothing, - formatter::AbstractFormatter = PlainFormat(), - context::LintContext = LintContext() - ) +""" + run_lint_on_dir(rootpath::String; + result::LintGlobalReport=LintGlobalReport(), + io::Union{IO, Nothing}=stdout, + io_violations::Union{IO, Nothing}=nothing, + io_recommendations::Union{IO, Nothing}=nothing, + formatter::AbstractFormatter=PlainFormat(), + context::LintContext=LintContext()) + +Lint the directory found at `rootpath`. +""" +function run_lint_on_dir(rootpath::String; + result::LintGlobalReport = LintGlobalReport(), + io::Union{IO, Nothing} = stdout, + io_violations::Union{IO, Nothing} = nothing, + io_recommendations::Union{IO, Nothing} = nothing, + formatter::AbstractFormatter = PlainFormat(), + context::LintContext = LintContext()) # Exit if we are in .git !isnothing(match(r".*/\.git.*", rootpath)) && return result for (root, dirs, files) in walkdir(rootpath) for file in files filename = joinpath(root, file) - if endswith(filename, ".jl") - run_lint(filename; result, io, io_violations, io_recommendations, formatter, context) - end + endswith(filename, ".jl") && + run_lint(filename; + result, + io, + io_violations, + io_recommendations, + formatter, + context) end for dir in dirs p = joinpath(root, dir) !isnothing(match(r".*/\.git.*", p)) && continue - _run_lint_on_dir(p; result, io, io_violations, io_recommendations, formatter, context) + run_lint_on_dir(p; + result, + io, + io_violations, + io_recommendations, + formatter, + context) end end return result end """ - run_lint(rootpath::String; io::IO=stdout, io_violations::Union{IO,Nothing}, io_recommendations::Union{IO,Nothing}) + run_lint(rootpath::String; + io::IO=stdout, + io_violations::Union{IO,Nothing}, + io_recommendations::Union{IO,Nothing}) -Run lint rules on a file `rootpath`, which must be an existing non-folder file. Return a -LintGlobalReport. +Lint the file found at `rootpath`. Return a LintGlobalReport. -Example of use: - import ReLint - ReLint.run_lint("foo/bar/myfile.jl") +# Examples + +``` +import ReLint +ReLint.run_lint("foo/bar/myfile.jl") +``` """ -function run_lint( - rootpath::String; - result::LintGlobalReport = LintGlobalReport(), - io::Union{IO, Nothing} = stdout, - io_violations::Union{IO, Nothing} = nothing, - io_recommendations::Union{IO, Nothing} = nothing, - formatter::AbstractFormatter = PlainFormat(), - context::LintContext = LintContext() - ) - # If already linted, then we merely exit +function run_lint(rootpath::String; + result::LintGlobalReport=LintGlobalReport(), + io::Union{IO, Nothing}=stdout, + io_violations::Union{IO, Nothing}=nothing, + io_recommendations::Union{IO, Nothing}=nothing, + formatter::AbstractFormatter=PlainFormat(), + context::LintContext=LintContext()) + # Return if `rootpath` is already linted. rootpath in result.linted_files && return result - - # If we are running Lint on a directory - isdir(rootpath) && return _run_lint_on_dir(rootpath; result, io, io_violations, io_recommendations, formatter, context) - - # Check if we have to be run on a Julia file. Simply exit if not. - # This simplify the amount of work in GitHub Action + # Lint as directory if `rootpath` is a directory. + isdir(rootpath) && return run_lint_on_dir(rootpath; + result, + io, + io_violations, + io_recommendations, + formatter, + context) + # Don't lint non-Julia files. endswith(rootpath, ".jl") || return result + # Exclude files as specified by the pre-commit configuration. + should_be_excluded(context.file_exclusions, rootpath) && return result - # We should ignore this file? - should_be_excluded(convert(Vector{LintFileExclusion}, context.regex_exclusions), rootpath) && return result - - # We are running Lint on a Julia file - lint_reports = ReLint.lint_file(rootpath, context) + # Here, `rootpath` is surely an unlinted Julia file. + # + # Lint the file and print the report. + lint_reports = lint_file(rootpath, context) isempty(lint_reports) || print_header(formatter, io, rootpath) - + # Extract reports according to rule type. violation_reports = filter(is_violation, lint_reports) recommandation_reports = filter(is_recommendation, lint_reports) - fatalviolation_reports = filter(is_fatal, lint_reports) - + fatal_violation_reports = filter(is_fatal, lint_reports) count_violations = length(violation_reports) count_recommendations = length(recommandation_reports) - count_fatalviolations = length(fatalviolation_reports) - - # Fatal reports are printed in io_violations, but first - io_tmp = isnothing(io_violations) ? io : io_violations - for r in fatalviolation_reports - print_report(formatter, io_tmp, r, result) + count_fatalviolations = length(fatal_violation_reports) + # Print reports to their specific `IO`s. + io_fatal_violations = isnothing(io_violations) ? io : io_violations + for r in fatal_violation_reports + print_report(formatter, io_fatal_violations, r, result) end - - io_tmp = isnothing(io_violations) ? io : io_violations + io_violations = isnothing(io_violations) ? io : io_violations for r in violation_reports - print_report(formatter, io_tmp, r, result) + print_report(formatter, io_violations, r, result) end - - io_tmp = isnothing(io_recommendations) ? io : io_recommendations + io_recommendations = isnothing(io_recommendations) ? io : io_recommendations for r in recommandation_reports - print_report(formatter, io_tmp, r, result) + print_report(formatter, io_recommendations, r, result) end - - # We run Lint on a single file. - append!(result, LintGlobalReport(1, count_violations, count_recommendations, count_fatalviolations, [rootpath], 0, lint_reports)) + # Collect all reports and return the result. + append!(result, LintGlobalReport(1, + count_violations, + count_recommendations, + count_fatalviolations, + [rootpath], + 0, + lint_reports)) return result end +# TODO: Remove/replace this. """ file_name corresponds to a file name that is used to create the temporary file. This is useful to test some rules that depends on the filename. @@ -364,7 +424,7 @@ function run_lint_on_text( result::LintGlobalReport = LintGlobalReport(), io::Union{IO, Nothing} = stdout, formatter::AbstractFormatter = PlainFormat(), - directory::String = "", # temporary directory to be created. If empty, let Julia decide + directory::String = "", context::LintContext = LintContext() ) io_violations = IOBuffer() @@ -404,274 +464,271 @@ function run_lint_on_text( end """ - generate_report(filenames::Vector{String}, output_filename::String;...) - -Main entry point of ReLint.jl. The function `generate_report` takes as argument a list -of files on which lint has to process. A report is generated containing the result of the -Lint analysis. - -The procuded markdown report is intenteded to be posted as a comment on a GitHub PR. -Furthermore, a JSON report file is produced to feed DataDog. - -Here are the arguments: - - - `filenames` is the list of all the file that have to be analyzed. From this lint -only Julia files are considered. Filenames provided to that list that do not end with `.jl` -will be simply ignored. Note that this variable is not considered if -`analyze_all_file_found_locally` is set to true. - - `output_filename` is the file to be created that will contains the Markdown report. -If the file already exist, then the no analysis is run. - - `json_output` is an output stream to which the JSON report has to be printed. Note -that the value provided to this variable may be overriden by `json_filename`. In the future, -the variable `json_output` can be removed. - - `json_filename` file is a filename used to create the JSON report for DataDog - - `github_repository` is the name of the repository, e.g., `raicode` - - `branch_name` is a GitHub branch name, useful for the reporting - - `file_prefix_to_remove` prefix to remove for all the file to be analyzed. This is -because GHAction creates a folder of the same name before cloning it. However, this -option can be removed in the future with a simple `cd` in that folder. - - `analyze_all_file_found_locally`, when set to `true` the `filenames` argument is not -used and instead all the file found locally, from `.` will be analyzed. This is used by -the github action workflow to run Lint on master. - -When provided, `github_repository` and `branch_name` are used to have clickable links in -the Markdown report. -""" -function generate_report( - filenames::Vector{String}, - output_filename::String - ; - json_output::IO = stdout, - json_filename::Union{Nothing, String} = nothing, # Override `json_output` when not nothing - github_repository::String = "", - branch_name::String = "", - file_prefix_to_remove::String = "", - analyze_all_file_found_locally::Bool = false, - stream_workflowcommand::IO = stdout, - rules_to_run::Vector{Rule} = all_rules(), - pre_commit_file::String = "", - ) + generate_report(filenames::Vector{String}, + output_filename::String; + json_output::IO=stdout, + json_filename::Union{Nothing, String}=nothing, + github_repository::String="", + branch_name::String="", + file_prefix_to_remove::String="", + local_files_only::Bool=false, + stream_workflowcommand::IO=stdout, + rules::Vector{Rule}=all_rules(), + pre_commit_file::String="") + +Generate a Markdown report based on the linting all given files. The report is intenteded +to be posted as a comment on a GitHub PR. + +The report could also be generated in JSON format (useful for DataDog). + +# Arguments + + - `filenames`: the list of files to be analysed. Only Julia files are linted; + if `local_files_only` is `true`, `filenames` is ignored; + - `output_filename`: the file where the Markdown report will be printed; + if the file already exist, no analysis is run; + - `json_output`: the output stream where the JSON report should be printed; may be + overridden by `json_filename`; + - `json_filename`: the file where the JSON report will be printed; + - `github_repository`: the name of the repository where the analysis is run; + - `branch_name`: the name of the branch where the analysis is run; + - `file_prefix_to_remove`: prefix to remove for all analysed files; this is + because GHAction creates a folder of the same name before + cloning it; this argument can be removed in the future; + - `local_files_only`: when set to `true` replace `filenames` with the list of files found + at `.` (`pwd`); used by the github action workflow to run ReLint + on master; + - `rules`: the set of lint rules to be run; + - `pre_commit_file`: the path to the pre-commit configuration file. +""" +function generate_report(filenames::Vector{String}, + output_filename::String; + json_output::IO=stdout, + json_filename::Union{Nothing, String}=nothing, + github_repository::String="", + branch_name::String="", + file_prefix_to_remove::String="", + local_files_only::Bool=false, + stream_workflowcommand::IO=stdout, + rules::Vector{Rule}=all_rules(), + pre_commit_file::String="") if isfile(output_filename) @error "File $(output_filename) exist already." - return + return nothing end if !isnothing(json_filename) if isfile(json_filename) - @error "File $(json_filename) exist already, cannot create json file." - return + @error "File $(json_filename) exist already, cannot create JSON file." + return nothing end json_output = open(json_filename, "w") end local errors_count = 0 - local julia_filenames = filter(n -> endswith(n, ".jl"), filenames) - - # Result of the whole analysis - lint_result = LintGlobalReport() - lint_result.branch = branch_name - - # If analyze_all_file_found_locally is set to true, we discard all the provided files - # and analyze everything accessible from "." - if analyze_all_file_found_locally - julia_filenames = [pwd()] - end - + local julia_filenames = local_files_only ? + [pwd()] : + filter(n -> endswith(n, ".jl"), filenames) + + # Initialise the report. + lint_report = LintGlobalReport() + lint_report.branch = branch_name + # Run the rules and print the report in `output_filename`. open(output_filename, "w") do output_io - println(output_io, "## Static code analyzer report") - println(output_io, "**Output of the [ReLint.jl code analyzer]\ + # Print report header. + println(output_io, "## Static analysis report") + println(output_io, "**Output of [ReLint.jl]\ (https://github.com/RelationalAI-oss/ReLint.jl). \ - 🫵[Want to contribute?](https://github.com/RelationalAI-oss/ReLint.jl/blob/main/README.md#contributing-to-staticlintjl)🫵 \ - [RelationalAI Style Guide for Julia](https://github.com/RelationalAI/RAIStyle)**\n\ + 🫵[Want to contribute?]\ + (https://github.com/RelationalAI-oss/ReLint.jl?tab=readme-ov-file#contributing-to-relintjl)🫵\ + **\n\ Report creation time (UTC): ($(now(UTC)))") - - + # Initialise Markdown formatter. formatter = MarkdownFormat( branch_name, github_repository, file_prefix_to_remove, stream_workflowcommand, ) - + # Set up report buffers for each rule category. io_violations = IOBuffer() io_recommendations = IOBuffer() - - context = nothing - if !isempty(pre_commit_file) - context = LintContext( - rules_to_run, - extract_file_exclusions_from_precommit_file(pre_commit_file) - ) - else - context = LintContext(rules_to_run) - end - - # RUN LINT!!! + # Set up the lint context. + context = isempty(pre_commit_file) ? + LintContext(rules) : + LintContext(rules, + extract_file_exclusions_from_precommit_file(pre_commit_file)) + # Lint all files. for filename in julia_filenames - ReLint.run_lint( - filename; - result = lint_result, - io = output_io, - io_violations = io_violations, - io_recommendations = io_recommendations, - formatter, - context - ) + run_lint(filename; + result=lint_report, + io=output_io, + io_violations, + io_recommendations, + formatter, + context) end + # Print violation reports. print(output_io, String(take!(io_violations))) - + # Print recommendation reports. recommendations = String(take!(io_recommendations)) if !isempty(recommendations) println(output_io, "\n") println( output_io, """
- For PR Reviewer ($(lint_result.recommendations_count)) + For PR Reviewer ($(lint_report.recommendations_count)) $(recommendations)
""" ) end - - has_julia_file = !isempty(lint_result.linted_files) - - if lint_result.violations_count + lint_result.recommendations_count > lint_result.printout_count - println(output_io, "⚠️Only a subset of the violations and recommandations are here reported⚠️") + # Print a warning if not all reports are shown. + violation_and_recommendation_reports_count = + lint_report.violations_count + lint_report.recommendations_count + if violation_and_recommendation_reports_count > lint_report.printout_count + println(output_io, + "⚠️Only a subset of the reports are shown⚠️") end - - ending = length(julia_filenames) > 1 ? "s" : "" - if !has_julia_file - println(output_io, "No Julia file is modified or added in this PR.") + # Print summary. + if isempty(lint_report.linted_files) + println(output_io, "No Julia files were modified or added in this PR.") else - errors_count = lint_result.violations_count + - lint_result.recommendations_count + - lint_result.fatalviolations_count + errors_count = + violation_and_recommendation_reports_count + + lint_report.fatal_violations_count + ending = length(julia_filenames) == 1 ? "" : "s" if iszero(errors_count) - print(output_io, "🎉No potential threats are found over $(length(julia_filenames)) Julia file$(ending).👍\n\n") + print(output_io, string("🎉No potential threats were found over ", + length(julia_filenames), + " Julia file", + ending, + ".👍\n")) else - s_vio = lint_result.violations_count > 1 ? "s" : "" - s_rec = lint_result.recommendations_count > 1 ? "s" : "" - s_fvio = lint_result.fatalviolations_count > 1 ? "s" : "" - is_or_are = errors_count == 1 ? "is" : "are" - s_fil = lint_result.files_count > 1 ? "s" : "" - println(output_io, "🚨**In total, $(lint_result.fatalviolations_count) fatal rule violation$(s_fvio), $(lint_result.violations_count) rule violation$(s_vio) and $(lint_result.recommendations_count) PR reviewer recommendation$(s_rec) $(is_or_are) found over $(lint_result.files_count) Julia file$(s_fil)**🚨") + ending_recommendations = lint_report.recommendations_count == 1 ? "" : "s" + ending_violations = lint_report.violations_count == 1 ? "" : "s" + ending_fatal_violations = + lint_report.fatal_violations_count == 1 ? "" : "s" + was_or_were = errors_count == 1 ? "was" : "were" + ending_files = lint_report.files_count == 1 ? "" : "s" + println(output_io, string("🚨**In total, ", + lint_report.fatal_violations_count, + " fatal violation", + ending_fatal_violations, + ", ", + lint_report.violations_count, + " violation", + ending_violations, + " and ", + lint_report.recommendations_count, + " PR reviewer recommendation", + ending_recommendations, + " ", + was_or_were, + " found over ", + lint_report.files_count, + " Julia file", + ending_files, + "**🚨")) end end - - - println(output_io, "$(length(rules_to_run)) rules were used to build this report") + println(output_io, string(length(rules), " rules were used to build this report.")) end + # Print DataDog report. report_as_string = open(output_filename) do io read(io, String) end print_datadog_report( json_output, report_as_string, - lint_result.files_count, - lint_result.violations_count, - lint_result.recommendations_count, - lint_result.fatalviolations_count, - lint_result.branch, - length(rules_to_run), + lint_report.files_count, + lint_report.violations_count, + lint_report.recommendations_count, + lint_report.fatal_violations_count, + lint_report.branch, + length(rules), ) - # If a json_filename was provided, we are writing the result in json_output. - # In that case, we need to close the stream at the end. - return isnothing(json_filename) || close(json_output) -end - -struct ReLintDisabler <: CommentDisabler end -const relint_disabler = ReLintDisabler() + # If `json_filename` is provided, we are writing the result in `json_output` and need + # to close the stream. + isnothing(json_filename) || close(json_output) -relint_disabler(line::AbstractString) = is_disable_all_comment(line) -function relint_disabler(rule::Rule, line::AbstractString) - is_disable_comment(line) || return false - is_disable_all_comment(line) && return true - is_disable_rule_comment(line, rule) && return true - return false -end -is_disable_comment(str::AbstractString) = - startswith(str, "# lint-disable-next-line") -is_disable_all_comment(str::AbstractString) = str == "# lint-disable-next-line" -function is_disable_rule_comment(str::AbstractString, rule::Rule) - is_disable_comment(str) || return false - split_command_from_rules = split(str, ":") - length(split_command_from_rules) == 2 || return false - rules_list = strip.(split(split_command_from_rules[2], ",")) - rule.name in rules_list && return true - # Allow disabling by rule description as well. - return startswith(rule.description, lstrip(split_command_from_rules[2])) + return nothing end # Printing # -------- -print_header(::PreCommitFormat, io::IO, rootpath::String) = nothing +# Plain format -print_footer(::PreCommitFormat, io::IO) = nothing -function print_summary(::PreCommitFormat, io::IO, result::LintGlobalReport) - print_summary(PlainFormat(), io, result) - return printstyled(io, "Note that the list above only show fatal violations\n", color = :red) -end +print_header(::PlainFormat, io::IO, rootpath::String) = + printstyled(io, "-"^10 * " $(rootpath)\n", color = :blue) -function print_report(::PreCommitFormat, io::IO, lint_report::LintRuleReport, result::LintGlobalReport) - should_print_report(result) || return - # Do not print anything if it is not a fatal violation - is_fatal(lint_report) || return - printstyled(io, "Line $(lint_report.line), column $(lint_report.column):", color = :green) - print(io, " ") - print(io, lint_report.msg) - print(io, " ") - println(io, lint_report.file) - return result.printout_count += 1 -end - -should_print_report(result) = result.printout_count <= MAX_REPORTED_ERRORS - -function print_header(::PlainFormat, io::IO, rootpath::String) - return printstyled(io, "-"^10 * " $(rootpath)\n", color = :blue) -end +print_footer(::PlainFormat, io::IO) = + printstyled(io, "-"^10 * "\n\n", color = :blue) -function print_report(::PlainFormat, io::IO, lint_report::LintRuleReport, result::LintGlobalReport) +function print_report(::PlainFormat, + io::IO, + lint_report::LintRuleReport, + result::LintGlobalReport) should_print_report(result) || return - printstyled(io, "Line $(lint_report.line), column $(lint_report.column):", color = :green) + printstyled(io, + "Line $(lint_report.line), column $(lint_report.column):", + color = :green) print(io, " ") print(io, lint_report.msg) print(io, " ") println(io, lint_report.file) - return result.printout_count += 1 + result.printout_count += 1 + return nothing end -function print_summary( - ::PlainFormat, - io::IO, - result::LintGlobalReport - ) - nb_rulereports = result.violations_count + result.recommendations_count + result.fatalviolations_count - return if iszero(nb_rulereports) +function print_summary(::PlainFormat, + io::IO, + result::LintGlobalReport) + nb_rulereports = + result.recommendations_count + + result.violations_count + + result.fatal_violations_count + if iszero(nb_rulereports) printstyled(io, "No potential threats were found.\n", color = :green) else - plural = nb_rulereports > 1 ? "s are" : " is" - plural_vio = result.violations_count > 1 ? "s" : "" - plural_fatal = result.fatalviolations_count > 1 ? "s" : "" - plural_rec = result.recommendations_count > 1 ? "s" : "" - printstyled(io, "$(nb_rulereports) potential threat$(plural) found: ", color = :red) - printstyled(io, "$(result.fatalviolations_count) fatal violation$(plural_fatal), $(result.violations_count) violation$(plural_vio) and $(result.recommendations_count) recommendation$(plural_rec)\n", color = :red) + was_or_were = nb_rulereports == 1 ? " was" : "s were" + ending_violations = result.violations_count == 1 ? "" : "s" + ending_fata_violations = result.fatal_violations_count == 1 ? "" : "s" + ending_recommendations = result.recommendations_count == 1 ? "" : "s" + printstyled(io, + string(nb_rulereports, " potential threat$(was_or_were) found: "), + color = :red) + printstyled(io, + string(result.fatal_violations_count, + " fatal violation", + ending_fata_violations, + ", ", + result.violations_count, + " violation", + ending_violations, + " and ", + result.recommendations_count, + " recommendation", + ending_recommendations, + "\n"), + color = :red) end -end -function print_footer(::PlainFormat, io::IO) - return printstyled(io, "-"^10 * "\n\n", color = :blue) + return nothing end +# Markdown format + print_header(::MarkdownFormat, io::IO, rootpath::String) = nothing + print_footer(::MarkdownFormat, io::IO) = nothing -# Remove a leading '/' if the file starts with one. This is necessary to build the URL -# Remove the prefix mentioned in the Markdown from the file_name +# Remove the leading '/', if any. Remove the prefix mentioned in `generate_report`. function remove_prefix_from_filename(file_name::String, file_prefix_to_remove::String) corrected_file_name = first(file_name) == '/' ? file_name[2:end] : file_name if startswith(corrected_file_name, file_prefix_to_remove) @@ -679,46 +736,91 @@ function remove_prefix_from_filename(file_name::String, file_prefix_to_remove::S end return corrected_file_name end +remove_prefix_from_filename(file_name::String, format::MarkdownFormat) = + remove_prefix_from_filename(file_name, format.file_prefix_to_remove) -function remove_prefix_from_filename(file_name::String, format::MarkdownFormat) - return remove_prefix_from_filename(file_name, format.file_prefix_to_remove) -end - -function print_report(format::MarkdownFormat, io::IO, lint_report::LintRuleReport, result::LintGlobalReport) - should_print_report(result) || return +function print_report(format::MarkdownFormat, + io::IO, + lint_report::LintRuleReport, + result::LintGlobalReport) + should_print_report(result) || return nothing corrected_file_name = remove_prefix_from_filename(lint_report.file, format) - coordinates = "Line $(lint_report.line), column $(lint_report.column):" if !isempty(format.github_branch_name) && !isempty(format.github_repository_name) - extended_coordinates = "[$(coordinates)](https://github.com/$(format.github_repository_name)/blob/$(format.github_branch_name)/$(corrected_file_name)#L$(lint_report.line))" - print(io, " - **$(extended_coordinates)** $(lint_report.msg) $(lint_report.file)\n") - else - print(io, " - **$(coordinates)** $(lint_report.msg) $(lint_report.file)\n") + coordinates = string("[", + coordinates, + "](https://github.com/", + format.github_repository_name, + "/blob/", + format.github_branch_name, + "/", + corrected_file_name, + "#L", + lint_report.line, + ")") end - + print(io, " - **$(coordinates)** $(lint_report.msg) $(lint_report.file)\n") # Produce workflow command to see results in the PR file changed tab: # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#example-setting-an-error-message - println(format.stream_workflowcommand, "::error file=$(corrected_file_name),line=$(lint_report.line),col=$(lint_report.column)::$(lint_report.msg)") - return result.printout_count += 1 + println(format.stream_workflowcommand, + string("::error file=", + corrected_file_name, + ",line=", + lint_report.line, + ",col=", + lint_report.column, + "::", + lint_report.msg)) + result.printout_count += 1 + + return nothing end -print_summary( - ::MarkdownFormat, - io::IO, - result::LintGlobalReport -) = nothing - -function print_datadog_report( - json_output::IO, - report_as_string::String, - files_count::Integer, - violation_count::Integer, - recommandation_count::Integer, - fatalviolations_count::Integer, - branch::String, - rules_count::Integer, - ) +print_summary(::MarkdownFormat, io::IO, result::LintGlobalReport) = nothing + +# Pre-commit format + +print_header(::PreCommitFormat, io::IO, rootpath::String) = nothing + +print_footer(::PreCommitFormat, io::IO) = nothing + +function print_summary(::PreCommitFormat, io::IO, result::LintGlobalReport) + print_summary(PlainFormat(), io, result) + return printstyled(io, + "Note that the list above only shows fatal violations\n", + color = :red) +end + +function print_report(::PreCommitFormat, + io::IO, + lint_report::LintRuleReport, + result::LintGlobalReport) + should_print_report(result) || return nothing + # Only print fatal violations. + is_fatal(lint_report) || return nothing + printstyled(io, + "Line $(lint_report.line), column $(lint_report.column):", + color = :green) + print(io, " ") + print(io, lint_report.msg) + print(io, " ") + println(io, lint_report.file) + result.printout_count += 1 + + return nothing +end + +# DataDog + +function print_datadog_report(json_output::IO, + report_as_string::String, + files_count::Integer, + violation_count::Integer, + recommandation_count::Integer, + fatalviolations_count::Integer, + branch::String, + rules_count::Integer) event = Dict( :source => "ReLint", :specversion => "1.1", @@ -734,5 +836,10 @@ function print_datadog_report( :rules_count => rules_count, ) ) + return println(json_output, JSON3.write(event)) end + +# Utils + +should_print_report(result) = result.printout_count <= MAX_REPORTED_ERRORS diff --git a/src/rules/rules.jl b/src/rules/rules.jl index 355422c..2a812e3 100644 --- a/src/rules/rules.jl +++ b/src/rules/rules.jl @@ -1,5 +1,46 @@ using Argus +# Rule disabling +# ============== + +""" + ReLintDisabler <: CommentDisabler + +Type for the ReLint-specific rule disabler. See `relint_disabler`. +""" +struct ReLintDisabler <: CommentDisabler end +const relint_disabler = ReLintDisabler() + +""" + relint_disabler([rule::Rule,] line::AbstractString) + +ReLint-specific rule disabler. Allows disabling rules in source code via comments of the +form `# lint-disable[: [(|), ]+]?`. The rules are disabled for the +annotated node. + +See also: `Argus.default_disabler` +""" +relint_disabler(line::AbstractString) = is_disable_all_comment(line) +function relint_disabler(rule::Rule, line::AbstractString) + is_disable_comment(line) || return false + is_disable_all_comment(line) && return true + is_disable_rule_comment(line, rule) && return true + return false +end + +is_disable_comment(str::AbstractString) = + startswith(str, "# lint-disable-next-line") +is_disable_all_comment(str::AbstractString) = str == "# lint-disable-next-line" +function is_disable_rule_comment(str::AbstractString, rule::Rule) + is_disable_comment(str) || return false + split_command_from_rules = split(str, ":") + length(split_command_from_rules) == 2 || return false + rules_list = strip.(split(split_command_from_rules[2], ",")) + rule.name in rules_list && return true + # Allow disabling by rule description as well. + return startswith(rule.description, lstrip(split_command_from_rules[2])) +end + include("utils.jl") include("recommendations.jl") include("violations.jl") diff --git a/src/rules/utils.jl b/src/rules/utils.jl index 6b74b13..4772d7c 100644 --- a/src/rules/utils.jl +++ b/src/rules/utils.jl @@ -1,3 +1,6 @@ +# Rule hooks +# ========== + # TODO: `string_literal` syntax class @define_rule_hook :exclude_files begin args = @pattern [{files}...] diff --git a/test/common.jl b/test/common.jl index b3be81d..b747a32 100644 --- a/test/common.jl +++ b/test/common.jl @@ -1,4 +1,4 @@ -using ReLint: ReLint, run_lint_on_text, MarkdownFormat, PlainFormat, has_values +using ReLint: ReLint, run_lint_on_text, MarkdownFormat, PlainFormat using ReLint: LintGlobalReport, LintContext using Test @@ -31,3 +31,8 @@ end function lint_has_error_test(source::String, verbose=false; directory::String = "", context::LintContext=LintContext()) return count_lint_errors(source, verbose; directory, context) > 0 end + +has_values(l::ReLint.LintGlobalReport, a, b, c) = + l.files_count == a && + l.violations_count == b && + l.recommendations_count == c diff --git a/test/interface.jl b/test/interface.jl index c58ba67..dcaab21 100644 --- a/test/interface.jl +++ b/test/interface.jl @@ -128,8 +128,8 @@ end # First violations across files, then recommendations across files expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + ## Static analysis report + \*\*Output of \[ReLint\.jl\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* Report creation time \(UTC\): \H+ - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ @@ -141,7 +141,7 @@ end - 🚨\*\*In total, 0 fatal rule violation, 1 rule violation and 1 PR reviewer recommendation are found over 2 Julia files\*\*🚨 + 🚨\*\*In total, 0 fatal violations, 1 violation and 1 PR reviewer recommendation were found over 2 Julia files\*\*🚨 """ result_matching = !isnothing(match(expected, result)) # DEBUG: @@ -183,8 +183,8 @@ end # First violations across files, then recommendations across files expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + ## Static analysis report + \*\*Output of \[ReLint\.jl\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* Report creation time \(UTC\): \H+ - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ @@ -197,7 +197,7 @@ end - 🚨\*\*In total, 0 fatal rule violation, 2 rule violations and 2 PR reviewer recommendations are found over 2 Julia files\*\*🚨 + 🚨\*\*In total, 0 fatal violations, 2 violations and 2 PR reviewer recommendations were found over 2 Julia files\*\*🚨 """ result_matching = !isnothing(match(expected, result)) # DEBUG: @@ -244,8 +244,8 @@ end # First violations across files, then recommendations across files expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + ## Static analysis report + \*\*Output of \[ReLint\.jl\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* Report creation time \(UTC\): \H+ - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ - \*\*Line 2, column 3:\*\* Use `@spawn` instead of `@async`\. \H+ @@ -258,7 +258,7 @@ end - 🚨\*\*In total, 0 fatal rule violation, 2 rule violations and 2 PR reviewer recommendations are found over 2 Julia files\*\*🚨 + 🚨\*\*In total, 0 fatal violations, 2 violations and 2 PR reviewer recommendations were found over 2 Julia files\*\*🚨 """ result_matching = !isnothing(match(expected, result)) # DEBUG: @@ -283,10 +283,10 @@ end expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + ## Static analysis report + \*\*Output of \[ReLint\.jl\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* Report creation time \(UTC\): \H+ - No Julia file is modified or added in this PR. + No Julia files were modified or added in this PR. """ result = open(io->read(io, String), output_file) @@ -319,10 +319,10 @@ end expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + ## Static analysis report + \*\*Output of \[ReLint\.jl\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* Report creation time \(UTC\): \H+ - No Julia file is modified or added in this PR. + No Julia files were modified or added in this PR. """ result_matching = !isnothing(match(expected, result)) end @@ -359,10 +359,10 @@ end end expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + ## Static analysis report + \*\*Output of \[ReLint\.jl\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* Report creation time \(UTC\): \H+ - 🎉No potential threats are found over 2 Julia files.👍 + 🎉No potential threats were found over 2 Julia files.👍 """ result_matching = !isnothing(match(expected, result)) end @@ -396,10 +396,10 @@ end end expected = r""" - ## Static code analyzer report - \*\*Output of the \[ReLint\.jl code analyzer\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* + ## Static analysis report + \*\*Output of \[ReLint\.jl\]\(https://github\.com/RelationalAI-oss/ReLint\.jl\).+\*\* Report creation time \(UTC\): \H+ - 🎉No potential threats are found over 1 Julia file.👍 + 🎉No potential threats were found over 1 Julia file.👍 """ result_matching = !isnothing(match(expected, result)) end @@ -470,7 +470,7 @@ end github_repository="RelationalAI/raicode", branch_name="axb-foo-bar", file_prefix_to_remove="var/", - analyze_all_file_found_locally=true, # OVERRIDE THE PROVIDED SET OF FILES + local_files_only=true, # OVERRIDE THE PROVIDED SET OF FILES stream_workflowcommand=devnull ) @@ -524,8 +524,11 @@ end lines_count = length(all_lines) @test lines_count < 70 - @test all_lines[end-3] == "⚠️Only a subset of the violations and recommandations are here reported⚠️" - @test all_lines[end-2] == "🚨**In total, 0 fatal rule violation, 100 rule violations and 0 PR reviewer recommendation are found over 1 Julia file**🚨" + @test all_lines[end-3] == "⚠️Only a subset of the reports are shown⚠️" + @test all_lines[end-2] == """ + 🚨**In total, 0 fatal violations, 100 \ + violations and 0 PR reviewer recommendations \ + were found over 1 Julia file**🚨""" @test all_lines[end] == "" end end @@ -814,7 +817,7 @@ end expected = r""" ---------- \H+ Line 2, column 5: Use `@spawn` instead of `@async`\. \H+ - 1 potential threat is found: 0 fatal violation, 1 violation and 0 recommendation + 1 potential threat was found: 0 fatal violations, 1 violation and 0 recommendations ---------- """ @test !isnothing(match(expected, result)) @@ -860,7 +863,7 @@ end result = String(take!(str)) expected = r""" - 2 potential threats are found: 0 fatal violation, 2 violations and 0 recommendation + 2 potential threats were found: 0 fatal violations, 2 violations and 0 recommendations """ result_matching = !isnothing(match(expected, result)) end @@ -894,8 +897,8 @@ end expected = r""" Line 3, column 5: Unsafe logging statement\. You must enclose variables and strings with `@safe\(\.\.\.\)`\. \H+/bar.jl Line 3, column 3: Unsafe logging statement\. You must enclose variables and strings with `@safe\(\.\.\.\)`\. \H+/foo.jl - 4 potential threats are found: 2 fatal violations, 2 violations and 0 recommendation - Note that the list above only show fatal violations + 4 potential threats were found: 2 fatal violations, 2 violations and 0 recommendations + Note that the list above only shows fatal violations """ result_matching = !isnothing(match(expected, result)) end @@ -931,7 +934,7 @@ end expected = r""" ---------- \H+ Line 1, column 1: `Threads.nthreads\(\)` should not be used in a constant variable\. \H+ - 1 potential threat is found: 0 fatal violation, 1 violation and 0 recommendation + 1 potential threat was found: 0 fatal violations, 1 violation and 0 recommendations ---------- """ @test !isnothing(match(expected, result)) diff --git a/test/lint_context_tests.jl b/test/lint_context_tests.jl index 68d57c9..7df0bd7 100644 --- a/test/lint_context_tests.jl +++ b/test/lint_context_tests.jl @@ -4,19 +4,19 @@ using ReLint: LineLintRule @testset "Basic" begin - @test isempty(LintContext(DataType[]).rules_to_run) + @test isempty(LintContext(DataType[]).rules) dts = DataType[UseOfStaticThreads, LogStatementsMustBeSafe] c = LintContext(dts) - @test length(c.rules_to_run) == 2 - @test c.rules_to_run == dts + @test length(c.rules) == 2 + @test c.rules == dts dts_as_string = ["UseOfStaticThreads", "LogStatementsMustBeSafe"] c = LintContext(dts_as_string) - @test length(c.rules_to_run) == 2 - @test c.rules_to_run == dts + @test length(c.rules) == 2 + @test c.rules == dts - @test iszero(LintContext([]).rules_to_run) + @test iszero(LintContext([]).rules) end @testset "Non-existing rule" begin @@ -61,10 +61,10 @@ @testset "AST and Line rules" begin context=LintContext() - @test !isempty(context.rules_to_run) + @test !isempty(context.rules) @test !isempty(ast_rules(context)) @test !isempty(line_rules(context)) @test !isempty(filter(r->r <: LineLintRule, all_rules())) end -end \ No newline at end of file +end From d0d8a11b04188e5b814f8f59ebbba70d37c81d5f Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 9 Apr 2026 18:07:43 +0200 Subject: [PATCH 44/62] Update local script --- scripts/{run_julia_lint.sh => run_locally.sh} | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) rename scripts/{run_julia_lint.sh => run_locally.sh} (78%) diff --git a/scripts/run_julia_lint.sh b/scripts/run_locally.sh similarity index 78% rename from scripts/run_julia_lint.sh rename to scripts/run_locally.sh index 966314d..73093fc 100755 --- a/scripts/run_julia_lint.sh +++ b/scripts/run_locally.sh @@ -3,32 +3,33 @@ # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Run lint on a set of files, provided as arguments. # Result are printed in the stdout, if a fatal violation is found, then exit with 1 -# A rule can be specified using --rule NAME_OF_A_RULE +# A rule can be specified using --rule RULE # If no rule is provided, then run all the rules. # EXAMPLE1: # pwd -# /Users/alexandrebergel/Documents/RAI/ReLint.jl +# .../ReLint.jl -# ./scripts/run_lint_locally.sh /Users/alexandrebergel/Documents/RAI/raicode21/src/*.jl -# FULLNAME SCRIPT ./scripts/run_lint_locally.sh +# ./scripts/run_locally.sh dummy.jl +# FULLNAME SCRIPT ./scripts/run_locally.sh # RELINT PATH= ./scripts/.. -# CURRENT PATH= /Users/alexandrebergel/Documents/RAI/ReLint.jl +# CURRENT PATH= .../ReLint.jl # FILES_TO_RUN= /var/folders/nz/1c4rst196ws_18tjtfl0yb980000gn/T/tmp.SloaMkSs16 -# About to run ReLint.jl... +# About to run ReLint... # [ Info: Running lint on 2 files -# Line 47, column 9: Unsafe logging statement. You must enclose variables and strings with `@safe(...)`. /Users/alexandrebergel/Documents/RAI/raicode21/src/version.jl +# Line 47, column 9: Unsafe logging statement. You must enclose variables and strings with +# `@safe(...)`. dummy.jl # 6 potential threats are found: 1 fatal violation, 5 violations and 0 recommendation # Note that the list above only show fatal violations # ┌ Error: Fatal error discovered # └ @ Main none:25 # EXAMPLE2: -# ./scripts/run_julia_lint.sh /Users/alexandrebergel/Documents/RAI/raicode4/ --rule LogStatementsMustBeSafe +# ./scripts/run_locally.sh dummy/ --rule VIOLATIONS["@async"] # EXAMPLE3: -# ./scripts/run_julia_lint.sh /Users/alexandrebergel/Documents/RAI/raicode4/ +# ./scripts/run_locally.sh dummy/ # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -39,7 +40,7 @@ while [[ $# -gt 0 ]]; do # echo "DEBUG: " $1 case $1 in -r|--rule) - RULE+="\"$2\", " + RULE+="ReLint.$2, " shift # past argument shift # past value ;; @@ -90,7 +91,7 @@ echo "RULE = $RULE" echo "RELINTPATH PATH =" $RELINTPATH echo "FILES_TO_RUN =" $FILES_TO_RUN -echo "About to run ReLint.jl..." +echo "About to run ReLint..." MAX_RETRIES=5 RETRY_DELAY=5 ATTEMPT=0 @@ -100,7 +101,7 @@ ATTEMPT=0 # So we have a retry mechanism. while [[ $ATTEMPT -lt $MAX_RETRIES ]]; do ATTEMPT=$((ATTEMPT + 1)) - echo "Attempt $ATTEMPT of $MAX_RETRIES to run ReLint.jl..." + echo "Attempt $ATTEMPT of $MAX_RETRIES to run ReLint..." julia --startup-file=no --history-file=no --project=$RELINTPATH -e " import Pkg @@ -122,13 +123,14 @@ while [[ $ATTEMPT -lt $MAX_RETRIES ]]; do formatter = ReLint.PreCommitFormat() context = LintContext($RULE) - @info \"context\" context + rule_names = [r.name for r in context.rules] + @info \"rules\" rule_names for f in all_files ReLint.run_lint(f; result, formatter, context) end - if result.fatalviolations_count > 0 + if result.fatal_violations_count > 0 ReLint.print_summary(formatter, stdout, result) @error \"Fatal error discovered\" exit(1) @@ -140,18 +142,18 @@ while [[ $ATTEMPT -lt $MAX_RETRIES ]]; do EXIT_STATUS=$? if [[ $EXIT_STATUS -eq 0 ]]; then - echo "ReLint.jl completed successfully." + echo "ReLint completed successfully." exit 0 elif [[ $EXIT_STATUS -eq 1 ]]; then - echo "ReLint.jl found fatal violations" + echo "ReLint found fatal violations" exit 1 else - echo "ReLint.jl failed with exit code $EXIT_STATUS. Retrying in $RETRY_DELAY seconds..." + echo "ReLint failed with exit code $EXIT_STATUS. Retrying in $RETRY_DELAY seconds..." sleep $RETRY_DELAY fi done if [[ $ATTEMPT -eq $MAX_RETRIES ]]; then - echo "ReLint.jl failed after $MAX_RETRIES attempts." + echo "ReLint failed after $MAX_RETRIES attempts." exit $EXIT_STATUS fi From 95d8e9daacabf8f22c883e6c128e858bdf8e8220 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 10 Apr 2026 16:03:58 +0200 Subject: [PATCH 45/62] Add comment rules --- src/interface.jl | 27 ++-------------- src/rules/fatal.jl | 6 ++++ src/rules/rules.jl | 3 +- src/rules/text_lint_rules.jl | 21 ------------- src/rules/violations.jl | 6 ++++ test/fatal.jl | 23 ++++++++++++++ test/line_rules_tests.jl | 60 ------------------------------------ test/runtests.jl | 1 - test/violations.jl | 22 +++++++++++++ 9 files changed, 60 insertions(+), 109 deletions(-) delete mode 100644 src/rules/text_lint_rules.jl delete mode 100644 test/line_rules_tests.jl diff --git a/src/interface.jl b/src/interface.jl index 4f487b5..9dcb634 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -249,13 +249,12 @@ Runs lint checks on `text`. Lints will be reported as comming from `filename`. function lint_text(text::String; filename="", context::LintContext=LintContext()) ast = JuliaSyntax.parseall(SyntaxNode, text; filename=filename) ast = Argus._normalise!(ast) - all_lines = split(text, "\n") lint_rule_reports = [] - - # AST rules match_results = rules_match(context.rules, ast; disabler=relint_disabler) # Remove refactorings. + # + # TODO: Include refactorings as suggestions. for (rule_name, match_result) in match_results successful_match_results = map(m -> m[1], match_result.matches) rule = @@ -265,28 +264,6 @@ function lint_text(text::String; filename="", context::LintContext=LintC end end - # # Text rules - # for (line_number, line) in enumerate(all_lines) - # for rule_type in line_rules(context) - # rule = rule_type() - # (is_error, msg) = check(rule, string(line), markers) - # if is_error - # # We have a violation - # lint_rule_report = LintRuleReport( - # rule, - # msg, - # "", - # filename, - # line_number, - # 1, - # false, - # 0 - # ) - # push!(lint_rule_reports, lint_rule_report) - # end - # end - # end - return lint_rule_reports end diff --git a/src/rules/fatal.jl b/src/rules/fatal.jl index 6962e42..c7cc1ba 100644 --- a/src/rules/fatal.jl +++ b/src/rules/fatal.jl @@ -257,3 +257,9 @@ FATAL_VIOLATIONS["return in anonymous function"] = Rule( {_:::do_call_with_return} ) ) + +FATAL_VIOLATIONS["TODO PR"] = Rule( + "TODO PR", + "Use `TODO (RAI-XXXXX)` instead of `TODO PR` to refer to a Jira issue.", + @comment r"TODO \(?PR\)?[\S\s]*" +) diff --git a/src/rules/rules.jl b/src/rules/rules.jl index 2a812e3..e46bcb2 100644 --- a/src/rules/rules.jl +++ b/src/rules/rules.jl @@ -45,10 +45,9 @@ include("utils.jl") include("recommendations.jl") include("violations.jl") include("fatal.jl") -include("text_lint_rules.jl") all_rules() = [ values(RECOMMENDATIONS)..., values(VIOLATIONS)..., - values(FATAL_VIOLATIONS)... + values(FATAL_VIOLATIONS)..., ] diff --git a/src/rules/text_lint_rules.jl b/src/rules/text_lint_rules.jl deleted file mode 100644 index fc50585..0000000 --- a/src/rules/text_lint_rules.jl +++ /dev/null @@ -1,21 +0,0 @@ -abstract type LintRule end -abstract type LineLintRule <: LintRule end -abstract type LineRecommendationLintRule <: LineLintRule end -abstract type LineViolationLintRule <: LineLintRule end -abstract type LineFatalLintRule <: LineLintRule end - -# Line rules -struct TodoPrRule <: LineFatalLintRule end -struct TodoJiraIssueRule <: LineViolationLintRule end - -function check(t::TodoPrRule, line::String, markers::Dict{Symbol,String}) - !isnothing(match(r"TODO\s?\(PR\)", line)) && return true, "`TODO (PR)` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue." - !isnothing(match(r"TODO PR", line)) && return true, "`TODO PR` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue." - return false, "" -end - -function check(t::TodoJiraIssueRule, line::String, markers::Dict{Symbol,String}) - (contains(line, "TODO") && !contains(line, r"TODO\s?\(RAI-\d+\)")) && - return true, "`TODO` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue." - return false, "" -end diff --git a/src/rules/violations.jl b/src/rules/violations.jl index 4ea5ad7..2f97148 100644 --- a/src/rules/violations.jl +++ b/src/rules/violations.jl @@ -345,3 +345,9 @@ VIOLATIONS["not fully parametrised constructor"] = Rule( :exclude_files => ["test/", "test.jl"] ) ) + +VIOLATIONS["TODO"] = Rule( + "TODO", + "Use `TODO (RAI-XXXXX)` instead of `TODO` to refer to a Jira issue.", + @comment r"TODO(?!:?\s*\(RAI-\d+\))[\S\s]*" +) diff --git a/test/fatal.jl b/test/fatal.jl index fcb5763..cbd5152 100644 --- a/test/fatal.jl +++ b/test/fatal.jl @@ -243,4 +243,27 @@ end end + @testset "TODO PR" begin + context = ReLint.LintContext([ReLint.FATAL_VIOLATIONS["TODO PR"]]) + @test lint_test("function f()\n # TODO (PR): fix this\n end", + """ + Line 2, column 2: Use `TODO (RAI-XXXXX)` instead of `TODO PR` \ + to refer to a Jira issue."""; + context) + @test lint_test("function f()\n # TODO PR: fix this\n end", + """ + Line 2, column 2: Use `TODO (RAI-XXXXX)` instead of `TODO PR` \ + to refer to a Jira issue."""; + context) + @test lint_test("function f()\n @info \"zork\" # TODO PR: fix this\n end", + """ + Line 2, column 15: Use `TODO (RAI-XXXXX)` instead of `TODO PR` \ + to refer to a Jira issue."""; + context) + + @test !lint_has_error_test("function f()\n # TODO (RAI-121) okay\n end") + @test !lint_has_error_test("function f()\n # TODO: fix this\n end"; context) + @test !lint_has_error_test("function f()\n # fix this\n end"; context) + end + end diff --git a/test/line_rules_tests.jl b/test/line_rules_tests.jl deleted file mode 100644 index d2fcf7c..0000000 --- a/test/line_rules_tests.jl +++ /dev/null @@ -1,60 +0,0 @@ -@testset "Line rule tests" begin - @testset "TodoPrRule tests" begin - using ReLint: TodoJiraIssueRule, TodoPrRule, SplattingRule - using ReLint: is_recommendation, is_violation, is_fatal - @test !is_recommendation(TodoPrRule()) - @test !is_recommendation(TodoJiraIssueRule()) - @test is_violation(TodoJiraIssueRule()) - @test is_fatal(TodoPrRule()) - @test !is_fatal(TodoJiraIssueRule()) - @test is_recommendation(SplattingRule()) - end - - @testset "TodoPrRule tests" begin - using ReLint: LintContext, TodoPrRule - - context = LintContext([TodoPrRule]) - @test lint_has_error_test("function f()\n # TODO (PR): fix this\n end"; context) - @test lint_test("function f()\n # TODO (PR): fix this\n end", - "Line 2, column 1: `TODO (PR)` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue.") - - @test lint_has_error_test("function f()\n # TODO PR: fix this\n end"; context) - @test lint_test("function f()\n # TODO PR: fix this\n end", - "Line 2, column 1: `TODO PR` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue.") - - @test !lint_has_error_test("function f()\n # TODO: fix this\n end"; context) - @test !lint_has_error_test("function f()\n # fix this\n end"; context) - - @test lint_test("function f()\n @info \"zork\" # TODO PR: fix this\n end", - "Line 2, column 2: Unsafe logging statement. You must enclose variables and strings with `@safe(...)`.") - - @test lint_test("function f()\n @info \"zork\" # TODO PR: fix this\n end", - "Line 2, column 1: `TODO PR` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue.") - - @test !lint_has_error_test("function f()\n # TODO probably okay\n end"; context) - end - - @testset "TodoPrRule tests" begin - using ReLint: LintContext, TodoJiraIssueRule - - context = LintContext([TodoJiraIssueRule]) - @test lint_has_error_test("function f()\n # TODO (PR): fix this\n end"; context) - @test lint_test("function f()\n # TODO (PR): fix this\n end", - "Line 2, column 1: `TODO` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue.") - - @test lint_has_error_test("function f()\n # TODO: fix this\n end"; context) - @test lint_test("function f()\n # TODO: fix this\n end", - "Line 2, column 1: `TODO` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue.") - - @test !lint_has_error_test("function f()\n # TODO (RAI-12314): fix this\n end"; context) - @test !lint_has_error_test("function f()\n # fix this\n end"; context) - - @test lint_test("function f()\n @info \"zork\" # TODO: fix this\n end", - "Line 2, column 2: Unsafe logging statement. You must enclose variables and strings with `@safe(...)`.") - - @test lint_test("function f()\n @info \"zork\" # TODO fix this\n end", - "Line 2, column 1: `TODO` found, use `TODO (RAI-XXXXX)` instead to refer to a Jira issue.") - - @test !lint_has_error_test("function f()\n # TODO (RAI-121) probably okay\n end") - end -end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 65bb33e..408640f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,7 +3,6 @@ using Test include(joinpath(@__DIR__, "common.jl")) -# include(joinpath(@__DIR__, "line_rules_tests.jl")) include(joinpath(@__DIR__, "fatal.jl")) include(joinpath(@__DIR__, "violations.jl")) include(joinpath(@__DIR__, "recommendations.jl")) diff --git a/test/violations.jl b/test/violations.jl index 73095c7..7005c0c 100644 --- a/test/violations.jl +++ b/test/violations.jl @@ -706,4 +706,26 @@ end end + @testset "TODO comments" begin + context = ReLint.LintContext([ReLint.VIOLATIONS["TODO"]]) + @test lint_test("function f()\n # TODO (PR): fix this\n end", + """ + Line 2, column 2: Use `TODO (RAI-XXXXX)` instead of `TODO` \ + to refer to a Jira issue."""; + context) + @test lint_test("function f()\n # TODO: fix this\n end", + """ + Line 2, column 2: Use `TODO (RAI-XXXXX)` instead of `TODO` \ + to refer to a Jira issue."""; + context) + @test lint_test("function f()\n @info \"zork\" # TODO fix this\n end", + """ + Line 2, column 15: Use `TODO (RAI-XXXXX)` instead of `TODO` \ + to refer to a Jira issue."""; + context) + + @test !lint_has_error_test("function f()\n # TODO (RAI-121) okay\n end") + @test !lint_has_error_test("function f()\n # fix this\n end"; context) + end + end From c6f05c14ece5cd02448901dfa2cb4df9d192728a Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 10 Apr 2026 17:03:35 +0200 Subject: [PATCH 46/62] Remove Serialization dependency --- Project.toml | 1 - benchmark/benchmark_path.jl | 16 ++++++++++++++-- benchmark/run_benchmarks.jl | 21 +++++++++++++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Project.toml b/Project.toml index c5ce431..587cdf1 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,6 @@ Argus = "e39743ec-b2cc-45ef-a990-b666fc093538" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" -Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" [compat] Argus = "0.1.0" diff --git a/benchmark/benchmark_path.jl b/benchmark/benchmark_path.jl index cc4b109..da2d31b 100644 --- a/benchmark/benchmark_path.jl +++ b/benchmark/benchmark_path.jl @@ -2,8 +2,15 @@ if isempty(ARGS) || length(ARGS) == 1 error("provide package path as argument") end -using BenchmarkTools -using Serialization +import Pkg +benchmarktools_added = "BenchmarkTools" in keys(Pkg.project().dependencies) +serialization_added = "Serialization" in keys(Pkg.project().dependencies) +redirect_stdout(devnull) do + benchmarktools_added || Pkg.add("BenchmarkTools") + serialization_added || Pkg.add("Serialization") +end + +using BenchmarkTools, Serialization import ReLint b = redirect_stdout(devnull) do @@ -11,3 +18,8 @@ b = redirect_stdout(devnull) do end serialize(ARGS[1], b) + +redirect_stdout(devnull) do + benchmarktools_added || Pkg.rm("BenchmarkTools"; mode=Pkg.PKGMODE_MANIFEST) + serialization_added || Pkg.rm("Serialization"; mode=Pkg.PKGMODE_MANIFEST) +end diff --git a/benchmark/run_benchmarks.jl b/benchmark/run_benchmarks.jl index 6d386e0..99b759d 100644 --- a/benchmark/run_benchmarks.jl +++ b/benchmark/run_benchmarks.jl @@ -1,7 +1,15 @@ -using BenchmarkTools, Serialization -import Pkg import Git +import Pkg +benchmarktools_added = "BenchmarkTools" in keys(Pkg.project().dependencies) +serialization_added = "Serialization" in keys(Pkg.project().dependencies) +redirect_stdout(devnull) do + benchmarktools_added || Pkg.add("BenchmarkTools") + serialization_added || Pkg.add("Serialization") +end + +using BenchmarkTools, Serialization + const git = Git.git() const BENCHMARK_DIR = if !isempty(ARGS) && isdir(ARGS[1]) @@ -37,12 +45,12 @@ b_main = try run(`julia --project=. $(BENCHMARKS_FILE) $(SERIALIZED_BENCHMARKS_FILE) ../$(MAIN_BRANCH)`) deserialize(SERIALIZED_BENCHMARKS_FILE) catch err - run(`$git worktree remove ../$(MAIN_BRANCH)`) + run(`$git worktree remove ../$(MAIN_BRANCH) --force`) rethrow(err) end # Clean up. cd(CURRENT_DIR) -run(`$git worktree remove ../$(MAIN_BRANCH)`) +run(`$git worktree remove ../$(MAIN_BRANCH) --force`) # Run benchmarks on Argus branch. Pkg.activate(CURRENT_DIR) @@ -62,3 +70,8 @@ println(b_argus) println() println("Comparison (`iulia/argus` vs `main`):") println(judge(median(b_argus), median(b_main))) + +redirect_stdout(devnull) do + benchmarktools_added || Pkg.rm("BenchmarkTools"; mode=Pkg.PKGMODE_MANIFEST) + serialization_added || Pkg.rm("Serialization"; mode=Pkg.PKGMODE_MANIFEST) +end From f75932bd470ee7c56a242773d194cb07cde554b3 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Fri, 10 Apr 2026 17:47:55 +0200 Subject: [PATCH 47/62] Include `LintContext` tests in interface tests --- test/interface.jl | 49 ++++++++++++++++++++++++++ test/lint_context_tests.jl | 70 -------------------------------------- test/runtests.jl | 5 ++- 3 files changed, 51 insertions(+), 73 deletions(-) delete mode 100644 test/lint_context_tests.jl diff --git a/test/interface.jl b/test/interface.jl index dcaab21..9f9cf01 100644 --- a/test/interface.jl +++ b/test/interface.jl @@ -1,3 +1,52 @@ +@testset "LintContext" begin + using ReLint: LintContext, VIOLATIONS + + @testset "Basic" begin + @test isempty(LintContext(Rule[]).rules) + + rules = [VIOLATIONS["@async"], VIOLATIONS["unsafe_ function"]] + c = LintContext(rules) + @test length(c.rules) == 2 + @test c.rules == rules + + @test iszero(LintContext(Rule[]).rules) + end + + @testset "Setting context or not" begin + source = """ + function f() + _unsafe_g() + end + + function _unsafe_g() + return 42 + end + """ + @test count_lint_errors(source) == 1 + @test count_lint_errors(source; context=LintContext()) == 1 + @test count_lint_errors(source; context=LintContext(Rule[])) == 0 + @test count_lint_errors(source; + context=LintContext([VIOLATIONS["@async"]])) == 0 + @test count_lint_errors(source; + context=LintContext([VIOLATIONS["unsafe_ function"]])) == 1 + end + + @testset "File in a dir" begin + mktempdir() do dir + open(joinpath(dir, "foo.jl"), "w") do io + write(io, "function f()\n @async 1 + 1\nend\n") + flush(io) + + @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) + + context=LintContext(Rule[]) + @test has_values(ReLint.run_lint(dir; io, context), 1, 0, 0) + + end + end + end +end + @testset "Run several times on same file" begin mktempdir() do dir open(joinpath(dir, "foo.jl"), "w") do io diff --git a/test/lint_context_tests.jl b/test/lint_context_tests.jl deleted file mode 100644 index 7df0bd7..0000000 --- a/test/lint_context_tests.jl +++ /dev/null @@ -1,70 +0,0 @@ -@testset "Lint context" begin - using ReLint: LintContext, UseOfStaticThreads, LogStatementsMustBeSafe, UnsafeRule - using ReLint: ast_rules, line_rules, all_rules - using ReLint: LineLintRule - - @testset "Basic" begin - @test isempty(LintContext(DataType[]).rules) - - dts = DataType[UseOfStaticThreads, LogStatementsMustBeSafe] - c = LintContext(dts) - @test length(c.rules) == 2 - @test c.rules == dts - - dts_as_string = ["UseOfStaticThreads", "LogStatementsMustBeSafe"] - c = LintContext(dts_as_string) - @test length(c.rules) == 2 - @test c.rules == dts - - @test iszero(LintContext([]).rules) - end - - @testset "Non-existing rule" begin - dts_as_string = ["UseOfStaticThreads", "LogStatementsMustBeSafe", "DoesNotExist"] - @test_throws ErrorException LintContext(dts_as_string) - end - - @testset "Setting context or not" begin - source = """ - function f() - _unsafe_g() - end - - function _unsafe_g() - return 42 - end - """ - @test count_lint_errors(source) == 1 - @test count_lint_errors(source; context=LintContext()) == 1 - @test count_lint_errors(source; context=LintContext([])) == 0 - @test count_lint_errors(source; context=LintContext([LogStatementsMustBeSafe])) == 0 - @test count_lint_errors(source; context=LintContext([UnsafeRule])) == 1 - - @test count_lint_errors(source; context=LintContext(["LogStatementsMustBeSafe"])) == 0 - @test count_lint_errors(source; context=LintContext(["UnsafeRule"])) == 1 - end - - @testset "File in a dir" begin - mktempdir() do dir - open(joinpath(dir, "foo.jl"), "w") do io - write(io, "function f()\n @async 1 + 1\nend\n") - flush(io) - - @test has_values(ReLint.run_lint(dir; io), 1, 1, 0) - - context=LintContext([]) - @test has_values(ReLint.run_lint(dir; io, context), 1, 0, 0) - - end - end - end - - @testset "AST and Line rules" begin - context=LintContext() - @test !isempty(context.rules) - @test !isempty(ast_rules(context)) - @test !isempty(line_rules(context)) - - @test !isempty(filter(r->r <: LineLintRule, all_rules())) - end -end diff --git a/test/runtests.jl b/test/runtests.jl index 408640f..fe04512 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,11 +1,10 @@ using ReLint +using Argus: Rule, @pattern using Test include(joinpath(@__DIR__, "common.jl")) +include(joinpath(@__DIR__, "interface.jl")) include(joinpath(@__DIR__, "fatal.jl")) include(joinpath(@__DIR__, "violations.jl")) include(joinpath(@__DIR__, "recommendations.jl")) - -# include(joinpath(@__DIR__, "lint_context_tests.jl")) -include(joinpath(@__DIR__, "interface.jl")) From 00acf58b26d72f77b0551aff35302299ac9fccc8 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Mon, 13 Apr 2026 14:38:24 +0200 Subject: [PATCH 48/62] Add single-rule constructor method for `LintContext` --- src/interface.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interface.jl b/src/interface.jl index 9dcb634..78deda9 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -228,6 +228,7 @@ struct LintContext file_exclusions::Vector{LintFileExclusion} LintContext(rules::Vector{Rule}) = new(rules, []) + LintContext(rule::Rule) = new([rule], []) LintContext() = new(all_rules(), []) LintContext(a, b) = new(a, b) end From 448aefc04692fb1893b9e428cafb875ae6b8008e Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Mon, 13 Apr 2026 14:38:51 +0200 Subject: [PATCH 49/62] Update local script and pre-commit hooks --- .pre-commit-hooks.yaml | 10 ++--- scripts/run_locally.sh | 96 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 37f369f..84b4308 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -2,21 +2,21 @@ name: lint-checks description: Checks for ReLint violations in Julia code base language: script - entry: ./scripts/run_julia_lint.sh + entry: ./scripts/run_locally.sh types: [file] - id: unsafe-log-checks - args: [--rule, LogStatementsMustBeSafe] + args: [--rule, 'FATAL_VIOLATIONS["unsafe-logging"]'] name: unsafe-log-checks description: Checks for unsafe logs in Julia code base language: script - entry: ./scripts/run_julia_lint.sh + entry: ./scripts/run_locally.sh types: [file] - id: lint-fatal-checks - args: [--rule, LogStatementsMustBeSafe, --rule, AssertionStatementsMustBeSafe, --rule, NoinlineAndLiteralRule, --rule, NoReturnInAnonymousFunctionRule, --rule, GeneratedRule, --rule, TodoPrRule] + args: [--rule-group, FATAL_VIOLATIONS] name: lint-fatal-checks description: Checks for fatal violations in Julia code base language: script - entry: ./scripts/run_julia_lint.sh + entry: ./scripts/run_locally.sh types: [file] diff --git a/scripts/run_locally.sh b/scripts/run_locally.sh index 73093fc..33bf2e1 100755 --- a/scripts/run_locally.sh +++ b/scripts/run_locally.sh @@ -6,7 +6,7 @@ # A rule can be specified using --rule RULE # If no rule is provided, then run all the rules. -# EXAMPLE1: +# Run all rules: # pwd # .../ReLint.jl @@ -25,22 +25,64 @@ # ┌ Error: Fatal error discovered # └ @ Main none:25 -# EXAMPLE2: -# ./scripts/run_locally.sh dummy/ --rule VIOLATIONS["@async"] +# Run specific rule: +# ./scripts/run_locally.sh dummy/ --rule 'VIOLATIONS["@async"]' +# ./scripts/run_locally.sh dummy/ -r 'VIOLATIONS["@async"]' -# EXAMPLE3: -# ./scripts/run_locally.sh dummy/ +# Run multiple rules: +# ./scripts/run_locally.sh dummy/ --rules 'VIOLATIONS["@async"][,] FATAL_VIOLATIONS["unsafe-logging"]' +# ./scripts/run_locally.sh dummy/ -rs 'VIOLATIONS["@async"][,] FATAL_VIOLATIONS["unsafe-logging"]' + +# Run rule group: +# ./scripts/run_locally.sh dummy/ --rule-group FATAL_VIOLATIONS +# ./scripts/run_locally.sh dummy/ -rg FATAL_VIOLATIONS + +# Run multiple rule groups: +# ./scripts/run_locally.sh dummy/ --rule-groups 'VIOLATIONS[,] FATAL_VIOLATIONS' +# ./scripts/run_locally.sh dummy/ -rgs 'VIOLATIONS[,] FATAL_VIOLATIONS' # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # PARSE ARGUMENTS RULE="" +RULES="Rule[]" +RULE_GROUP_NAME="" +RULE_GROUP_NAMES="" FILES_TO_RUN_FROM_COMMAND_LINE="" while [[ $# -gt 0 ]]; do # echo "DEBUG: " $1 case $1 in -r|--rule) - RULE+="ReLint.$2, " + RULE="ReLint.$2" + shift # past argument + shift # past value + ;; + -rs|--rules) + RULES="[" + for rule in $2 + do + rule=${rule%,} # remove trailing comma + RULES+="ReLint.$rule, " + done + RULES=${RULES%, } # remove trailing comma and space + RULES+="]" + shift # past argument + shift # past value + ;; + -rg|--rule-group) + RULE_GROUP_NAME+="ReLint.$2, " + shift # past argument + shift # past value + ;; + -rgs|--rule-groups) + RULE_GROUP_NAMES="[" + for rule_group in $2 + do + rule_group=${rule_group%,} # remove trailing comma + RULE_GROUP_NAMES+="ReLint.$rule_group, " + done + RULE_GROUP_NAMES=${RULE_GROUP_NAMES%, } # remove trailing comma and space + RULE_GROUP_NAMES+="]" shift # past argument shift # past value ;; @@ -75,10 +117,19 @@ else # echo "---" fi -# If no rule was set, when we have the empty rule -# Else, we set it for Julia -if [[ ! -z "$RULE" ]] ; then - RULE="[ $RULE ]" +# Create the `RULES` array either from the given rule group(s) or from the given rules. +if [[ ! -z "$RULE_GROUP_NAMES" ]] ; then + if [[ -z "$RULES" ]] ; then + RULES="append!([collect(values(rg)) for rg in $RULE_GROUP_NAMES]...)"; + else + RULES="append!($RULES, append!([collect(values(rg)) for rg in $RULE_GROUP_NAMES]...))"; + fi +elif [[ ! -z "$RULE_GROUP_NAME" ]] ; then + if [[ -z "$RULES" ]] ; then + RULES="collect(values($RULE_GROUP_NAME))"; + else + RULES="append!($RULES, collect(values($RULE_GROUP_NAME)))"; + fi fi # Initializing some variables @@ -87,9 +138,18 @@ RELINTPATH=$(dirname $0)/.. # Running StaticLint echo "FULLNAME SCRIPT =" $0 # echo "FILES_TO_RUN_FROM_COMMAND_LINE = " $FILES_TO_RUN_FROM_COMMAND_LINE -echo "RULE = $RULE" +if [[ ! -z "$RULE" ]] ; then + echo "RULE = $RULE" +fi +if [[ ! -z "$RULE_GROUP_NAMES" ]] ; then + echo "RULE_GROUP_NAMES = $RULE_GROUP_NAMES" +elif [[ ! -z "$RULE_GROUP_NAME" ]] ; then + echo "RULE_GROUP_NAME = $RULE_GROUP_NAME"; +elif [[ "$RULES" != "Rule[]" ]] ; then + echo "RULES = $RULES" +fi echo "RELINTPATH PATH =" $RELINTPATH -echo "FILES_TO_RUN =" $FILES_TO_RUN +echo "FILES_TO_RUN =" $(cat $FILES_TO_RUN) echo "About to run ReLint..." MAX_RETRIES=5 @@ -113,6 +173,7 @@ while [[ $ATTEMPT -lt $MAX_RETRIES ]]; do end using ReLint: ReLint, LintContext + using Argus: Rule result = ReLint.LintGlobalReport() all_files_tmp=split(open(io->read(io, String), \"$FILES_TO_RUN\", \"r\")) all_files=map(string, all_files_tmp) @@ -122,9 +183,10 @@ while [[ $ATTEMPT -lt $MAX_RETRIES ]]; do @info \"Running lint on \$(length(all_files)) files\" formatter = ReLint.PreCommitFormat() - context = LintContext($RULE) + rules = $RULES + context = isempty(rules) ? LintContext($RULE) : LintContext(rules) rule_names = [r.name for r in context.rules] - @info \"rules\" rule_names + @info \"Running rules:\" rule_names for f in all_files ReLint.run_lint(f; result, formatter, context) @@ -134,6 +196,9 @@ while [[ $ATTEMPT -lt $MAX_RETRIES ]]; do ReLint.print_summary(formatter, stdout, result) @error \"Fatal error discovered\" exit(1) + else + ReLint.print_summary(formatter, stdout, result) + exit(2) end exit(0) " @@ -147,6 +212,9 @@ while [[ $ATTEMPT -lt $MAX_RETRIES ]]; do elif [[ $EXIT_STATUS -eq 1 ]]; then echo "ReLint found fatal violations" exit 1 + elif [[ $EXIT_STATUS -eq 2 ]]; then + echo "ReLint found non-fatal violations" + exit 0 else echo "ReLint failed with exit code $EXIT_STATUS. Retrying in $RETRY_DELAY seconds..." sleep $RETRY_DELAY From bf4a375eef72b406f66320506ed26e693b68ff38 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Mon, 13 Apr 2026 15:40:38 +0200 Subject: [PATCH 50/62] Update README --- README.md | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 197 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index dc4f358..163d6e4 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,207 @@ # ReLint.jl -ReLint is a static code analyzer for Julia. It searches for patterns in Julia source -code, such patterns aiming to indicate issues and deserve to be reported to the end-user. -ReLint.jl is inspired from [StaticLint.jl](https://github.com/julia-vscode/StaticLint.jl) -while being versatile. In particular, highlights of ReLint include: +Lightweight linter for Julia programs. It provides a set of linting +rules grouped by severity, GitHub Action integration and some +pre-commit hooks. -- Lint rules can be easily added; -- ReLint.jl is runnable from a GitHub Action workflow; -- ReLint.jl offer a [pre-commit](https://pre-commit.com/) hook, useful to run it at each commit; -- ReLint.jl is lightweight and fast. +## Overview -## Installing and Running ReLint +ReLint is a fast and lightweight linter for Julia built on top of +[Argus.jl](https://github.com/iuliadmtru/Argus.jl). It provides a set +of pre-defined linting rules grouped in three categories: +recommendations, violations and fatal violations. -Installing and running ReLint.jl is easy. Several options are available: +```julia +julia> using ReLint + +julia> keys(ReLint.RECOMMENDATIONS) +KeySet for a Dict{String, Argus.Rule} with 16 entries. Keys: + "string concatenation" + "@cfunction" + "@inbounds" + "finalizer" + "mutating ENV" + "@threads" + "isnothing" + "sleep" + "unlock" + "yield" + "@sync" + "interpolation in `@warnv_safe_to_log`" + "closure capture by reference" + "return type annotation" + "splatting" + "ccall" + +julia> keys(ReLint.VIOLATIONS) +KeySet for a Dict{String, Argus.Rule} with 27 entries. Keys: + "RelPath" + "@async" + "equal" + "unreachable branch" + "RelPath relpath_from_signature" + "RelPath drop_first" + "uv" + "unsafe_ function" + "untyped array comprehension" + "array with no specific type" + "no import RAICode" + "not fully parametrised constructor" + "in" + "no import" + "non-const untyped global" + "Task" + "error" + "haskey" + "static threads" + "RelPath split_path" + "TODO" + "string interpolation" + "initializing with `nthreads`" + "initializing with `is_local_deployment`" + "bare using" + "ErrorException" + "remove_page" + +julia> keys(ReLint.FATAL_VIOLATIONS) +KeySet for a Dict{String, Argus.Rule} with 7 entries. Keys: + "unsafe-logging" + "noinline with non-literal/identifier args" + "show" + "TODO PR" + "return in anonymous function" + "@generated" + "unsafe-assert" +``` + +The rules can be run on a file or directory using `run_lint`: + +```julia +julia> f = tempname() * ".jl"; + +julia> write(f, """ + function f(x)::Int + y = unsafe_f(x) + return y + end + """); + +julia> ReLint.run_lint(f) +---------- /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_HGJU58HAr2.jl +Line 2, column 9: An `unsafe_` function should be called only from an `unsafe_` function. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_HGJU58HAr2.jl +Line 1, column 1: Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_HGJU58HAr2.jl +ReLint.LintGlobalReport(1, 1, 1, 0, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_HGJU58HAr2.jl"], 2, ReLint.LintRuleReport[], "master") +``` + +The `scripts/` directory contains a shell script that can be used to +run the rules locally: + +```julia +➜ touch temp.jl +➜ echo "@generated function f(x)::Int + y = unsafe_f(x) + return y +end" >> temp.jl +➜ ./scripts/run_locally.sh temp.jl +FULLNAME SCRIPT = ./scripts/run_locally.sh +RELINTPATH PATH = ./scripts/.. +FILES_TO_RUN = temp.jl +About to run ReLint... +Attempt 1 of 5 to run ReLint... +[ Info: Running lint on 1 files +┌ Info: Running rules: +│ rule_names = +│ 50-element Vector{String}: +│ "string concatenation" +│ "@cfunction" +│ "@inbounds" +│ "finalizer" +│ "mutating ENV" +│ "@threads" +│ "isnothing" +│ "sleep" +│ "unlock" +│ "yield" +│ ⋮ +│ "ErrorException" +│ "remove_page" +│ "unsafe-logging" +│ "noinline with non-literal/identifier args" +│ "show" +│ "TODO PR" +│ "return in anonymous function" +│ "@generated" +└ "unsafe-assert" +Line 1, column 1: `@generated` should be used with extreme caution. temp.jl +3 potential threats were found: 1 fatal violation, 1 violation and 1 recommendation +Note that the list above only shows fatal violations +┌ Error: Fatal error discovered +└ @ Main none:32 +ReLint found fatal violations +➜ ./scripts/run_locally.sh temp.jl --rule 'VIOLATIONS["unsafe_ function"]' +FULLNAME SCRIPT = ./scripts/run_locally.sh +RULE = ReLint.VIOLATIONS["unsafe_ function"] +RELINTPATH PATH = ./scripts/.. +FILES_TO_RUN = temp.jl +About to run ReLint... +Attempt 1 of 5 to run ReLint... +[ Info: Running lint on 1 files +┌ Info: Running rules: +│ rule_names = +│ 1-element Vector{String}: +└ "unsafe_ function" +1 potential threat was found: 0 fatal violations, 1 violation and 0 recommendations +Note that the list above only shows fatal violations +ReLint found non-fatal violations +➜ ./scripts/run_locally.sh temp.jl --rule-group RECOMMENDATIONS +FULLNAME SCRIPT = ./scripts/run_locally.sh +RULE_GROUP_NAME = ReLint.RECOMMENDATIONS, +RELINTPATH PATH = ./scripts/.. +FILES_TO_RUN = temp.jl +About to run ReLint... +Attempt 1 of 5 to run ReLint... +[ Info: Running lint on 1 files +┌ Info: Running rules: +│ rule_names = +│ 16-element Vector{String}: +│ "string concatenation" +│ "@cfunction" +│ "@inbounds" +│ "finalizer" +│ "mutating ENV" +│ "@threads" +│ "isnothing" +│ "sleep" +│ "unlock" +│ "yield" +│ "@sync" +│ "interpolation in `@warnv_safe_to_log`" +│ "closure capture by reference" +│ "return type annotation" +│ "splatting" +└ "ccall" +1 potential threat was found: 0 fatal violations, 0 violations and 1 recommendation +Note that the list above only shows fatal violations +ReLint found non-fatal violations +``` + +Three pre-commit hooks are provided in `.pre-commit-hooks.yaml`. You +can modify or extend them by writing your own hooks file. + +## Getting Started + +### Installation + +ReLint is not yet registered in the Julia package system. It can be installed with `Pkg`: + +```julia +using Pkg +Pkg.add("https://github.com/RelationalAI-oss/ReLint.jl") +``` - - Run in the Julia REPL `import Pkg ; Pkg.add(url="https://github.com/RelationalAI-oss/ReLint.jl")` - - You can use our pre-commit hook. +### Basic Usage -## Basic usage +TODO: README updated until here... There are several ways to use ReLint.jl. Here are a few usage examples: From 7757b29f83b76b1b56afa18d647d27cffe8ee9eb Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Mon, 13 Apr 2026 17:55:47 +0200 Subject: [PATCH 51/62] Allow registering new rules --- src/interface.jl | 61 ++++++++++++++++++++++++++++++++++++++++++---- src/rules/rules.jl | 2 +- test/interface.jl | 4 +-- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/interface.jl b/src/interface.jl index 78deda9..4813f94 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -229,7 +229,7 @@ struct LintContext LintContext(rules::Vector{Rule}) = new(rules, []) LintContext(rule::Rule) = new([rule], []) - LintContext() = new(all_rules(), []) + LintContext() = new(ALL_RULES, []) LintContext(a, b) = new(a, b) end @@ -337,6 +337,7 @@ function run_lint(rootpath::String; io::Union{IO, Nothing}=stdout, io_violations::Union{IO, Nothing}=nothing, io_recommendations::Union{IO, Nothing}=nothing, + io_others::Union{IO, Nothing}=nothing, formatter::AbstractFormatter=PlainFormat(), context::LintContext=LintContext()) # Return if `rootpath` is already linted. @@ -363,9 +364,12 @@ function run_lint(rootpath::String; violation_reports = filter(is_violation, lint_reports) recommandation_reports = filter(is_recommendation, lint_reports) fatal_violation_reports = filter(is_fatal, lint_reports) + other_reports = filter(r -> !is_recommendation(r) && !is_violation(r) && !is_fatal(r), + lint_reports) count_violations = length(violation_reports) count_recommendations = length(recommandation_reports) - count_fatalviolations = length(fatal_violation_reports) + count_fatal_violations = length(fatal_violation_reports) + count_others = length(other_reports) # Print reports to their specific `IO`s. io_fatal_violations = isnothing(io_violations) ? io : io_violations for r in fatal_violation_reports @@ -379,11 +383,15 @@ function run_lint(rootpath::String; for r in recommandation_reports print_report(formatter, io_recommendations, r, result) end + io_others = isnothing(io_others) ? io : io_others + for r in other_reports + print_report(formatter, io_others, r, result) + end # Collect all reports and return the result. append!(result, LintGlobalReport(1, count_violations, count_recommendations, - count_fatalviolations, + count_fatal_violations, [rootpath], 0, lint_reports)) @@ -451,7 +459,7 @@ end file_prefix_to_remove::String="", local_files_only::Bool=false, stream_workflowcommand::IO=stdout, - rules::Vector{Rule}=all_rules(), + rules::Vector{Rule}=ALL_RULES, pre_commit_file::String="") Generate a Markdown report based on the linting all given files. The report is intenteded @@ -488,7 +496,7 @@ function generate_report(filenames::Vector{String}, file_prefix_to_remove::String="", local_files_only::Bool=false, stream_workflowcommand::IO=stdout, - rules::Vector{Rule}=all_rules(), + rules::Vector{Rule}=ALL_RULES, pre_commit_file::String="") if isfile(output_filename) @error "File $(output_filename) exist already." @@ -636,6 +644,49 @@ function generate_report(filenames::Vector{String}, return nothing end +# Registering rules +# ----------------- + +""" + register_rule!(rule::Rule) + +Register a rule to ReLint. +""" +function register_rule!(rule::Rule) + push!(ALL_RULES, rule) + return nothing +end + +""" + register_rules!(rules::Vector{Rule}) + +Register a set of rules to ReLint. +""" +function register_rules!(rules::Vector{Rule}) + append!(ALL_RULES, rules) + return nothing +end + +""" + register_rule_group!(rule_group::RuleGroup) + +Register a rule group to ReLint. +""" +function register_rule_group!(rule_group::RuleGroup) + append!(ALL_RULES, collect(values(rule_group))) + return nothing +end + +""" + register_rule_groups!(rule_groups::Vector{RuleGroup}) + +Register a set of rule groups to ReLint. +""" +function register_rule_groups!(rule_groups::Vector{RuleGroup}) + [register_rule_group!(rg) for rg in rule_groups] + return nothing +end + # Printing # -------- diff --git a/src/rules/rules.jl b/src/rules/rules.jl index e46bcb2..8ac2468 100644 --- a/src/rules/rules.jl +++ b/src/rules/rules.jl @@ -46,7 +46,7 @@ include("recommendations.jl") include("violations.jl") include("fatal.jl") -all_rules() = [ +ALL_RULES::Vector{Rule} = [ values(RECOMMENDATIONS)..., values(VIOLATIONS)..., values(FATAL_VIOLATIONS)..., diff --git a/test/interface.jl b/test/interface.jl index 9f9cf01..b87afb2 100644 --- a/test/interface.jl +++ b/test/interface.jl @@ -678,7 +678,7 @@ end flush(io2) re = extract_file_exclusions_from_precommit_file(precommit_full_path) - context = LintContext(ReLint.all_rules(), re) + context = LintContext(ReLint.ALL_RULES, re) # Run the linter on the directory str = IOBuffer() @@ -720,7 +720,7 @@ end flush(io2) re = extract_file_exclusions_from_precommit_file(precommit_full_path) - context = LintContext(ReLint.all_rules(), re) + context = LintContext(ReLint.ALL_RULES, re) # Run the linter on the directory output_file = tempname() From 1b2ca879aa24a2ff32e73b1f10b42e2c110719d3 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Mon, 13 Apr 2026 17:56:00 +0200 Subject: [PATCH 52/62] Add help text --- scripts/run_locally.sh | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/scripts/run_locally.sh b/scripts/run_locally.sh index 33bf2e1..002d974 100755 --- a/scripts/run_locally.sh +++ b/scripts/run_locally.sh @@ -86,8 +86,26 @@ while [[ $# -gt 0 ]]; do shift # past argument shift # past value ;; + -h|--help) + echo "./run_locally.sh [options]" + echo + echo "Available options:" + echo "-r|--rule - run a single rule" + echo "-rs|--rules - run a set of rules" + echo "-rg|--rule-group - run a rule group" + echo "-rgs|--rule-groups - run a set of rule groups" + echo "-h|--help - show this message" + exit 0 + ;; -*|--*) - echo "Unknown option $1" + echo "./run_locally.sh [options]" + echo + echo "Available options:" + echo "-r|--rule - run a single rule" + echo "-rs|--rules - run a set of rules" + echo "-rg|--rule-group - run a rule group" + echo "-rgs|--rule-groups - run a set of rule groups" + echo "-h|--help - show this message" exit 1 ;; *) From 18c23e65c8c4410b173393ad4a1b99a8ff04683d Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Mon, 13 Apr 2026 17:56:08 +0200 Subject: [PATCH 53/62] Update README --- README.md | 295 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 176 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 163d6e4..d33953f 100644 --- a/README.md +++ b/README.md @@ -201,145 +201,212 @@ Pkg.add("https://github.com/RelationalAI-oss/ReLint.jl") ### Basic Usage -TODO: README updated until here... +The main entry point of `ReLint` is `run_lint`, which runs all +available linting rules on a given file or directory. -There are several ways to use ReLint.jl. Here are a few usage examples: +```julia +julia> using ReLint -```Julia -ReLint.run_lint_on_text("function f() @async 1 + 2 end "); ----------- /var/folders/nz/1c4rst196ws_18tjtfl0yb980000gn/T/jl_1QHeJ2vm1U.jl -Line 1, column 14: Use `@spawn` instead of `@async`. /var/folders/nz/1c4rst196ws_18tjtfl0yb980000gn/T/jl_1QHeJ2vm1U.jl -1 potential threat is found: 1 violation and 0 recommendation +julia> f = tempname() * ".jl"; -``` -Replacing `@async` by `@spawn` make ReLint happy: +julia> write(f, """ + @generated function f(x)::Any + y = unsafe_f(x) + return y + end + """); -```Julia -julia> ReLint.run_lint_on_text("function f() @spawn 1 + 2 end "); ----------- /var/folders/nz/1c4rst196ws_18tjtfl0yb980000gn/T/jl_gbkLM58LEL.jl -No potential threats were found. ----------- +julia> ReLint.run_lint(f) +---------- /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +Line 1, column 1: `@generated` should be used with extreme caution. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +Line 2, column 9: An `unsafe_` function should be called only from an `unsafe_` function. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +Line 1, column 12: Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +ReLint.LintGlobalReport(1, 1, 1, 1, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl"], 3, ReLint.LintRuleReport[], "master") ``` -ReLint can be run on a file: +A specific set of rules can be given through `LintContext`: -```Julia -ReLint.run_lint("/Users/alexandrebergel/Documents/RAI/raicode13/src/RAICode.jl") +```julia +julia> ReLint.run_lint(f; context=ReLint.LintContext([values(ReLint.RECOMMENDATIONS)...])) +---------- /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +Line 1, column 12: Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +ReLint.LintGlobalReport(1, 0, 1, 0, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl"], 1, ReLint.LintRuleReport[], "master") ``` -Note that files directly and indirectly included by `RAICode.jl` are also analyzed. +ReLint's rules are constructs that come from +[Argus](https://github.com/iuliadmtru/Argus.jl). For more information +on syntax patterns, rules and rule matching see the [Argus +documentation](https://github.com/iuliadmtru/Argus.jl?tab=readme-ov-file#argusjl). -When a directory is provided to `run_lint`, then ReLint will look for Julia files. E.g., +The available set of rules can be extended either by adding rules to +the existing rule groups or by defining new groups: -```Julia -ReLint.run_lint("/Users/alexandrebergel/Documents/RAI/raicode13/src/") +```julia +julia> using Argus: Rule, @pattern + +julia> ReLint.RECOMMENDATIONS["my rule"] = Rule( + "my rule", + "This is my recommendation rule -- don't use `::Any`", + @pattern {_}::Any + ) +my rule: +This is my recommendation rule -- don't use `::Any` + +Pattern: +[::-i] + _:::expr :: ~var + Any :: Identifier + +Template: + + +Hooks: + + + +julia> ReLint.run_lint(f; context=ReLint.LintContext([values(ReLint.RECOMMENDATIONS)...])) +---------- /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +Line 1, column 12: Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +Line 1, column 21: This is my recommendation rule -- don't use `::Any` /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +ReLint.LintGlobalReport(1, 0, 2, 0, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl"], 2, ReLint.LintRuleReport[], "master") + +julia> MY_GROUP = RuleGroup("my group") +RuleGroup("my group") + +julia> MY_GROUP["my rule"] = Rule( + "my rule", + "This is my recommendation rule -- don't use `::Any`", + @pattern {_}::Any + ); + +julia> ReLint.run_lint(f; context=ReLint.LintContext([values(MY_GROUP)...])) +---------- /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +Line 1, column 21: This is my recommendation rule -- don't use `::Any` /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl +ReLint.LintGlobalReport(1, 0, 1, 0, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_nIHKJ0JiPW.jl"], 1, ReLint.LintRuleReport[], "master") ``` -The expression above outputs 1928 potential threats. +> [!NOTE] +> Currently, ReLint limits the output of the report to 60 errors. This +> limit can be increased by setting `MAX_REPORTED_ERRORS` to the +> desired amount. -## Contributing to ReLint.jl +### Defining New Rules -You may want to contribute to ReLint.jl for many reasons. Here are a few of them: +To learn how to define new rules, see the [Argus documentation on +rules](https://github.com/iuliadmtru/Argus.jl?tab=readme-ov-file#rules) -- _A rule needs to be better documented_. It is easy to do so: create a PR to this repository that improves one of the rules defined [HERE]([https://github.com/RelationalAI-oss/ReLint.jl/blob/main/src/linting/extended_checks.jl]). This `extended_checks.jl` file contains all the RAI-specific rules. -- _A new rule has to be defined_. As a system grows and evolves, new rules may have to be defined. The beginning of the file [extended_checks.jl](https://github.com/RelationalAI-oss/ReLint.jl/blob/main/src/linting/extended_checks.jl) and the section below detail this process. You can always ask `@Alexandre Bergel` on Slack for assistance. Create a new PR with the rule. +Once you have a rule, a set of rules or a rule group, you can register +them to ReLint: -## Lint rules - -Several RAI-specific and generic rules are verified on Julia source code. -A number of Julia keywords are known to be [either incompatible or dangerous when committed into raicode](https://relationalai.atlassian.net/browse/RAI-5839). \ -The Lint rules available to be run on Julia source code may be found in this [FILE](https://github.com/RelationalAI-oss/ReLint.jl/blob/main/src/linting/extended_checks.jl). - -Adding a new rule is easy. Only the file `src/linting/extended_checks.jl` has to be modified. You need to follow the steps: -1. Create a subtype of `LintRule`, e.g., `struct AsyncRule <: LintRule end`. Lint rules are dynamically looked up by looking at subtypes of `LintRule`. -2. Create an overload of `check` to perform the actual check. +```julia +help?> ReLint.register_rule! + register_rule!(rule::Rule) -Here is an example of a `check`: + Register a rule to ReLint. -```Julia -check(::AsyncRule, x::EXPR) = generic_check(x, "@async hole_variable", "Use `@spawn` instead of `@async`.") -``` +help?> ReLint.register_rules! + register_rules!(rules::Vector{Rule}) -The `generic_check` function takes as a second parameter the expression to be searched. The template string `"@async hole_variable"` means that the expression `x` will be matched against the template. The pseudo variable `hole_variable` matches everything. In case you want to match any arbitrary number of arguments, you can use `hole_variable_star` (look at the test for concrete examples). + Register a set of rules to ReLint. -If the expression `x` does match the template, then the expression is marked with the error message and used as an output. +help?> ReLint.register_rule_group! + register_rule_group!(rule_group::RuleGroup) -In case the expression must be matched in a particular context, e.g., only with a `const` expression, then you can use a `markers`, e.g., -``` -function check(::InitializingWithFunctionRule, x::EXPR, markers::Dict{Symbol,Symbol}) - # Threads.nthreads() must not be used in a const field, but it is allowed elsewhere - haskey(markers, :const) || return - generic_check(x, "Threads.nthreads()", "`Threads.nthreads()` should not be used in a constant variable.") -end -``` + Register a rule group to ReLint. -The different markers currently supported are: +help?> ReLint.register_rule_groups! + register_rule_groups!(rule_groups::Vector{RuleGroup}) -| Marker | Value | -|:------------- |:---------------| -| `:const` | Const variable name | -| `:function` | Function definition name | -| `:macro` | Macro definition name | -| `:macrocall` | Macro call name | -| `:filename` | Path and name of the analyzed file | + Register a set of rule groups to ReLint. -If you wish to run a particular rule only in a directory, you could do: +julia> ReLint.register_rule_group!(MY_GROUP) -``` -function check(::InitializingWithFunctionRule, x::EXPR, markers::Dict{Symbol,Symbol}) - isnothing(match(r".*/myfolder/.*", markers[:filename])) || return - generic_check(x, "Threads.nthreads()", "`Threads.nthreads()` should not be used in a constant variable.") -end +julia> ReLint.run_lint(f) +---------- /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_GRSpoXwPWl.jl +Line 1, column 1: `@generated` should be used with extreme caution. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_GRSpoXwPWl.jl +Line 2, column 9: An `unsafe_` function should be called only from an `unsafe_` function. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_GRSpoXwPWl.jl +Line 1, column 12: Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_GRSpoXwPWl.jl +Line 1, column 21: This is my recommendation rule -- don't use `::Any` /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_GRSpoXwPWl.jl +ReLint.LintGlobalReport(1, 1, 1, 1, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_GRSpoXwPWl.jl"], 4, ReLint.LintRuleReport[], "master") ``` -This will run the `"Threads.nthreads()"` described earlier in all folders except in `myfolder`. +### Disabling Rules -## Locally disabling ReLint +Rules can be disabled using `#lint-disable-next-line[: +]` comments. -ReLint can be locally disabled. For now, only for a given line. E.g., +```julia +julia> write(f, """ + # lint-disable-next-line + @generated function f(x)::Any + y = unsafe_f(x) + return y + end + """); -```Julia -function f1() - # The following line will not emit an error - @async 1 + 2 # lint-disable-line -end +julia> ReLint.run_lint(f) +ReLint.LintGlobalReport(1, 0, 0, 0, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl"], 0, ReLint.LintRuleReport[], "master") -function f2() - # lint-disable-next-line - @async 1 + 2 - @async 1 + 2 # This line will emit an error -end -``` +julia> write(f, """ + # lint-disable-next-line: my rule + @generated function f(x)::Any + y = unsafe_f(x) + return y + end + """); + +julia> ReLint.run_lint(f) +---------- /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl +Line 2, column 1: `@generated` should be used with extreme caution. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl +Line 3, column 9: An `unsafe_` function should be called only from an `unsafe_` function. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl +Line 2, column 12: Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl +ReLint.LintGlobalReport(1, 1, 1, 1, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl"], 3, ReLint.LintRuleReport[], "master") -A specific rule can be locally disabled using `lint-disable-next-line:` taking as an argument -the message that has to be ignored. Consider this example: +julia> write(f, """ + # lint-disable-next-line: `@generated` should be used with extreme caution + @generated function f(x)::Any + y = unsafe_f(x) + return y + end + """); -```Julia -function f() - # lint-disable-next-line: Use `@spawn` instead of `@async`. - @async 1 + 1 -end +julia> ReLint.run_lint(f) +---------- /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl +Line 3, column 9: An `unsafe_` function should be called only from an `unsafe_` function. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl +Line 2, column 12: Avoid return type annotations `function foo()::Type`. Return type annotations can hurt performance by forcing type conversions. /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl +Line 2, column 21: This is my recommendation rule -- don't use `::Any` /var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl +ReLint.LintGlobalReport(1, 1, 1, 0, ["/var/folders/4p/xtm72jnx4654xybjwm1mpd0h0000gn/T/jl_fwJaTGzSJe.jl"], 3, ReLint.LintRuleReport[], "master") ``` -The instruction `@async 1 + 1` raises the error: Use `@spawn` instead of `@async`. -Providing this error msg to the comment `lint-disable-next-line:` disabled it. +The rule disabling mechanism can be changed. See the [Argus +documentation](https://github.com/iuliadmtru/Argus.jl?tab=readme-ov-file#disabling-rules) +for more details. + +## Linting Locally -Note that it is not necessary to have the full message. The beginning of it is enough. As -such, the code above is equivalent to: +The `scripts/` directory contains a shell script for running ReLint +locally. -```Julia -function f() - # lint-disable-next-line: Use `@spawn` instead - @async 1 + 1 -end +``` +➜ ./scripts/run_locally.sh -h +./run_locally.sh [options] + +Available options: +-r|--rule - run a single rule +-rs|--rules - run a set of rules +-rg|--rule-group - run a rule group +-rgs|--rule-groups - run a set of rule groups +-h|--help - show this message ``` -## Integration with GitHub Action +## Integration With GitHub Actions and Workflows -In addition to being run locally, as described above, ReLint can be run via GitHub -Action. When a PR is created, ReLint is run on the files modified in this PR and the -result is posted as a comment. -Only one report of ReLint is posted in a PR, and it gets updated at each commit. +ReLint can be run via GitHub Actions. When a PR is created, ReLint is +run on the files modified in the PR and the result is posted as a +comment. Only one report is posted per PR, which gets updated at each +commit. + +ReLint provides three pre-commit hooks in `.pre-commit-hooks.yaml`. ## Editor Integration @@ -416,29 +483,19 @@ julia-mode: (add-hook 'julia-mode-hook 'eglot-ensure) ``` -## Listing all violations - -Currently, ReLint limits the output of the report. In total, the number of reported -violations and recommendations does not exceed 60. This limit is set by the variable -`MAX_REPORTED_ERRORS`. You may want to increase it if you wish to have the full report -from ReLint. +## Contributing -## Fork +For bug reports and improvement suggestions, feel free to file an +issue. -This repository was originally a fork of https://github.com/julia-vscode/StaticLint.jl but no code from StaticLint can be found. The decision to -fork this project instead of directly contributing to it was not taken lightly. First, the -julia-vscode/StaticLint.jl is not designed to be easily and modularly extended. As such -extending the original StaticLint with specific rules was not an easy or even feasible -task. +### Adding New Rules or Modifying Existing Rules -## Update process +If you wish to improve or extend the default set of rules, you can do +so by modifying the relevant file(s) in the `rules/` directory. Please +read the [relevant ReLint](#defining-new-rules) and +[Argus](https://github.com/iuliadmtru/Argus.jl?tab=readme-ov-file#rules) +documentation before. -Here is a helper for two common processes when updating Lint rules: +### Improving the Interface - - Adding a new non-fatal rule to ReLint: - - if the rule should only appear in the PR comment, then simply add the rule to ReLint.jl. No need to update the GitHub repo client. - - Make a rule block a PR using pre-commit: - - Create a rule subtype of `FatalLintRule`. Merge the PR containing this rule in `main` of ReLint.jl - - If the rule can be run with other (fatal lint) rules, then you should modify the hook `lint-fatal-checks` in the file `.pre-commit-hooks.yaml`, in ReLint.jl - - Create a new tag of the corresponding ReLint.jl's commit and update `.pre-commit-config.yaml` with this new tag in the client. - - _If the rule should be run in a pre-commit job_ (in parallel with other pre-commit jobs), then you need to add a hook in the file `.pre-commit-hooks.yaml` in ReLint.jl. You will then need to call this hook in the file `.pre-commit-config.yaml` in the client +All interface-related code can be found in `src/interface.jl`. From 4215f554dcadb90e062819d90f3efa4401f4945d Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Sat, 30 May 2026 17:43:49 +0200 Subject: [PATCH 54/62] Compatibility with julia v1.10 and v1.11 --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 587cdf1..965cc96 100644 --- a/Project.toml +++ b/Project.toml @@ -9,8 +9,8 @@ JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" [compat] -Argus = "0.1.0" -Dates = "1.11.0" +Argus = "0.3.0" +Dates = "1.10" JuliaSyntax = "1.0.2" julia = "1" From 2cbd8ed83c8729d166d48e7ff02aa0e8bb95e065 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Mon, 1 Jun 2026 15:31:40 +0200 Subject: [PATCH 55/62] Removing running tests from Julia 1.9 --- .github/workflows/run_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 5eb5471..52422a1 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - julia-version: ['1.9', '1.10', '1.11', '1.12'] + julia-version: ['1.10', '1.11', '1.12'] julia-arch: [x64, x86] os: [ubuntu-latest, macOS-latest] exclude: From 647fee19256e35d81022860dacef70829dce93e1 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Tue, 2 Jun 2026 18:02:28 +0200 Subject: [PATCH 56/62] disable coverage --- .github/workflows/run_tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 52422a1..296fc41 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -36,5 +36,5 @@ jobs: - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - # with: - # annotate: true \ No newline at end of file + with: + coverage: false \ No newline at end of file From 541171b846e4f4bcb5b06e7dcce0f599e68f8f62 Mon Sep 17 00:00:00 2001 From: Alexandre Bergel Date: Tue, 2 Jun 2026 18:07:55 +0200 Subject: [PATCH 57/62] Still with the tests --- .github/workflows/run_tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 296fc41..e4b7c17 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -26,6 +26,8 @@ jobs: exclude: - os: macOS-latest julia-arch: x86 + - os: ubuntu-latest + julia-arch: x86 steps: - uses: actions/checkout@v6 From 32cf4c761e50295a7d44f84e8b017d80dd3773c9 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 2 Jun 2026 22:17:30 +0200 Subject: [PATCH 58/62] Update to Argus 0.4.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 965cc96..ee5d9b2 100644 --- a/Project.toml +++ b/Project.toml @@ -9,7 +9,7 @@ JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" [compat] -Argus = "0.3.0" +Argus = "0.4.0" Dates = "1.10" JuliaSyntax = "1.0.2" julia = "1" From 63783a8533875e0ad3c3c99cfa95e91b02a3b477 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Tue, 2 Jun 2026 22:21:38 +0200 Subject: [PATCH 59/62] Add back ubuntu x86 tests --- .github/workflows/run_tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index e4b7c17..296fc41 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -26,8 +26,6 @@ jobs: exclude: - os: macOS-latest julia-arch: x86 - - os: ubuntu-latest - julia-arch: x86 steps: - uses: actions/checkout@v6 From 7072db042033a7ca9afa899a5d67cb70e44dbb98 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 4 Jun 2026 19:35:19 +0200 Subject: [PATCH 60/62] Argus v0.4.1 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ee5d9b2..85e7d5e 100644 --- a/Project.toml +++ b/Project.toml @@ -9,7 +9,7 @@ JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" [compat] -Argus = "0.4.0" +Argus = "0.4.1" Dates = "1.10" JuliaSyntax = "1.0.2" julia = "1" From a07cdede6c5e42ed13f654f500740aacebda3854 Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 4 Jun 2026 19:39:40 +0200 Subject: [PATCH 61/62] Add back coverage --- .github/workflows/run_tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 296fc41..89790cf 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -35,6 +35,4 @@ jobs: arch: ${{ matrix.julia-arch }} - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - with: - coverage: false \ No newline at end of file + - uses: julia-actions/julia-runtest@v1 \ No newline at end of file From e5ef030bec7557f6276d52dc9f0c36c92002988e Mon Sep 17 00:00:00 2001 From: Iulia Dumitru Date: Thu, 4 Jun 2026 19:43:38 +0200 Subject: [PATCH 62/62] Revert "Add back coverage" This reverts commit 89128e179bbc5f3aaaf3db798f47e898b5ee69d7. --- .github/workflows/run_tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 89790cf..296fc41 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -35,4 +35,6 @@ jobs: arch: ${{ matrix.julia-arch }} - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 \ No newline at end of file + - uses: julia-actions/julia-runtest@v1 + with: + coverage: false \ No newline at end of file