Created
March 31, 2026 06:39
-
-
Save mattn/06020b32c9e5abff01804e2640cd8c3e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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