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
31#
32# Check delta comments:
33#	- Have the correct form.
34#	- Have a synopsis matching that of the bug
35#	- Appear only once.
36#	- Do not contain common spelling errors.
37#
38
39import re, sys
40from onbld.Checks.DbLookups import BugDB
41from onbld.Checks.SpellCheck import spellcheck_line
42
43
44bugre = re.compile(r'^(\d{2,7}) (.*)$')
45
46
47def isBug(comment):
48	return bugre.match(comment)
49
50
51def comchk(comments, check_db=True, output=sys.stderr):
52	'''Validate checkin comments against ON standards.
53
54	Comments must be a list of one-line comments, with no trailing
55	newline.
56
57	If check_db is True (the default), validate bug synopses against the
58	databases.
59
60	Error messages intended for the user are written to output,
61	which defaults to stderr
62	'''
63	bugnospcre = re.compile(r'^(\d{2,7})([^ ].*)')
64	ignorere = re.compile(r'^(' +
65                              r'Portions contributed by|' +
66                              r'Contributed by|' +
67                              r'Reviewed[ -]by|' +
68                              r'Approved[ -]by|' +
69                              r'back[ -]?out)' +
70                              r'[: ]')
71
72	errors = { 'bugnospc': [],
73		   'mutant': [],
74		   'dup': [],
75		   'nomatch': [],
76		   'nonexistent': [],
77		   'spelling': [] }
78	bugs = {}
79	ret = 0
80	blanks = False
81
82	lineno = 0
83	for com in comments:
84		lineno += 1
85
86		# Our input must be newline-free, comments are line-wise.
87		if com.find('\n') != -1:
88			raise ValueError("newline in comment '%s'" % com)
89
90		# Ignore valid comments we can't check
91		if ignorere.search(com):
92			continue
93
94		if not com or com.isspace():
95			blanks = True
96			continue
97
98		for err in spellcheck_line(com):
99			errors['spelling'].append(
100			    'comment line {} - {}'.format(lineno, err))
101
102		match = bugre.search(com)
103		if match:
104			if match.group(1) not in bugs:
105				bugs[match.group(1)] = []
106			bugs[match.group(1)].append(match.group(2))
107			continue
108
109		#
110		# Bugs missing a space after the ID are still bugs
111		# for the purposes of the duplicate ID and synopsis
112		# checks.
113		#
114		match = bugnospcre.search(com)
115		if match:
116			if match.group(1) not in bugs:
117				bugs[match.group(1)] = []
118			bugs[match.group(1)].append(match.group(2))
119			errors['bugnospc'].append(com)
120			continue
121
122		# Anything else is bogus
123		errors['mutant'].append(com)
124
125	if len(bugs) > 0 and check_db:
126		bugdb = BugDB()
127		results = bugdb.lookup(list(bugs.keys()))
128
129	for crid, insts in bugs.items():
130		if len(insts) > 1:
131			errors['dup'].append(crid)
132
133		if not check_db:
134			continue
135
136		if crid not in results:
137			errors['nonexistent'].append(crid)
138			continue
139
140		#
141		# For each synopsis, compare the real synopsis with
142		# that in the comments, allowing for possible '(fix
143		# stuff)'-like trailing text
144		#
145		for entered in insts:
146			synopsis = results[crid]["synopsis"]
147			if not re.search(r'^' + re.escape(synopsis) +
148					r'( \([^)]+\))?$', entered):
149				errors['nomatch'].append([crid, synopsis,
150							entered])
151
152
153	if blanks:
154		output.write("WARNING: Blank line(s) in comments\n")
155		ret = 1
156
157	if errors['dup']:
158		ret = 1
159		output.write("These IDs appear more than once in your "
160			     "comments:\n")
161		for err in errors['dup']:
162			output.write("  %s\n" % err)
163
164	if errors['bugnospc']:
165		ret = 1
166		output.write("These bugs are missing a single space following "
167			     "the ID:\n")
168		for com in errors['bugnospc']:
169			output.write("  %s\n" % com)
170
171	if errors['mutant']:
172		ret = 1
173		output.write("These comments are not valid bugs:\n")
174		for com in errors['mutant']:
175			output.write("  %s\n" % com)
176
177	if errors['nonexistent']:
178		ret = 1
179		output.write("These bugs were not found in the databases:\n")
180		for id in errors['nonexistent']:
181			output.write("  %s\n" % id)
182
183	if errors['nomatch']:
184		ret = 1
185		output.write("These bug synopses don't match "
186			     "the database entries:\n")
187		for err in errors['nomatch']:
188			output.write("Synopsis of %s is wrong:\n" % err[0])
189			output.write("  should be: '%s'\n" % err[1])
190			output.write("         is: '%s'\n" % err[2])
191
192	if errors['spelling']:
193		ret = 1
194		output.write("Spellcheck:\n")
195		for err in errors['spelling']:
196			output.write('{}\n'.format(err))
197
198	return ret
199