Skip to content

Instantly share code, notes, and snippets.

@mattn
Created March 31, 2026 06:39
Show Gist options
  • Select an option

  • Save mattn/06020b32c9e5abff01804e2640cd8c3e to your computer and use it in GitHub Desktop.

Select an option

Save mattn/06020b32c9e5abff01804e2640cd8c3e to your computer and use it in GitHub Desktop.
name: Check option modeline safety
on:
push:
branches: ['**']
paths:
- 'src/optiondefs.h'
- 'src/option.h'
pull_request:
paths:
- 'src/optiondefs.h'
- 'src/option.h'
permissions:
contents: read
jobs:
check-options:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check options with side effects have modeline protection
run: python3 .github/scripts/check_option_security.py
#!/usr/bin/env python3
"""
Check that new Vim options are reviewed for modeline safety.
Every option in optiondefs.h must be in one of two lists:
- PROTECTED: has P_SECURE, P_NO_ML, or P_MLE (side effects, unsafe in modeline)
- KNOWN_SAFE: reviewed and confirmed safe without modeline protection
A new option not in either list causes CI to fail, forcing the developer to
decide whether it needs P_SECURE/P_NO_ML/P_MLE or is safe without it.
Additionally, options listed in PROTECTED are verified to actually have the
corresponding flag in optiondefs.h (catches accidental flag removal).
"""
import re
import sys
OPTIONDEFS_PATH = "src/optiondefs.h"
MODELINE_PROTECTION_FLAGS = {"P_SECURE", "P_NO_ML", "P_MLE"}
# Options that have modeline protection flags (P_SECURE, P_NO_ML, or P_MLE).
# Removing an option from this list or removing its flag requires review.
PROTECTED = {
"backupdir",
"balloonexpr",
"cdhome",
"cdpath",
"charconvert",
"completefunc",
"cscopeprg",
"diffexpr",
"directory",
"encoding",
"equalprg",
"errorfile",
"exrc",
"findfunc",
"foldexpr",
"foldtext",
"formatexpr",
"formatprg",
"fsync",
"grepprg",
"guitablabel",
"helpfile",
"iconstring",
"imactivatefunc",
"imstatusfunc",
"imstyle",
"includeexpr",
"indentexpr",
"keywordprg",
"langmap",
"luadll",
"makeef",
"makeprg",
"mkspellmem",
"modelineexpr",
"mzschemedll",
"mzschemegcdll",
"omnifunc",
"operatorfunc",
"packpath",
"patchexpr",
"perldll",
"printdevice",
"printexpr",
"pumborder",
"pythondll",
"pythonhome",
"pythonthreedll",
"pythonthreehome",
"pyxversion",
"quickfixtextfunc",
"rubydll",
"rulerformat",
"runtimepath",
"secure",
"shell",
"shellcmdflag",
"shellpipe",
"shellquote",
"shellredir",
"shellxescape",
"shellxquote",
"spellfile",
"spellsuggest",
"statusline",
"statuslineopt",
"tabline",
"tabpanel",
"tagfunc",
"tcldll",
"thesaurusfunc",
"titleold",
"titlestring",
"undodir",
"verbosefile",
"viewdir",
"viminfo",
"viminfofile",
"winptydll",
}
# Options reviewed and confirmed safe without modeline protection.
KNOWN_SAFE = {
"aleph",
"allowrevins",
"altkeymap",
"ambiwidth",
"antialias",
"arabic",
"arabicshape",
"autochdir",
"autocomplete",
"autocompletedelay",
"autocompletetimeout",
"autoindent",
"autoprint",
"autoread",
"autoshelldir",
"autowrite",
"autowriteall",
"background",
"backspace",
"backup",
"backupcopy",
"backupext",
"backupskip",
"balloondelay",
"ballooneval",
"balloonevalterm",
"beautify",
"belloff",
"binary",
"bioskey",
"bomb",
"breakat",
"breakindent",
"breakindentopt",
"browsedir",
"bufhidden",
"buflisted",
"buftype",
"casemap",
"cedit",
"chistory",
"cindent",
"cinkeys",
"cinoptions",
"cinscopedecls",
"cinwords",
"clipboard",
"clipmethod",
"cmdheight",
"cmdwinheight",
"colorcolumn",
"columns",
"comments",
"commentstring",
"compatible",
"complete",
"completefuzzycollect",
"completeitemalign",
"completeopt",
"completepopup",
"completeslash",
"completetimeout",
"concealcursor",
"conceallevel",
"confirm",
"conskey",
"copyindent",
"cpoptions",
"cryptmethod",
"cscopepathcomp",
"cscopequickfix",
"cscoperelative",
"cscopetag",
"cscopetagorder",
"cscopeverbose",
"cursorbind",
"cursorcolumn",
"cursorline",
"cursorlineopt",
"debug",
"define",
"delcombine",
"dictionary",
"diff",
"diffanchors",
"diffopt",
"digraph",
"display",
"eadirection",
"edcompatible",
"emoji",
"endoffile",
"endofline",
"equalalways",
"errorbells",
"errorformat",
"esckeys",
"eventignore",
"eventignorewin",
"expandtab",
"fileencoding",
"fileencodings",
"fileformat",
"fileformats",
"fileignorecase",
"filetype",
"fillchars",
"fixendofline",
"fkmap",
"flash",
"foldclose",
"foldcolumn",
"foldenable",
"foldignore",
"foldlevel",
"foldlevelstart",
"foldmarker",
"foldmethod",
"foldminlines",
"foldnestmax",
"foldopen",
"formatlistpat",
"formatoptions",
"gdefault",
"graphic",
"grepformat",
"guicursor",
"guifont",
"guifontset",
"guifontwide",
"guiheadroom",
"guiligatures",
"guioptions",
"guipty",
"guitabtooltip",
"hardtabs",
"helpheight",
"helplang",
"hidden",
"highlight",
"history",
"hkmap",
"hkmapp",
"hlsearch",
"icon",
"ignorecase",
"imactivatekey",
"imcmdline",
"imdisable",
"iminsert",
"imsearch",
"include",
"incsearch",
"indentkeys",
"infercase",
"insertmode",
"isfname",
"isident",
"iskeyword",
"isprint",
"joinspaces",
"jumpoptions",
"key",
"keymap",
"keymodel",
"keyprotocol",
"langmenu",
"langnoremap",
"langremap",
"laststatus",
"lazyredraw",
"lhistory",
"linebreak",
"lines",
"linespace",
"lisp",
"lispoptions",
"lispwords",
"list",
"listchars",
"loadplugins",
"macatsui",
"magic",
"makeencoding",
"matchpairs",
"matchtime",
"maxcombine",
"maxfuncdepth",
"maxmapdepth",
"maxmem",
"maxmempattern",
"maxmemtot",
"maxsearchcount",
"menuitems",
"mesg",
"messagesopt",
"modeline",
"modelines",
"modifiable",
"modified",
"more",
"mouse",
"mousefocus",
"mousehide",
"mousemodel",
"mousemoveevent",
"mouseshape",
"mousetime",
"mzquantum",
"novice",
"nrformats",
"number",
"numberwidth",
"open",
"opendevice",
"optimize",
"osctimeoutlen",
"osfiletype",
"paragraphs",
"paste",
"pastetoggle",
"patchmode",
"path",
"preserveindent",
"previewheight",
"previewpopup",
"previewwindow",
"printencoding",
"printfont",
"printheader",
"printmbcharset",
"printmbfont",
"printoptions",
"prompt",
"pumheight",
"pummaxwidth",
"pumwidth",
"quoteescape",
"readonly",
"redraw",
"redrawtime",
"regexpengine",
"relativenumber",
"remap",
"renderoptions",
"report",
"restorescreen",
"revins",
"rightleft",
"rightleftcmd",
"ruler",
"scroll",
"scrollbind",
"scrollfocus",
"scrolljump",
"scrolloff",
"scrollopt",
"sections",
"selection",
"selectmode",
"sessionoptions",
"shellslash",
"shelltemp",
"shelltype",
"shiftround",
"shiftwidth",
"shortmess",
"shortname",
"showbreak",
"showcmd",
"showcmdloc",
"showfulltag",
"showmatch",
"showmode",
"showtabline",
"showtabpanel",
"sidescroll",
"sidescrolloff",
"signcolumn",
"slowopen",
"smartcase",
"smartindent",
"smarttab",
"smoothscroll",
"softtabstop",
"sourceany",
"spell",
"spellcapcheck",
"spelllang",
"spelloptions",
"splitbelow",
"splitkeep",
"splitright",
"startofline",
"suffixes",
"suffixesadd",
"swapfile",
"swapsync",
"switchbuf",
"synmaxcol",
"syntax",
"tabclose",
"tabpagemax",
"tabpanelopt",
"tabstop",
"tagbsearch",
"tagcase",
"taglength",
"tagrelative",
"tags",
"tagstack",
"term",
"termbidi",
"termencoding",
"termguicolors",
"termresize",
"termsync",
"termwinkey",
"termwinscroll",
"termwinsize",
"termwintype",
"terse",
"textauto",
"textmode",
"textwidth",
"thesaurus",
"tildeop",
"timeout",
"timeoutlen",
"title",
"titlelen",
"toolbar",
"toolbariconsize",
"ttimeout",
"ttimeoutlen",
"ttybuiltin",
"ttyfast",
"ttymouse",
"ttyscroll",
"ttytype",
"undofile",
"undolevels",
"undoreload",
"updatecount",
"updatetime",
"varsofttabstop",
"vartabstop",
"verbose",
"viewoptions",
"virtualedit",
"visualbell",
"w1200",
"w300",
"w9600",
"warn",
"weirdinvert",
"whichwrap",
"wildchar",
"wildcharm",
"wildignore",
"wildignorecase",
"wildmenu",
"wildmode",
"wildoptions",
"winaltkeys",
"wincolor",
"window",
"winfixbuf",
"winfixheight",
"winfixwidth",
"winheight",
"winhighlight",
"winminheight",
"winminwidth",
"winwidth",
"wlseat",
"wlsteal",
"wltimeoutlen",
"wrap",
"wrapmargin",
"wrapscan",
"write",
"writeany",
"writebackup",
"writedelay",
"xtermcodes",
}
def parse_options(path):
"""Parse option entries from optiondefs.h.
Returns list of (name, set_of_flags, line_number).
"""
with open(path) as f:
text = f.read()
pattern = re.compile(
r'\{"(\w+)"' # option full name
r',\s*"?\w*"?\s*,' # short name (may be NULL)
r'\s*'
r'((?:P_\w+[\s|]*)+)', # flags
re.MULTILINE,
)
options = []
for m in pattern.finditer(text):
name = m.group(1)
flags_str = m.group(2)
flags = set(re.findall(r'P_\w+', flags_str))
line_no = text[:m.start()].count('\n') + 1
options.append((name, flags, line_no))
return options
def check_options(options):
errors = []
for name, flags, line_no in options:
has_protection = bool(flags & MODELINE_PROTECTION_FLAGS)
in_protected = name in PROTECTED
in_safe = name in KNOWN_SAFE
if not in_protected and not in_safe:
# New option not in either list — must be reviewed.
errors.append(
f"src/optiondefs.h:{line_no}: new option '{name}' not in "
f"PROTECTED or KNOWN_SAFE list. Review whether it needs "
f"P_SECURE/P_NO_ML/P_MLE and add it to the appropriate list "
f"in .github/scripts/check_option_security.py"
)
elif in_protected and not has_protection:
# Option should have protection but flag was removed.
errors.append(
f"src/optiondefs.h:{line_no}: option '{name}' is in PROTECTED "
f"list but lacks P_SECURE/P_NO_ML/P_MLE flag in optiondefs.h"
)
elif in_safe and has_protection:
# Option gained protection — move it to PROTECTED list.
errors.append(
f"src/optiondefs.h:{line_no}: option '{name}' is in KNOWN_SAFE "
f"list but now has modeline protection flag. Move it to "
f"PROTECTED list in .github/scripts/check_option_security.py"
)
return errors
def main():
options = parse_options(OPTIONDEFS_PATH)
if not options:
print("ERROR: failed to parse any options from", OPTIONDEFS_PATH)
sys.exit(1)
print(f"Parsed {len(options)} options from {OPTIONDEFS_PATH}")
errors = check_options(options)
if errors:
print(f"\n{len(errors)} error(s):\n")
for e in errors:
print(f" {e}")
sys.exit(1)
print("All options are accounted for in the modeline safety review.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment