Skip to content

Commit 1531473

Browse files
CopilotFreed-Wu
andcommitted
Add EditorConfig support for shellcheck directives
Fix EditorConfig section priority, glob depth matching, and root=true search stop Co-authored-by: Freed-Wu <32936898+Freed-Wu@users.noreply.github.com>
1 parent 9af7ee2 commit 1531473

6 files changed

Lines changed: 401 additions & 4 deletions

File tree

ShellCheck.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ library
8484
ShellCheck.Checks.Custom
8585
ShellCheck.Checks.ShellSupport
8686
ShellCheck.Data
87+
ShellCheck.EditorConfig
8788
ShellCheck.Fixer
8889
ShellCheck.Formatter.Format
8990
ShellCheck.Formatter.CheckStyle

shellcheck.1.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,31 @@ Use `shellcheckrc` without the dot instead.
334334
Note for Docker users: ShellCheck will only be able to look for files that
335335
are mounted in the container, so `~/.shellcheckrc` will not be read.
336336

337+
# EDITORCONFIG
338+
339+
Unless `--norc` is used, ShellCheck will also look for a file `.editorconfig`
340+
in the script's directory and each parent directory. Any section whose glob
341+
pattern matches the checked file will have its `shellcheck.*` keys read as
342+
directives, with the `shellcheck.` prefix stripped. This uses the same
343+
`key=value` syntax as `.shellcheckrc`.
344+
345+
For example:
346+
347+
[*.{ebuild,eclass}]
348+
shellcheck.shell=bash
349+
shellcheck.disable=SC2034
350+
351+
[{PKGBUILD,APKBUILD}]
352+
shellcheck.shell=bash
353+
shellcheck.disable=SC2034
354+
355+
If no matching directives are found in any `.editorconfig` in the parent
356+
directories, ShellCheck will look in the global default
357+
`$XDG_CONFIG_HOME/editorconfig.ini` (usually `~/.config/editorconfig.ini`).
358+
359+
Directives from `.shellcheckrc`/`shellcheckrc` and from `.editorconfig` are
360+
both applied, with `.shellcheckrc` taking precedence in case of conflicts.
361+
337362

338363
# ENVIRONMENT VARIABLES
339364

shellcheck.hs

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import qualified ShellCheck.Analyzer
2121
import ShellCheck.Checker
2222
import ShellCheck.Data
23+
import ShellCheck.EditorConfig
2324
import ShellCheck.Interface
2425
import ShellCheck.Regex
2526

@@ -110,7 +111,7 @@ options = [
110111
Option "" ["list-optional"]
111112
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
112113
Option "" ["norc"]
113-
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
114+
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc and .editorconfig files",
114115
Option "" ["rcfile"]
115116
(ReqArg (Flag "rcfile") "RCFILE")
116117
"Prefer the specified configuration file over searching for one",
@@ -514,8 +515,22 @@ ioInterface options files = do
514515
fallback path _ = return path
515516

516517

517-
-- Returns the name and contents of .shellcheckrc for the given file
518-
getConfig cache filename =
518+
-- Returns the name and contents of .shellcheckrc for the given file,
519+
-- merged with any shellcheck.* directives found in applicable
520+
-- EditorConfig files.
521+
getConfig cache filename = do
522+
rcResult <- getRcConfig cache filename
523+
ecResult <- getEditorConfig filename
524+
return $ mergeConfigs filename rcResult ecResult
525+
526+
mergeConfigs filename rcResult ecResult =
527+
case (rcResult, ecResult) of
528+
(Nothing, Nothing) -> Nothing
529+
(Just (rcPath, rc), Nothing) -> Just (rcPath, rc)
530+
(Nothing, Just (ecPath, ec)) -> Just (ecPath, ec)
531+
(Just (rcPath, rc), Just (_, ec)) -> Just (rcPath, rc ++ "\n" ++ ec)
532+
533+
getRcConfig cache filename =
519534
case rcfile options of
520535
Just file -> do
521536
-- We have a specified rcfile. Ignore normal rcfile resolution.
@@ -541,6 +556,63 @@ ioInterface options files = do
541556
writeIORef cache (dir, result)
542557
return result
543558

559+
-- Look for .editorconfig files in the target file's directory and
560+
-- all its parents (as per the EditorConfig spec), plus the global
561+
-- ${XDG_CONFIG_HOME}/editorconfig.ini default. shellcheck.* keys in
562+
-- matching sections are turned into directives.
563+
getEditorConfig filename = do
564+
-- Resolve the directory (to find .editorconfig files) but keep
565+
-- the leaf filename as-is so that globs match the symlink name
566+
-- rather than the resolved target.
567+
let name = takeFileName filename
568+
dir <- normalize (takeDirectory filename)
569+
let path = dir </> name
570+
dirConfigs <- collectDirConfigs dir
571+
globalConfig <- readGlobalEditorConfig
572+
let allConfigs = dirConfigs ++ globalConfig
573+
contributions = concatMap (directivesFor path) allConfigs
574+
return $ case contributions of
575+
[] -> Nothing
576+
((sourceFile, _):_) -> Just (sourceFile, concatMap snd contributions)
577+
where
578+
-- For each EditorConfig file: either report an invalid 'root'
579+
-- declaration at its line, or yield the matching shellcheck.*
580+
-- directives. Invalid roots take priority over directives.
581+
directivesFor path (file, contents) =
582+
case invalidRootLines contents of
583+
(badLine:_) ->
584+
[(file, replicate (badLine - 1) '\n' ++ "invalid editorconfig value\n")]
585+
[] ->
586+
let relative = makeRelativeTo (takeDirectory file) path
587+
result = parseEditorConfig contents relative
588+
in if null result then [] else [(file, result)]
589+
590+
makeRelativeTo dir path =
591+
case stripPrefix (addTrailingSlash dir) path of
592+
Just rest -> rest
593+
Nothing -> takeFileName path
594+
595+
addTrailingSlash dir
596+
| null dir = dir
597+
| last dir == '/' = dir
598+
| otherwise = dir ++ "/"
599+
600+
collectDirConfigs dir = do
601+
current <- readConfig (dir </> ".editorconfig")
602+
let isRoot = maybe False (isEditorConfigRoot . snd) current
603+
next = takeDirectory dir
604+
rest <- if next /= dir && not isRoot
605+
then collectDirConfigs next
606+
else return []
607+
return $ maybeToList current ++ rest
608+
609+
readGlobalEditorConfig = do
610+
path <- (getXdgDirectory XdgConfig "editorconfig.ini")
611+
`catch` ((const $ return "") :: IOException -> IO FilePath)
612+
if null path
613+
then return []
614+
else maybeToList <$> readConfig path
615+
544616
findConfig paths =
545617
case paths of
546618
(file:rest) -> do

0 commit comments

Comments
 (0)