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'Contributed by|' +
76                              r'Reviewed[ -]by|' +
77                              r'Approved[ -]by|' +
78                              r'back[ -]?out)' +
79                              r'[: ]')
80
81	errors = { 'bugnospc': [],
82		   'changeid': [],
83		   'mutant': [],
84		   'dup': [],
85		   'nomatch': [],
86		   'nonexistent': [],
87		   'spelling': [] }
88	if bugs is None:
89		bugs = {}
90	newbugs = set()
91	ret = 0
92	blanks = False
93
94	if changeid_present(comments):
95		comments = comments[:-2]
96		errors['changeid'].append('Change Id present')
97
98	lineno = 0
99	for com in comments:
100		lineno += 1
101
102		# Our input must be newline-free, comments are line-wise.
103		if com.find('\n') != -1:
104			raise ValueError("newline in comment '%s'" % com)
105
106		# Ignore valid comments we can't check
107		if ignorere.search(com):
108			continue
109
110		if not com or com.isspace():
111			blanks = True
112			continue
113
114		for err in spellcheck_line(com):
115			errors['spelling'].append(
116			    'comment line {} - {}'.format(lineno, err))
117
118		match = bugre.search(com)
119		if match:
120			(bugid, synopsis) = match.groups()
121			bugs.setdefault(bugid, []).append(synopsis)
122			newbugs.add(bugid)
123			continue
124
125		#
126		# Bugs missing a space after the ID are still bugs
127		# for the purposes of the duplicate ID and synopsis
128		# checks.
129		#
130		match = bugnospcre.search(com)
131		if match:
132			(bugid, synopsis) = match.groups()
133			bugs.setdefault(bugid, []).append(synopsis)
134			newbugs.add(bugid)
135			errors['bugnospc'].append(com)
136			continue
137
138		# Anything else is bogus
139		errors['mutant'].append(com)
140
141	if len(bugs) > 0 and check_db:
142		bugdb = BugDB()
143		results = bugdb.lookup(list(bugs.keys()))
144
145	for crid in sorted(newbugs):
146		insts = bugs[crid]
147		if len(insts) > 1:
148			errors['dup'].append(crid)
149
150		if not check_db:
151			continue
152
153		if crid not in results:
154			errors['nonexistent'].append(crid)
155			continue
156
157		#
158		# For each synopsis, compare the real synopsis with
159		# that in the comments, allowing for possible '(fix
160		# stuff)'-like trailing text
161		#
162		for entered in insts:
163			synopsis = results[crid]["synopsis"]
164			if not re.search(r'^' + re.escape(synopsis) +
165					r'( \([^)]+\))?$', entered):
166				errors['nomatch'].append([crid, synopsis,
167							entered])
168
169
170	if blanks:
171		output.write("WARNING: Blank line(s) in comments\n")
172		ret = 1
173
174	if errors['dup']:
175		ret = 1
176		output.write("These IDs appear more than once in your "
177			     "comments:\n")
178		for err in errors['dup']:
179			output.write("  %s\n" % err)
180
181	if errors['bugnospc']:
182		ret = 1
183		output.write("These bugs are missing a single space following "
184			     "the ID:\n")
185		for com in errors['bugnospc']:
186			output.write("  %s\n" % com)
187
188	if errors['changeid']:
189		ret = 1
190		output.write("NOTE: Change-Id present in comment\n")
191
192	if errors['mutant']:
193		ret = 1
194		output.write("These comments are not valid bugs:\n")
195		for com in errors['mutant']:
196			output.write("  %s\n" % com)
197
198	if errors['nonexistent']:
199		ret = 1
200		output.write("These bugs were not found in the databases:\n")
201		for id in errors['nonexistent']:
202			output.write("  %s\n" % id)
203
204	if errors['nomatch']:
205		ret = 1
206		output.write("These bug synopses don't match "
207			     "the database entries:\n")
208		for err in errors['nomatch']:
209			output.write("Synopsis of %s is wrong:\n" % err[0])
210			output.write("  should be: '%s'\n" % err[1])
211			output.write("         is: '%s'\n" % err[2])
212
213	if errors['spelling']:
214		ret = 1
215		output.write("Spellcheck:\n")
216		for err in errors['spelling']:
217			output.write('{}\n'.format(err))
218
219	return ret
220