├── .github └── workflows │ └── action.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── example.nim ├── example2.nim ├── test.nim └── test.nims ├── nimlint.nimble ├── src ├── nimlint.nim ├── nimlint.nims └── nimlintpkg │ ├── nims_common.nim │ └── utils.nim └── tests ├── config.nims ├── core ├── example.nim └── tnimlint.nim └── tall.nim /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Test nim-lint 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | test: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu-latest 13 | - windows-latest 14 | - macOS-latest 15 | nim-version: 16 | # - stable 17 | - devel 18 | exclude: 19 | - os: macos-latest 20 | nim-version: devel 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | 25 | - name: Cache choosenim 26 | id: cache-choosenim 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.choosenim 30 | key: ${{ runner.os }}-choosenim-${{ matrix.nim-version}} 31 | 32 | - name: Cache nimble 33 | id: cache-nimble 34 | uses: actions/cache@v2 35 | with: 36 | path: ~/.nimble 37 | key: ${{ runner.os }}-nimble-${{ matrix.nim-version}}-${{ hashFiles('prologue.nimble') }} 38 | restore-keys: | 39 | ${{ runner.os }}-nimble-${{ matrix.nim-version}}- 40 | - name: Setup nim 41 | uses: jiro4989/setup-nim-action@v1 42 | with: 43 | nim-version: ${{ matrix.nim-version }} 44 | 45 | - name: Install Packages 46 | run: nimble install -y 47 | 48 | - name: Test 49 | run: nimble tests 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | build/* 3 | 4 | nimcache 5 | # would be nice to make sure those are generated in build/ or bin/ (or nimcache automatically with `nim r `) 6 | # which makes it easier to gitignore or move in bulk 7 | outputGotten.txt 8 | outputExpected.txt 9 | megatest.nim 10 | core.json 11 | *.exe 12 | out.nim 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 nim-compiler-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lint 2 | nimlint makes developing softer. 3 | 4 | Ref https://github.com/timotheecour/Nim/issues/415 5 | 6 | ## lint items 7 | 8 | - [x] code block => runnableExamples 9 | - [x] proc + noSideEffect => func 10 | - [x] assert in a test file => doAssert 11 | - [x] isMainModule in stdlib => recommend moving to tests/stdlib/tfoo.nim 12 | - [x] double backticks => single backticks 13 | - [x] the first char should be upper 14 | 15 | ## TODO 16 | 17 | - [x] Better messages with filename, line and col number 18 | - [x] Github APP integration :arrow_right: https://github.com/juancarlospaco/nimlint-action 19 | - [ ] Fix them 20 | - [ ] Syntax to ignore lint recommendations, analog to #!nimpretty off 21 | (maybe via a pragma or special #!nimlint:off syntax) 22 | -------------------------------------------------------------------------------- /examples/example.nim: -------------------------------------------------------------------------------- 1 | for i in 1 .. 10: 2 | echo i 3 | 4 | let x = "1234567898765432187652367489668754326789348565784932873456" 5 | 6 | ## 1134567890 7 | proc hello(a: int) {.nosideeffect.} = 8 | ## ``Double quote`` should be removed. 9 | ## Test 10 | ## .. code-block:: 11 | ## echo 23 12 | runnableExamples: 13 | ## Hello Kitty 14 | assert 12 == 12 15 | debugecho "Hello, World" 16 | assert 12 == 12 17 | when isMainModule: 18 | assert 1 == 1 19 | 20 | ## .. code-block::nim 21 | ## echo 23 22 | ## test ``double ticks`` 23 | ## 24 | 25 | func rest = discard 26 | 27 | when isMainModule: 28 | hello(12) 29 | -------------------------------------------------------------------------------- /examples/example2.nim: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # Nim's Runtime Library 4 | # (c) Copyright 2012 Andreas Rumpf 5 | # 6 | # See the file "copying.txt", included in this 7 | # distribution, for details about the copyright. 8 | # 9 | 10 | ## This module implements a simple proc for opening URLs with the user's 11 | ## default browser. 12 | ## 13 | ## Unstable API. 14 | 15 | import std/private/since 16 | 17 | import strutils 18 | 19 | when defined(windows): 20 | import winlean 21 | from os import absolutePath 22 | else: 23 | import os, osproc 24 | 25 | const osOpenCmd* = 26 | when defined(macos) or defined(macosx) or defined(windows): "open" else: "xdg-open" ## \ 27 | ## Alias for the operating system specific *"open"* command, 28 | ## ``"open"`` on OSX, MacOS and Windows, ``"xdg-open"`` on Linux, BSD, etc. 29 | 30 | proc prepare(s: string): string = 31 | if s.contains("://"): 32 | result = s 33 | else: 34 | result = "file://" & absolutePath(s) 35 | 36 | proc openDefaultBrowserImpl(url: string) = 37 | when defined(windows): 38 | var o = newWideCString(osOpenCmd) 39 | var u = newWideCString(prepare url) 40 | discard shellExecuteW(0'i32, o, u, nil, nil, SW_SHOWNORMAL) 41 | elif defined(macosx): 42 | discard execShellCmd(osOpenCmd & " " & quoteShell(prepare url)) 43 | else: 44 | var u = quoteShell(prepare url) 45 | if execShellCmd(osOpenCmd & " " & u) == 0: return 46 | for b in getEnv("BROWSER").string.split(PathSep): 47 | try: 48 | # we use ``startProcess`` here because we don't want to block! 49 | discard startProcess(command = b, args = [url], options = {poUsePath}) 50 | return 51 | except OSError: 52 | discard 53 | 54 | proc openDefaultBrowser*(url: string) = 55 | ## Opens `url` with the user's default browser. This does not block. 56 | ## The URL must not be empty string, to open on a blank page see `openDefaultBrowser()`. 57 | ## 58 | ## Under Windows, ``ShellExecute`` is used. Under Mac OS X the ``open`` 59 | ## command is used. Under Unix, it is checked if ``xdg-open`` exists and 60 | ## used if it does. Otherwise the environment variable ``BROWSER`` is 61 | ## used to determine the default browser to use. 62 | ## 63 | ## This proc doesn't raise an exception on error, beware. 64 | ## 65 | ## .. code-block:: nim 66 | ## block: openDefaultBrowser("https://nim-lang.org") 67 | doAssert url.len > 0, "URL must not be empty string" 68 | openDefaultBrowserImpl(url) 69 | 70 | proc openDefaultBrowser*() {.since: (1, 1).} = 71 | ## Opens the user's default browser without any `url` (blank page). This does not block. 72 | ## Implements IETF RFC-6694 Section 3, "about:blank" must be reserved for a blank page. 73 | ## 74 | ## Under Windows, ``ShellExecute`` is used. Under Mac OS X the ``open`` 75 | ## command is used. Under Unix, it is checked if ``xdg-open`` exists and 76 | ## used if it does. Otherwise the environment variable ``BROWSER`` is 77 | ## used to determine the default browser to use. 78 | ## 79 | ## This proc doesn't raise an exception on error, beware. 80 | ## 81 | ## **See also:** 82 | ## 83 | ## * https://tools.ietf.org/html/rfc6694#section-3 84 | ## 85 | ## .. code-block:: nim 86 | ## block: openDefaultBrowser() 87 | openDefaultBrowserImpl("http:about:blank") # See IETF RFC-6694 Section 3. 88 | -------------------------------------------------------------------------------- /examples/test.nim: -------------------------------------------------------------------------------- 1 | import src/nimlint 2 | 3 | 4 | main("examples/example2.nim", "out.nim", verbose = false) 5 | -------------------------------------------------------------------------------- /examples/test.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/..") 2 | switch("hints", "off") -------------------------------------------------------------------------------- /nimlint.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "xflywind + contributors" 5 | description = "Nim lint tool" 6 | license = "MIT" 7 | srcDir = "src" 8 | installExt = @["nim"] 9 | binDir = "bin" 10 | bin = @["nimlint"] 11 | 12 | 13 | # Dependencies 14 | 15 | requires "nim >= 1.5.1" 16 | requires "cligen >= 1.3.2" 17 | 18 | task tests, "Test all": 19 | # if this fails, use: `nim r tests/core/tnimlint.nim` 20 | # TODO: use getAppFileName() or getCurrentCompilerExe() to forward nim 21 | # exec "testament all" 22 | exec "nim r tests/core/tnimlint.nim" 23 | 24 | # TODO 25 | # requires "compiler" 26 | -------------------------------------------------------------------------------- /src/nimlint.nim: -------------------------------------------------------------------------------- 1 | #[ 2 | TODO: not portable, needs instead: `requires "compiler"` 3 | ]# 4 | import ../compiler/[ast, idents, msgs, syntaxes, options, pathutils] 5 | import std/[os, strformat, strutils, parseutils, sets] 6 | import std/private/miscdollars # avoids code duplication 7 | 8 | 9 | type 10 | HintStateKind* = enum 11 | # doc comments 12 | hintBackticks 13 | hintCodeBlocks 14 | hintFirstChar 15 | # functions 16 | hintFunc 17 | hintIsMainModule 18 | # testament 19 | hintExitcode 20 | hintAssert 21 | 22 | HintState* = object 23 | kind: HintStateKind 24 | info: tuple[file: string, line, col: int] 25 | 26 | const 27 | hintStateKindTable = ["double backticks => single backtick", 28 | "code blocks => runnableExamples", 29 | "the first char should be upper", 30 | 31 | "proc + noSideEffect => func", 32 | "isMainModule in stdlib => moving to tests/*/*.nim", 33 | 34 | "exitcode: 0 is usually useless", 35 | "assert in tests => doAssert" 36 | ] 37 | 38 | static: doAssert hintStateKindTable.high == HintStateKind.high.ord 39 | 40 | proc initHintState(kind: HintStateKind, file: string, line, col: int): HintState = 41 | HintState(kind: kind, info: (file, line, col)) 42 | 43 | template add( 44 | tabs: var seq[HintState], kind: HintStateKind, 45 | conf: ConfigRef, file: string, line, col: int 46 | ) = 47 | tabs.add initHintState(kind, file, line, col) 48 | 49 | proc add(tabs: var seq[HintState], kind: HintStateKind, conf: ConfigRef, n: PNode) = 50 | let info = n.info 51 | let file = conf.toFullPath(info.fileIndex) 52 | tabs.add(kind, conf, file, info.line.int, info.col.int) 53 | 54 | const 55 | SpecialChars = {'\n', '`'} 56 | testsPath = "tests" 57 | 58 | proc cleanWhenModule(conf: ConfigRef, n: PNode, hintTable: var seq[HintState]) = 59 | if n.len > 0 and n[0].kind == nkElifBranch: 60 | let son = n[0] 61 | if son[0].kind == nkIdent and cmpIgnoreStyle(son[0].ident.s, "isMainModule") == 0: 62 | hintTable.add(hintIsMainModule, conf, n) 63 | 64 | proc cleanCodeBlocks( 65 | comments: string, start: var int, line: int, conf: ConfigRef, n: PNode, hintTable: var seq[HintState] 66 | ) = 67 | const cb = ".. code-block::" 68 | if start + cb.high < comments.len: 69 | # if comments[].startsWith(cb): 70 | if comments.substr(start, start + cb.high) == cb: 71 | let file = conf.toFullPath(n.info.fileIndex) 72 | hintTable.add(hintCodeBlocks, conf, file, line, 0) 73 | inc(start, cb.high) 74 | 75 | proc cleanComment(conf: ConfigRef, n: PNode, hintTable: var seq[HintState]) = 76 | let comments = n.comment 77 | 78 | if comments.len > 0: 79 | var start = 0 80 | var line = n.info.line.int 81 | var ticks = initHashSet[int]() 82 | 83 | if isLowerAscii(comments[0]): 84 | hintTable.add(hintFirstChar, conf, n) 85 | 86 | cleanCodeBlocks(comments, start, line, conf, n, hintTable) 87 | 88 | while start < comments.len: 89 | let incr = skipUntil(comments, SpecialChars, start) 90 | inc(start, incr) 91 | if start < comments.len: 92 | case comments[start] 93 | of '\n': 94 | inc start 95 | inc line 96 | # TODO optimization: more intelligent 97 | cleanCodeBlocks(comments, start, line, conf, n, hintTable) 98 | of '`': 99 | if start + 1 < comments.len and comments[start+1] == '`': 100 | if line notin ticks: 101 | hintTable.add(hintBackticks, conf, conf.toFullPath(n.info.fileIndex), line, 0) 102 | ticks.incl(line) 103 | inc start 104 | inc start 105 | else: 106 | inc start 107 | 108 | 109 | proc clean(conf: ConfigRef, n: PNode, hintTable: var seq[HintState], infile: string) = 110 | case n.kind 111 | of nkImportStmt, nkExportStmt, nkCharLit..nkUInt64Lit, 112 | nkFloatLit..nkFloat128Lit, nkStrLit..nkTripleStrLit: 113 | discard 114 | of nkWhenStmt: 115 | # Handles the most common case 116 | # { 117 | # "kind": "nkWhenStmt", 118 | # "typ": "nil", 119 | # "sons": [{ 120 | # "kind": "nkElifBranch", 121 | # "typ": "nil", 122 | # "sons": [{ 123 | # "kind": "nkIdent", 124 | # "typ": "nil", 125 | # "ident": "isMainModule" 126 | # }, { 127 | # "kind": "nkStmtList", 128 | # "typ": "nil" 129 | cleanWhenModule(conf, n, hintTable) 130 | of nkSym: 131 | discard 132 | of nkProcDef: 133 | for son in n[pragmasPos]: 134 | if son.kind == nkIdent: 135 | if cmpIgnoreStyle(son.ident.s, "noSideEffect") == 0: 136 | hintTable.add(hintFunc, conf, n) 137 | break 138 | clean(conf, n[bodyPos], hintTable, infile) 139 | of nkFuncDef: 140 | discard 141 | of nkIdent: 142 | var path = infile.expandFileName 143 | if path.find(testsPath) >= 0: 144 | if cmpIgnoreStyle(n.ident.s, "assert") == 0: 145 | hinttable.add(hintAssert, conf, n) 146 | of nkCommentStmt: 147 | cleanComment(conf, n, hintTable) 148 | else: 149 | for s in n.sons: 150 | clean(conf, s, hintTable, infile) 151 | 152 | proc prettyPrint*(infile, outfile: string, hintTable: var seq[HintState]) = 153 | # TODO: is outfile written to? 154 | # outfile needs nimpretty 155 | var conf = newConfigRef() 156 | let fileIdx = fileInfoIdx(conf, AbsoluteFile infile) 157 | let f = splitFile(outfile.expandTilde) 158 | conf.outFile = RelativeFile f.name & f.ext 159 | conf.outDir = toAbsoluteDir f.dir 160 | var parser: Parser 161 | var cache = newIdentCache() 162 | 163 | conf.options.excl(optHints) 164 | 165 | if setupParser(parser, fileIdx, cache, conf): 166 | var ast = parseFile(conf.projectMainIdx, cache, conf) 167 | clean(conf, ast, hintTable, infile) 168 | 169 | proc toString(a: HintState, verbose: bool): string = 170 | result = fmt"[lint] {a.kind}: " 171 | let loc = a.info 172 | result.toLocation(loc.file, loc.line, loc.col) 173 | 174 | if verbose: 175 | result.add "\n" & hintStateKindTable[a.kind.ord] & "\n" 176 | 177 | proc `$`*(a: HintState): string = 178 | toString(a, false) 179 | 180 | proc main*(input, output: string, verbose = true) = 181 | var hintTable: seq[HintState] 182 | prettyPrint(input, output, hintTable) 183 | 184 | for item in hintTable: 185 | echo toString(item, verbose) 186 | 187 | when isMainModule: 188 | import cligen 189 | dispatch main 190 | -------------------------------------------------------------------------------- /src/nimlint.nims: -------------------------------------------------------------------------------- 1 | import nimlintpkg/nims_common 2 | -------------------------------------------------------------------------------- /src/nimlintpkg/nims_common.nim: -------------------------------------------------------------------------------- 1 | # switch("define", "nimpretty") # maybe there's a better way; this is needed to enable some parts in `compiler/` 2 | switch("hints", "off") -------------------------------------------------------------------------------- /src/nimlintpkg/utils.nim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-dev/nimlint/a98b461c824bbfdbd9e804873f4e5d9422226277/src/nimlintpkg/utils.nim -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") 2 | import ../src/nimlintpkg/nims_common 3 | -------------------------------------------------------------------------------- /tests/core/example.nim: -------------------------------------------------------------------------------- 1 | for i in 1 .. 10: 2 | echo i 3 | 4 | let x = "1234567898765432187652367489668754326789348565784932873456" 5 | 6 | ## 1134567890 7 | proc hello(a: int) {.noSideEffect.} = 8 | ## ``Double quote`` should be removed. 9 | ## Test 10 | runnableExamples: 11 | ## lowerascii 12 | assert 12 == 12 13 | debugecho "Hello, World" 14 | assert 12 == 12 15 | when isMainModule: 16 | debugecho 8888 17 | 18 | 19 | when isMainModule: 20 | hello(12) 21 | -------------------------------------------------------------------------------- /tests/core/tnimlint.nim: -------------------------------------------------------------------------------- 1 | import std/[os] 2 | import ../../src/nimlint 3 | 4 | proc main = 5 | const fileInput = currentSourcePath.parentDir / "example.nim" 6 | const buildDir = currentSourcePath.parentDir.parentDir.parentDir / "build" 7 | createDir buildDir 8 | const fileOutput = buildDir / fileInput.lastPathPart 9 | echo fileOutput 10 | removeFile fileOutput 11 | main(fileInput, fileOutput) 12 | # doAssert fileOutput.fileExists, fileOutput 13 | # TODO: check file content matches some groundtruth 14 | 15 | main() 16 | -------------------------------------------------------------------------------- /tests/tall.nim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-dev/nimlint/a98b461c824bbfdbd9e804873f4e5d9422226277/tests/tall.nim --------------------------------------------------------------------------------