Hi Guoqing, On Wed, Jul 1, 2026 at 9:45 AM Guoqing Yang <[email protected]> wrote: > > Fix grammar in buffer lock wait message > > Correct the wait message from "Waiting to acquire a exclusive lock on a > buffer" > to "Waiting to acquire an exclusive lock on a buffer". > > The word "exclusive" starts with a vowel sound, so the indefinite article > should be "an" instead of "a".
This seems to be a typo. I asked Sol to check this kind of issue in the tree, and it found a bunch of them. Do you bother to do a further check? As John suggested, we better fix grammar issues wholesale[1]. [1] https://www.postgresql.org/message-id/CANWCAZaZgQN54ccVheWd%3Dyg2KW_c1vTPB7hd0CXkqBNLD2-pDA%40mail.gmail.com -- Regards, Xuneng Zhou HighGo Software Co., Ltd.
fix-indefinite-articles.patch
Description: Binary data
#!/usr/bin/env python3
"""Check prose and messages for likely incorrect uses of "a" and "an".
The check is intentionally conservative. English article choice depends on
pronunciation rather than spelling, so this script ignores known exceptions
and ambiguous abbreviation-like words instead of trying to be a grammar
checker. With no path arguments, it scans tracked files in the repository.
"""
import argparse
import os
from pathlib import Path
import re
import subprocess
import sys
ARTICLE_RE = re.compile(
r"(?<![A-Za-z0-9_<./-])\b(a|an)[ \t]{1,3}([A-Za-z][A-Za-z0-9'-]*)",
re.IGNORECASE,
)
TAG_RE = re.compile(r"<[^>]*>")
SGML_BLOCK_OPEN_RE = re.compile(
r"<(programlisting|screen|synopsis|literallayout|cmdsynopsis)\b[^>]*>",
re.IGNORECASE,
)
SGML_EMPTY_RE = re.compile(r"<([A-Za-z][A-Za-z0-9_-]*)\b[^>]*/>")
MARKDOWN_CODE_RE = re.compile(r"`([^`]*)`")
PROSE_SUFFIXES = {".htm", ".html", ".man", ".md", ".rst", ".txt", ".xml", ".xsl"}
C_SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".h", ".l", ".pgc", ".s", ".y"}
SCRIPT_SUFFIXES = {".pl", ".pm", ".py", ".sh", ".t"}
COMMENT_ONLY_SUFFIXES = {".build", ".conf", ".in", ".mk", ".sample"}
# These vowel-letter prefixes begin with a consonant sound.
A_EXCEPTIONS = (
"eucaly",
"eugen",
"eul",
"eup",
"euro",
"ewe",
"once",
"one",
"ubiquit",
"u-",
"uchar",
"unicast",
"unicode",
"unicorn",
"uniform",
"unifi",
"union",
"unique",
"unit",
"univ",
"unify",
"unilateral",
"uniprocessor",
"unix",
"upc",
"uri",
"unary",
"umask",
"usable",
"usage",
"use",
"user",
"usual",
"utc",
"utf",
"utility",
"uint",
"uuid",
)
SILENT_H_PREFIXES = ("heir", "herb", "honest", "honor", "honour", "hour")
# A consonant in this set unambiguously has a consonant sound in ordinary
# words. Other initial letters are often spoken as abbreviations ("an LSN",
# "an XID"), so treating them as errors would be too noisy.
UNAMBIGUOUS_CONSONANTS = frozenset("bcdgjkpqtvwyz")
# These are commonly adjacent code tokens or verbs, not article/noun pairs.
NON_ARTICLE_FOLLOWERS = frozenset(
(
"a",
"already",
"an",
"and",
"anyelement",
"are",
"as",
"at",
"if",
"in",
"inner",
"int",
"integer",
"into",
"is",
"isnull",
"implies",
"of",
"on",
"or",
"overlap",
"overlaps",
"uint",
"uint8-array",
"used",
"using",
)
)
def repository_root():
return Path(__file__).resolve().parents[2]
def tracked_files(root):
result = subprocess.run(
["git", "ls-files", "-z"],
cwd=root,
check=True,
stdout=subprocess.PIPE,
)
names = result.stdout.decode("utf-8", errors="surrogateescape").split("\0")
return [root / name for name in names if name]
def requested_files(paths):
files = []
for path in paths:
path = path.resolve()
if path.is_dir():
for dirname, dirnames, filenames in os.walk(path):
dirnames[:] = [name for name in dirnames if name != ".git"]
files.extend(Path(dirname) / name for name in filenames)
else:
files.append(path)
return files
def file_mode(path):
suffix = path.suffix.lower()
name = path.name
if suffix == ".sgml":
return "sgml"
if suffix in PROSE_SUFFIXES or name.startswith("README"):
return "prose"
if suffix in C_SOURCE_SUFFIXES:
return "c-source"
if suffix in SCRIPT_SUFFIXES:
return "script"
if suffix in COMMENT_ONLY_SUFFIXES or name in {"GNUmakefile", "Makefile"}:
return "comments"
return None
def quoted_fragment(line, start, quote):
i = start + 1
while i < len(line):
if line[i] == "\\":
i += 2
elif line[i] == quote:
return line[start + 1 : i], i + 1
else:
i += 1
return line[start + 1 :], len(line)
def c_source_fragments(line, in_block_comment):
fragments = []
i = 0
while i < len(line):
if in_block_comment:
end = line.find("*/", i)
if end < 0:
fragments.append(line[i:])
return fragments, True
fragments.append(line[i:end])
i = end + 2
in_block_comment = False
elif line.startswith("/*", i):
in_block_comment = True
i += 2
elif line.startswith("//", i):
fragments.append(line[i + 2 :])
break
elif line[i] == '"':
fragment, i = quoted_fragment(line, i, '"')
fragments.append(fragment)
elif line[i] == "'":
_, i = quoted_fragment(line, i, "'")
else:
i += 1
return fragments, in_block_comment
def script_fragments(line):
in_quote = None
i = 0
while i < len(line):
if in_quote:
if line[i] == "\\":
i += 2
elif line[i] == in_quote:
in_quote = None
i += 1
else:
i += 1
elif line[i] in {'"', "'"}:
in_quote = line[i]
i += 1
elif line[i] == "#":
return [line[i + 1 :]]
else:
i += 1
return []
def comment_fragment(line):
stripped = line.lstrip()
if stripped.startswith("#"):
return [stripped[1:]]
return []
def sgml_fragments(line, blocked_tag):
fragments = []
pos = 0
while pos < len(line):
if blocked_tag:
close = re.search(
r"</{}\s*>".format(re.escape(blocked_tag)),
line[pos:],
re.IGNORECASE,
)
if close is None:
return fragments, blocked_tag
pos += close.end()
blocked_tag = None
continue
opening = SGML_BLOCK_OPEN_RE.search(line, pos)
end = opening.start() if opening else len(line)
fragment = line[pos:end]
fragment = SGML_EMPTY_RE.sub(
lambda match: " {} ".format(match.group(1)), fragment
)
fragments.append(TAG_RE.sub(" ", fragment))
if opening is None:
break
blocked_tag = opening.group(1)
pos = opening.end()
return fragments, blocked_tag
def a_takes_an(word):
lower = word.lower()
if lower[0] not in "aeiou":
return False
if lower.startswith(A_EXCEPTIONS):
return False
if word[0] == "U" and (len(word) == 1 or not word[1].islower()):
return False
return True
def an_takes_a(word):
lower = word.lower()
if lower.startswith(A_EXCEPTIONS):
return True
if lower.startswith(SILENT_H_PREFIXES):
return False
if lower[0] in "aeiou":
return False
if lower[0] in UNAMBIGUOUS_CONSONANTS:
return True
return False
def check_fragment(fragment):
for match in ARTICLE_RE.finditer(fragment):
raw_article = match.group(1)
article = raw_article.lower()
word = match.group(2)
lower_word = word.lower()
if lower_word in NON_ARTICLE_FOLLOWERS:
continue
if word[0].islower() and any(char.isdigit() for char in word):
continue
if raw_article == "A":
prefix = fragment[: match.start()].rstrip()
if prefix and prefix[-1] not in ".!?:":
continue
if article == "a" and a_takes_an(word):
yield "an", word
elif article == "an" and an_takes_a(word):
yield "a", word
def display_path(path, root):
try:
return path.relative_to(root)
except ValueError:
return path
def check_file(path, root):
mode = file_mode(path)
if mode is None:
return 0
try:
lines = path.read_text(encoding="utf-8").splitlines()
except UnicodeDecodeError:
return 0
except OSError as error:
print(
"{}: cannot read: {}".format(display_path(path, root), error),
file=sys.stderr,
)
return 0
findings = 0
in_block_comment = False
blocked_sgml_tag = None
for lineno, line in enumerate(lines, 1):
if mode == "sgml":
fragments, blocked_sgml_tag = sgml_fragments(line, blocked_sgml_tag)
elif mode == "prose":
fragments = [MARKDOWN_CODE_RE.sub(r"\1", line)]
elif mode == "c-source":
fragments, in_block_comment = c_source_fragments(line, in_block_comment)
elif mode == "script":
fragments = script_fragments(line)
else:
fragments = comment_fragment(line)
for fragment in fragments:
for replacement, word in check_fragment(fragment):
context = " ".join(line.strip().split())
if len(context) > 160:
context = context[:157] + "..."
print(
'{}:{}: use "{}" before "{}": {}'.format(
display_path(path, root), lineno, replacement, word, context
)
)
findings += 1
return findings
def parse_arguments():
parser = argparse.ArgumentParser(
description="check likely a/an errors in tracked prose and messages"
)
parser.add_argument(
"paths",
metavar="PATH",
nargs="*",
type=Path,
help="files or directories to scan (default: tracked repository files)",
)
return parser.parse_args()
def main():
args = parse_arguments()
root = repository_root()
files = requested_files(args.paths) if args.paths else tracked_files(root)
findings = sum(check_file(path, root) for path in sorted(set(files)))
if findings:
sys.stdout.flush()
print("{} likely article error(s) found".format(findings), file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
