def check(doc: str) -> list[Diagnostic]:
"""Statically lint the usage grammar itself, before any argv is parsed.
Returns a list of ``"warning"``-level diagnostics for defects the usage message contains that
[`docopt`][docopt2.docopt] would otherwise accept silently: an option declared but never usable, a
``[default: ...]`` on an always-required element, an empty ``[options]`` shortcut.
The check is read-only and does not affect parsing or matching. A usage message too malformed to parse
returns no warnings, since that error surfaces at parse time instead.
"""
warnings: list[Diagnostic] = []
declared: list[tuple[Option, Span, Span]] = []
for line, offset in _section_lines(doc, "options:"):
if not _OPTION_LINE.match(line.lstrip()):
continue # a wrapped description, or a prose bullet: `- fast, quick` is not an option
try:
option = Option.parse(line)
except DocoptLanguageError:
return [] # docopt rejects the whole message; that error is the one worth reading, not a lint
declared.append((option, _token_span(line, offset), _default_span(line, offset)))
options = [option for option, _name, _default in declared]
try:
# formal_tokens, not formal_usage: the latter builds the same tree but offsets every leaf span, and
# the redundant-alternative rule reads the source a branch was written from.
usage = single_usage_section(doc)
pattern = parse_pattern(formal_tokens(usage), list(options))
except (DocoptLanguageError, RecursionError):
return warnings
in_usage = {leaf.name for leaf in pattern.flat(Option)}
required = set(always_required_names(pattern))
has_shortcut = bool(pattern.flat(OptionsShortcut))
for option, name_span, default_span in declared:
if option.name not in in_usage and not has_shortcut:
warnings.append(
_warn(
f"option `{option.name}` is declared but never used",
doc,
name_span,
"in the options:",
"declared here",
f"add `{option.name}` to a usage line, or add `[options]` to accept it",
)
)
if option.argcount and option.value is not None and option.name in required:
warnings.append(
_warn(
f"dead default on `{option.name}`, which the usage always requires",
doc,
default_span,
"in the options:",
"never applies",
f"make `{option.name}` optional with `[ ... ]`, or drop the default",
)
)
for line, offset in _section_lines(doc, "arguments:"):
name = line.split()[0]
if _DEFAULT.search(line) and name in required:
warnings.append(
_warn(
f"dead default on `{name}`, which the usage always requires",
doc,
_default_span(line, offset),
"in the arguments:",
"never applies",
f"make `{name}` optional with `[{name}]`, or drop the default",
)
)
if _variadic_units(pattern) >= 2:
warnings.append(
Diagnostic(
summary="ambiguous grammar: two variadic positionals share one sequence",
help="the token split between them is undefined; make one non-variadic, or split into branches",
level="warning",
)
)
# Not `flat(Either)`: it returns a matching node without descending into it, and a multi-line usage is
# the outermost Either - so an `(a | a)` written inside a usage line would go unseen.
for either in _nested_eithers(pattern):
seen: set[str] = set()
for branch in either.children:
if _branch_key(branch, usage) in seen:
warnings.append(
Diagnostic(
summary="redundant alternative: this branch repeats an earlier one",
help="one of the identical `|` alternatives can never be reached; remove it or fix the typo",
level="warning",
)
)
seen.add(_branch_key(branch, usage))
if has_shortcut and not options:
at = doc.lower().find("[options]")
span = (at, at + len("[options]")) if at != -1 else None
warnings.append(
_warn(
"`[options]` accepts nothing - no options are described",
doc,
span,
"in the usage:",
"expands to nothing",
"describe options in an `Options:` section, or remove `[options]`",
)
)
return warnings