blob: d55f08a5fe5f983ab9b5960b4aa36d60a9949040 [file] [log] [blame]
Anthony Sottile8f615292022-01-15 19:24:05 -05001from __future__ import annotations
2
Anthony Sottile53f1dc02015-01-04 13:06:21 -08003import argparse
4import io
5import tokenize
Anthony Sottilef5c42a02020-02-05 11:10:42 -08006from tokenize import tokenize as tokenize_tokenize
Anthony Sottile030bfac2019-01-31 19:19:10 -08007from typing import Sequence
Anthony Sottile53f1dc02015-01-04 13:06:21 -08008
Anthony Sottilef5c42a02020-02-05 11:10:42 -08009NON_CODE_TOKENS = frozenset((
10 tokenize.COMMENT, tokenize.ENDMARKER, tokenize.NEWLINE, tokenize.NL,
11 tokenize.ENCODING,
12))
Anthony Sottile53f1dc02015-01-04 13:06:21 -080013
14
Anthony Sottilef5c42a02020-02-05 11:10:42 -080015def check_docstring_first(src: bytes, filename: str = '<unknown>') -> int:
Anthony Sottile53f1dc02015-01-04 13:06:21 -080016 """Returns nonzero if the source has what looks like a docstring that is
17 not at the beginning of the source.
18
19 A string will be considered a docstring if it is a STRING token with a
20 col offset of 0.
21 """
22 found_docstring_line = None
23 found_code_line = None
24
Anthony Sottile2f6a2512019-03-30 15:31:42 -070025 tok_gen = tokenize_tokenize(io.BytesIO(src).readline)
Anthony Sottile53f1dc02015-01-04 13:06:21 -080026 for tok_type, _, (sline, scol), _, _ in tok_gen:
27 # Looks like a docstring!
28 if tok_type == tokenize.STRING and scol == 0:
29 if found_docstring_line is not None:
30 print(
Anthony Sottileb13ff9b2022-04-06 16:55:26 -040031 f'{filename}:{sline}: Multiple module docstrings '
Anthony Sottilef5c42a02020-02-05 11:10:42 -080032 f'(first docstring on line {found_docstring_line}).',
Anthony Sottile53f1dc02015-01-04 13:06:21 -080033 )
34 return 1
35 elif found_code_line is not None:
36 print(
Anthony Sottileb13ff9b2022-04-06 16:55:26 -040037 f'{filename}:{sline}: Module docstring appears after code '
Anthony Sottilef5c42a02020-02-05 11:10:42 -080038 f'(code seen on line {found_code_line}).',
Anthony Sottile53f1dc02015-01-04 13:06:21 -080039 )
40 return 1
41 else:
42 found_docstring_line = sline
43 elif tok_type not in NON_CODE_TOKENS and found_code_line is None:
44 found_code_line = sline
45
46 return 0
47
48
Anthony Sottile8f615292022-01-15 19:24:05 -050049def main(argv: Sequence[str] | None = None) -> int:
Anthony Sottile53f1dc02015-01-04 13:06:21 -080050 parser = argparse.ArgumentParser()
51 parser.add_argument('filenames', nargs='*')
52 args = parser.parse_args(argv)
53
54 retv = 0
55
56 for filename in args.filenames:
Anthony Sottile2f6a2512019-03-30 15:31:42 -070057 with open(filename, 'rb') as f:
Anthony Sottile5dc306b2018-06-18 00:00:38 -070058 contents = f.read()
Anthony Sottile53f1dc02015-01-04 13:06:21 -080059 retv |= check_docstring_first(contents, filename=filename)
60
61 return retv