xref: /illumos-gate/usr/src/tools/onbld/Checks/Comments.py (revision fe231ea6f3cdffee825d2e92e1a4639b3bc796b7)
1#! /usr/bin/python
2#
3# CDDL HEADER START
4#
5# The contents of this file are subject to the terms of the
6# Common Development and Distribution License (the "License").
7# You may not use this file except in compliance with the License.
8#
9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10# or http://www.opensolaris.org/os/licensing.
11# See the License for the specific language governing permissions
12# and limitations under the License.
13#
14# When distributing Covered Code, include this CDDL HEADER in each
15# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16# If applicable, add the following below this CDDL HEADER, with the
17# fields enclosed by brackets "[]" replaced with your own identifying
18# information: Portions Copyright [yyyy] [name of copyright owner]
19#
20# CDDL HEADER END
21#
22
23#
24# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
25# Use is subject to license terms.
26#
27
28# Copyright 2007, 2010 Richard Lowe
29# Copyright 2019 OmniOS Community Edition (OmniOSce) Association.
30# Copyright 2024 Bill Sommerfeld
31
32#
33# Check delta comments:
34#	- Have the correct form.
35#	- Have a synopsis matching that of the bug
36#	- Appear only once.
37#	- Do not contain common spelling errors.
38#
39
40import re, sys
41from onbld.Checks.DbLookups import BugDB
42from onbld.Checks.SpellCheck import spellcheck_line
43
44
45bugre = re.compile(r'^(\d{2,7}) (.*)$')
46
47
48def isBug(comment):
49	return bugre.match(comment)
50
51def changeid_present(comments):
52	if len(comments) < 3:
53		return False
54	if comments[-2] != '':
55		return False
56	if re.match('^Change-Id: I[0-9a-f]+', comments[-1]):
57		return True
58	return False
59
60def comchk(comments, check_db=True, output=sys.stderr, bugs=None):
61	'''Validate checkin comments against ON standards.
62
63	Comments must be a list of one-line comments, with no trailing
64	newline.
65
66	If check_db is True (the default), validate bug synopses against the
67	databases.
68
69	Error messages intended for the user are written to output,
70	which defaults to stderr
71	'''
72	bugnospcre = re.compile(r'^(\d{2,7})([^ ].*)')
73	ignorere = re.compile(r'^(' +
74                              r'Portions contributed by|' +
75                              r'Imported[ -]from|' +
76                              r'Contributed by|' +
77                              r'Reviewed[ -]by|' +
78                              r'Approved[ -]by|' +
79                              r'back[ -]?out)' +
80                              r'[: ]')
81
82	errors = { 'bugnospc': [],
83		   'changeid': [],
84		   'mutant': [],
85		   'dup': [],
86		   'nomatch': [],
87		   'nonexistent': [],
88		   'spelling': [] }
89	if bugs is None:
90		bugs = {}
91	newbugs = set()
92	ret = 0
93	blanks = False
94
95	if changeid_present(comments):
96		comments = comments[:-2]
97		errors['changeid'].append('Change Id present')
98
99	lineno = 0
100	for com in comments:
101		lineno += 1
102
103		# Our input must be newline-free, comments are line-wise.
104		if com.find('\n') != -1:
105			raise ValueError("newline in comment '%s'" % com)
106
107		# Ignore valid comments we can't check
108		if ignorere.search(com):
109			continue
110
111		if not com or com.isspace():
112			blanks = True
113			continue
114
115		for err in spellcheck_line(com):
116			errors['spelling'].append(
117			    'comment line {} - {}'.format(lineno, err))
118
119		match = bugre.search(com)
120		if match:
121			(bugid, synopsis) = match.groups()
122			bugs.setdefault(bugid, []).append(synopsis)
123			newbugs.add(bugid)
124			continue
125
126		#
127		# Bugs missing a space after the ID are still bugs
128		# for the purposes of the duplicate ID and synopsis
129		# checks.
130		#
131		match = bugnospcre.search(com)
132		if match:
133			(bugid, synopsis) = match.groups()
134			bugs.setdefault(bugid, []).append(synopsis)
135			newbugs.add(bugid)
136			errors['bugnospc'].append(com)
137			continue
138
139		# Anything else is bogus
140		errors['mutant'].append(com)
141
142	if len(bugs) > 0 and check_db:
143		bugdb = BugDB()
144		results = bugdb.lookup(list(bugs.keys()))
145
146	for crid in sorted(newbugs):
147		insts = bugs[crid]
148		if len(insts) > 1:
149			errors['dup'].append(crid)
150
151		if not check_db:
152			continue
153
154		if crid not in results:
155			errors['nonexistent'].append(crid)
156			continue
157
158		#
159		# For each synopsis, compare the real synopsis with
160		# that in the comments, allowing for possible '(fix
161		# stuff)'-like trailing text
162		#
163		for entered in insts:
164			synopsis = results[crid]["synopsis"]
165			if not re.search(r'^' + re.escape(synopsis) +
166					r'( \([^)]+\))?$', entered):
167				errors['nomatch'].append([crid, synopsis,
168							entered])
169
170
171	if blanks:
172		output.write("WARNING: Blank line(s) in comments\n")
173		ret = 1
174
175	if errors['dup']:
176		ret = 1
177		output.write("These IDs appear more than once in your "
178			     "comments:\n")
179		for err in errors['dup']:
180			output.write("  %s\n" % err)
181
182	if errors['bugnospc']:
183		ret = 1
184		output.write("These bugs are missing a single space following "
185			     "the ID:\n")
186		for com in errors['bugnospc']:
187			output.write("  %s\n" % com)
188
189	if errors['changeid']:
190		ret = 1
191		output.write("NOTE: Change-Id present in comment\n")
192
193	if errors['mutant']:
194		ret = 1
195		output.write("These comments are not valid bugs:\n")
196		for com in errors['mutant']:
197			output.write("  %s\n" % com)
198
199	if errors['nonexistent']:
200		ret = 1
201		output.write("These bugs were not found in the databases:\n")
202		for id in errors['nonexistent']:
203			output.write("  %s\n" % id)
204
205	if errors['nomatch']:
206		ret = 1
207		output.write("These bug synopses don't match "
208			     "the database entries:\n")
209		for err in errors['nomatch']:
210			output.write("Synopsis of %s is wrong:\n" % err[0])
211			output.write("  should be: '%s'\n" % err[1])
212			output.write("         is: '%s'\n" % err[2])
213
214	if errors['spelling']:
215		ret = 1
216		output.write("Spellcheck:\n")
217		for err in errors['spelling']:
218			output.write('{}\n'.format(err))
219
220	return ret
221