def test_parse_errors(self): test_string = """\ <?xml version='1.0' encoding='UTF-8'?> <checkstyle version='4.3'> <file name='scripts/run_tests.sh' > <error line='31' column='26' severity='info' message='Double quote to prevent globbing and word splitting.' source='ShellCheck.SC2086' /> </file> <file name='scripts/setup.sh' > <error line='3' column='1' severity='warning' message='FOO appears unused. Verify it or export it.' source='ShellCheck.SC2034' /> </file> </checkstyle> """ result = checkstyle.parse(test_string) self.assertEqual(2, len(result)) self.assertIn( Problem( 'scripts/run_tests.sh', 31, 'ShellCheck.SC2086: Double quote to prevent ' 'globbing and word splitting.'), result) self.assertIn( Problem( 'scripts/setup.sh', 3, 'ShellCheck.SC2034: FOO appears unused. ' 'Verify it or export it.'), result)
def test_parse_errors(self): test_string = [ '{}/Classes/Foo + Bar/Foo + Menu/Controllers/' 'SomeController.swift:42: warning: ' 'Documentation Comment Violation: Needs documentation ' 'comment'.format(os.path.curdir), '{}/Classes/Foo + Bar/Foo + Menu/Controllers/' 'AnotherController.swift:50:5: warning: ' 'Documentation Comment Violation: Needs documentation ' 'comment'.format(os.path.curdir) ] result = swiftlint.parse('\n'.join(test_string)) self.assertEqual(2, len(result)) self.assertIn( Problem( 'Classes/Foo + Bar/Foo + Menu/Controllers/' 'SomeController.swift', 42, 'Documentation Comment Violation: ' 'Needs documentation comment'), result) self.assertIn( Problem( 'Classes/Foo + Bar/Foo + Menu/Controllers/' 'AnotherController.swift', 50, 'Documentation Comment Violation: ' 'Needs documentation comment'), result)
def test_parse_errors(self): test_string = """\ <?xml version="1.0" encoding="UTF-8"?> <pmd version="5.5.0" timestamp="2016-07-06T11:29:03.360"> <file name="{0}/DriverRideDrivingToWaypointController.java"> <violation beginline="287" endline="287" begincolumn="58" endcolumn="61" rule="UnusedFormalParameter" ruleset="Unused Code" package="me.lyft.android.ui.driver.ridescreens" class="DriverRideDrivingToWaypointController" method="displayWaypointAddress" variable="ride" externalInfoUrl="https://pmd.github.io/pmd-5.5.0/pmd-java/rules/java/unusedcode.html#UnusedFormalParameter" priority="3"> Avoid unused method parameters such as 'ride'. </violation> </file> <file name="{0}/AddCouponView.java"> <violation beginline="24" endline="24" begincolumn="1" endcolumn="38" rule="UnusedImports" ruleset="Import Statements" package="me.lyft.android.ui.payment" externalInfoUrl="https://pmd.github.io/pmd-5.5.0/pmd-java/rules/java/imports.html#UnusedImports" priority="4"> Avoid unused imports such as 'me.lyft.android.common.Strings' </violation> </file> </pmd> """.format(os.path.curdir) result = pmd.parse(test_string) self.assertEqual(2, len(result)) self.assertIn( Problem( './DriverRideDrivingToWaypointController.java', 287, 'UnusedFormalParameter: Avoid unused method' " parameters such as 'ride'."), result) self.assertIn( Problem( './AddCouponView.java', 24, 'UnusedImports: Avoid unused imports such as' " 'me.lyft.android.common.Strings'"), result)
def test_parse_errors(self): test_string = [ 'Something happened!', "More stuff 'happened'", ] result = passthrough.parse('\n'.join(test_string)) self.assertEqual(2, len(result)) self.assertIn(Problem('', 0, 'Something happened!'), result) self.assertIn(Problem('', 0, "More stuff 'happened'"), result)
def test_parse_errors(self): test_string = ''' src/linters/pylint.py:10: [E302] expected 2 blank lines, found 1 tests/linters/pylint.py:42: [E302] expected 2 blank lines, found 1 ''' result = pylint.parse(test_string) self.assertEqual(2, len(result)) self.assertIn( Problem('src/linters/pylint.py', 10, '[E302] expected 2 blank lines, found 1'), result) self.assertIn( Problem('tests/linters/pylint.py', 42, '[E302] expected 2 blank lines, found 1'), result)
def test_parse_errors(self): test_string = ''' src/linters/pylint.py: note: expected 2 blank lines, found 1 tests/linters/pylint.py:42: error: "module" has no attribute "foo" tests/linters/pylint.py:33: error: "module" has no attribute "bar" ''' result = mypy.parse(test_string) self.assertEqual(2, len(result)) self.assertIn( Problem('tests/linters/pylint.py', 42, 'error: "module" has no attribute "foo"'), result) self.assertIn( Problem('tests/linters/pylint.py', 33, 'error: "module" has no attribute "bar"'), result)
async def get_existing_problems(self) -> Set[Problem]: note_ref = '' result = set() if self.remote: fetch_notes = await asyncio.create_subprocess_exec( 'git', 'fetch', self.remote, '{0}:{0}'.format(NOTES_REF), **GIT_SUBPROCESS_KWARGS) await fetch_notes.wait() last_n_revisions_proc = await asyncio.create_subprocess_exec( 'git', 'log', '--skip=1', '-{}'.format(MAX_REVISIONS), '--pretty=%H', **GIT_SUBPROCESS_KWARGS) note_ref = self.get_note_ref(last_n_revisions_proc.stdout) await last_n_revisions_proc.wait() if note_ref: notes_proc = await asyncio.create_subprocess_exec( 'git', 'show', note_ref, **GIT_SUBPROCESS_KWARGS) async for line in notes_proc.stdout: try: problems = json.loads(line.decode()) for problem in problems: result.add(Problem.from_json(problem)) except Exception: pass await notes_proc.wait() return result
def test_comment_overflow(self, mock_getenv, mock_client_session): mock_args, fake_client_session = self.create_mock_pr( mock_getenv, mock_client_session) reporter = github_reporter.create_reporter(mock_args) problems = [Problem('another_file', x, 'Wat') for x in range(1, 13)] async_report = reporter.report('unit-test-linter', problems) loop = asyncio.get_event_loop() loop.run_until_complete(async_report) overflow_message = textwrap.dedent('''\ unit-test-linter says: Too many lint errors to report inline! 12 lines have a problem. Only reporting the first 10.''') overflow_call = call.post( 'https://api.github.com/repos/foo/bar/issues/1234/comments', headers={'Authorization': 'token MY_TOKEN'}, data=json.dumps({ 'body': overflow_message, }, sort_keys=True)) self.assertIn(overflow_call, fake_client_session.calls) self.assertEqual(3 + github_reporter.MAX_LINT_ERROR_REPORTS, len(fake_client_session.calls))
def report(self, problems: List[Problem]) -> None: grouped_problems = Problem.group_by_path_and_line(problems) headers = { 'Authorization': 'token {}'.format(self.auth_token), } with aiohttp.ClientSession(headers=headers) as client_session: (line_map, existing_messages) = yield from asyncio.gather( self.create_line_to_position_map(client_session), self.get_existing_messages(client_session)) lint_errors = 0 review_comment_awaitable = [] pr_url = self._get_pr_url() for location, problems_for_line in grouped_problems: message_for_line = [':sparkles:Linty Fresh Says:sparkles::', '', '```'] reported_problems_for_line = set() for problem in problems_for_line: if problem.message not in reported_problems_for_line: message_for_line.append(problem.message) reported_problems_for_line.add(problem.message) message_for_line.append('```') path = location[0] line_number = location[1] position = line_map.get(path, {}).get(line_number, None) if position is not None: message = '\n'.join(message_for_line) if (path, position, message) not in existing_messages: lint_errors += 1 if lint_errors <= MAX_LINT_ERROR_REPORTS: data = json.dumps({ 'body': message, 'commit_id': self.commit, 'path': path, 'position': position, }, sort_keys=True) review_comment_awaitable.append( client_session.post(pr_url, data=data)) if lint_errors > MAX_LINT_ERROR_REPORTS: message = ''':sparkles:Linty Fresh Says:sparkles:: Too many lint errors to report inline! {0} lines have a problem. Only reporting the first {1}.'''.format( lint_errors, MAX_LINT_ERROR_REPORTS) data = json.dumps({ 'body': message }) review_comment_awaitable.append( asyncio.async(client_session.post(pr_url, data=data))) responses = yield from asyncio.gather( *review_comment_awaitable ) # type: List[aiohttp.ClientResponse] for response in responses: response.close()
def test_parse_all(self): result = android.parse(test_string, pass_warnings=False) self.assertEqual(2, len(result)) self.assertIn(Problem('scripts/run_tests.sh', 15, 'ScrollView size validation: This LinearLayout ' 'should use ' '`android:layout_height="wrap_content"`'), result) self.assertIn(Problem('scripts/setup.sh', 238, 'Implied default locale in case conversion: ' 'Implicitly using the default locale is a ' 'common source of bugs: Use ' '`toLowerCase(Locale)` instead'), result)
def report(self, problems: List[Problem]) -> None: grouped_problems = Problem.group_by_path_and_line(problems) headers = { 'Authorization': 'token {}'.format(self.auth_token), } with aiohttp.ClientSession(headers=headers) as client_session: (line_map, existing_messages) = yield from asyncio.gather( self.create_line_to_position_map(client_session), self.get_existing_messages(client_session)) lint_errors = 0 review_comment_awaitable = [] pr_url = self._get_pr_url() for location, problems_for_line in grouped_problems: message_for_line = [ ':sparkles:Linty Fresh Says:sparkles::', '', '```' ] reported_problems_for_line = set() for problem in problems_for_line: if problem.message not in reported_problems_for_line: message_for_line.append(problem.message) reported_problems_for_line.add(problem.message) message_for_line.append('```') path = location[0] line_number = location[1] position = line_map.get(path, {}).get(line_number, None) if position is not None: message = '\n'.join(message_for_line) if (path, position, message) not in existing_messages: lint_errors += 1 if lint_errors <= MAX_LINT_ERROR_REPORTS: data = json.dumps( { 'body': message, 'commit_id': self.commit, 'path': path, 'position': position, }, sort_keys=True) review_comment_awaitable.append( client_session.post(pr_url, data=data)) if lint_errors > MAX_LINT_ERROR_REPORTS: message = ''':sparkles:Linty Fresh Says:sparkles:: Too many lint errors to report inline! {0} lines have a problem. Only reporting the first {1}.'''.format(lint_errors, MAX_LINT_ERROR_REPORTS) data = json.dumps({'body': message}) review_comment_awaitable.append(asyncio. async ( client_session.post(pr_url, data=data))) responses = yield from asyncio.gather( *review_comment_awaitable ) # type: List[aiohttp.ClientResponse] for response in responses: response.close()
def parse(contents: str, **kwargs) -> Set[Problem]: result = set() # type: Set[Problem] for line in contents.splitlines(): match = PYLINT_LINE_REGEX.match(line) if match: groups = match.groupdict() result.add( Problem(groups['path'], groups['line'], groups['message'])) return result
def test_parse_errors_only(self): result = android.parse(test_string, pass_warnings=True) self.assertEqual(1, len(result)) self.assertIn(Problem('scripts/run_tests.sh', 15, 'ScrollView size validation: This LinearLayout ' 'should use ' '`android:layout_height="wrap_content"`'), result)
def test_parse_errors(self): test_string = [ "<unknown>:0: error: no such file or directory: 'foo.swift'", "<unknown>:0: ERROR: no such file or directory: 'case.swift'", "{}/Classes/foo/bar baz/qux.swift:201:21: " "error: use of unresolved identifier 'FooBar'".format( os.path.curdir), "{}/Classes/foo/bar/Protocols/SomeProtocol.swift:7:10: " "note: did you mean 'SomeOtherProtocol'?".format(os.path.curdir), "{}/Resources/Storyboards & XIBs/Foo.storyboard:kB7-Bl-wC0: " "warning: Unsupported configuration of constraint attributes. " "This may produce unexpected results at runtime before Xcode 5.1". format(os.path.curdir), ] result = xcodebuild.parse('\n'.join(test_string)) self.assertEqual(5, len(result)) self.assertIn( Problem('<unknown>', 0, "no such file or directory: 'foo.swift'"), result) self.assertIn( Problem('<unknown>', 0, "no such file or directory: 'case.swift'"), result) self.assertIn( Problem('Classes/foo/bar baz/' 'qux.swift', 201, "use of unresolved identifier 'FooBar'"), result) self.assertIn( Problem('Classes/foo/bar/Protocols/' 'SomeProtocol.swift', 7, "did you mean 'SomeOtherProtocol'?"), result) self.assertIn( Problem( 'Resources/Storyboards & XIBs/' 'Foo.storyboard', 0, 'kB7-Bl-wC0: Unsupported configuration of ' 'constraint attributes. This may produce unexpected ' 'results at runtime before Xcode 5.1'), result)
def parse(contents: str) -> Set[Problem]: result = set() # type: Set[Problem] for line in contents.splitlines(): match = MYPY_LINE_REGEX.match(line) if match: groups = match.groupdict() if groups['code'] != 'note': result.add( Problem(groups['path'], groups['line'], '{}: {}'.format(groups['code'], groups['message']))) return result
def parse(contents: str, **kwargs) -> Set[Problem]: result = set() # type: Set[Problem] for line in contents.splitlines(): match = XCODEBUILD_LINE_REGEX.match(line) if match: groups = match.groupdict() line = groups['line'] or 0 message = groups['message'] reference = groups['reference'] if reference: message = '{}: {}'.format(reference, message) result.add(Problem(os.path.relpath(groups['path']), line, message)) return result
def parse(contents: str) -> Set[Problem]: result = set() # type: Set[Problem] try: root = ElementTree.fromstring(contents) except ElementTree.ParseError: return result for file in root.findall('file'): file_name = file.get('name') for error in file.findall('error'): result.add( Problem( file_name, error.get('line'), '{}: {}'.format(error.get('source'), error.get('message')))) return result
def parse(contents: str, **kwargs) -> Set[Problem]: result = set() # type: Set[Problem] try: root = ElementTree.fromstring(contents) except ElementTree.ParseError: return result for file in root.findall('file'): file_name = file.get('name') for violation in file.findall('violation'): result.add( Problem( file_name, violation.get('beginline'), '{}: {}'.format(violation.get('rule'), violation.text.strip()))) return result
def get_existing_problems(self) -> Set[Problem]: note_ref = '' result = set() if self.remote: fetch_notes = yield from asyncio.create_subprocess_exec( 'git', 'fetch', self.remote, '{0}:{0}'.format(NOTES_REF), **GIT_SUBPROCESS_KWARGS) yield from fetch_notes.wait() last_n_revisions_proc = yield from asyncio.create_subprocess_exec( 'git', 'log', '--skip=1', '-{}'.format(MAX_REVISIONS), '--pretty=%H', **GIT_SUBPROCESS_KWARGS) for line in last_n_revisions_proc.stdout: notes_ref_proc = yield from asyncio.create_subprocess_exec( 'git', 'notes', 'list', line.decode().strip(), **GIT_SUBPROCESS_KWARGS) yield from notes_ref_proc.wait() if notes_ref_proc.returncode == 0: note_ref = yield from notes_ref_proc.stdout.readline() note_ref = note_ref.decode().strip() if note_ref: break yield from last_n_revisions_proc.wait() if note_ref: notes_proc = yield from asyncio.create_subprocess_exec( 'git', 'show', note_ref, **GIT_SUBPROCESS_KWARGS) for line in notes_proc.stdout: try: problems = json.loads(line.decode()) for problem in problems: result.add(Problem.from_json(problem)) except Exception: pass yield from notes_proc.wait() return result
def test_comment_on_pr(self, mock_getenv, mock_client_session): mock_args, fake_client_session = self.create_mock_pr( mock_getenv, mock_client_session) reporter = github_reporter.create_reporter(mock_args) async_report = reporter.report('unit-test-linter', [ Problem('some_dir/some_file', 40, 'this made me sad'), Problem('some_dir/some_file', 40, 'really sad'), Problem('another_file', 2, 'This is OK'), Problem('another_file', 2, 'This is OK'), Problem('another_file', 3, 'I am a duplicate!'), Problem('another_file', 52, "#close_enough!!!"), Problem('missing_file', 42, "Missing file comment!!!"), ]) loop = asyncio.get_event_loop() loop.run_until_complete(async_report) diff_request = call.get( 'https://api.github.com/repos/foo/bar/pulls/1234', headers={ 'Accept': 'application/vnd.github.diff', 'Authorization': 'token MY_TOKEN' }) existing_comments_request = call.get( 'https://api.github.com/repos/foo/bar/pulls/1234/comments', headers={'Authorization': 'token MY_TOKEN'}) first_comment = call.post( 'https://api.github.com/repos/foo/bar/pulls/1234/comments', headers={'Authorization': 'token MY_TOKEN'}, data=json.dumps( { 'commit_id': 'abc123', 'path': 'another_file', 'body': textwrap.dedent('''\ unit-test-linter says: ``` This is OK ```'''), 'position': 2 }, sort_keys=True)) second_comment = call.post( 'https://api.github.com/repos/foo/bar/pulls/1234/comments', headers={'Authorization': 'token MY_TOKEN'}, data=json.dumps( { 'commit_id': 'abc123', 'path': 'some_dir/some_file', 'body': textwrap.dedent('''\ unit-test-linter says: ``` this made me sad really sad ```'''), 'position': 3 }, sort_keys=True)) close_enough_comment = call.post( 'https://api.github.com/repos/foo/bar/pulls/1234/comments', headers={'Authorization': 'token MY_TOKEN'}, data=json.dumps( { 'commit_id': 'abc123', 'path': 'another_file', 'body': textwrap.dedent('''\ unit-test-linter says: (From line 52) ``` #close_enough!!! ```'''), 'position': 12 }, sort_keys=True)) missing_file_call = call.post( 'https://api.github.com/repos/foo/bar/issues/1234/comments', headers={'Authorization': 'token MY_TOKEN'}, data=json.dumps( { 'body': textwrap.dedent('''\ unit-test-linter found some problems with lines not modified by this commit: ``` missing_file:42: \tMissing file comment!!! ```'''), }, sort_keys=True)) self.assertEqual(6, len(fake_client_session.calls)) self.assertIn(diff_request, fake_client_session.calls) self.assertIn(existing_comments_request, fake_client_session.calls) self.assertIn(first_comment, fake_client_session.calls) self.assertIn(second_comment, fake_client_session.calls) self.assertIn(close_enough_comment, fake_client_session.calls) self.assertIn(missing_file_call, fake_client_session.calls)
def parse(contents: str, **kwargs) -> Set[Problem]: if contents: return set([Problem('', 0, contents.strip())]) else: return set()
def test_comment_on_pr(self, mock_getenv, mock_client_session): mock_args, fake_client_session = self.create_mock_pr( mock_getenv, mock_client_session) reporter = github_reporter.create_reporter(mock_args) async_report = reporter.report([ Problem('some_dir/some_file', 40, 'this made me sad'), Problem('some_dir/some_file', 40, 'really sad'), Problem('another_file', 2, 'This is OK'), Problem('another_file', 2, 'This is OK'), Problem('another_file', 3, 'I am a duplicate!'), Problem('missing_file', 42, "Don't report me!!!"), ]) loop = asyncio.get_event_loop() loop.run_until_complete(async_report) diff_request = call.get( 'https://api.github.com/repos/foo/bar/pulls/1234', headers={ 'Accept': 'application/vnd.github.diff', 'Authorization': 'token MY_TOKEN' }) existing_comments_request = call.get( 'https://api.github.com/repos/foo/bar/pulls/1234/comments', headers={'Authorization': 'token MY_TOKEN'}) first_comment = call.post( 'https://api.github.com/repos/foo/bar/pulls/1234/comments', headers={'Authorization': 'token MY_TOKEN'}, data=json.dumps( { 'commit_id': 'abc123', 'path': 'another_file', 'body': textwrap.dedent('''\ :sparkles:Linty Fresh Says:sparkles:: ``` This is OK ```'''), 'position': 2 }, sort_keys=True)) second_comment = call.post( 'https://api.github.com/repos/foo/bar/pulls/1234/comments', headers={'Authorization': 'token MY_TOKEN'}, data=json.dumps( { 'commit_id': 'abc123', 'path': 'some_dir/some_file', 'body': textwrap.dedent('''\ :sparkles:Linty Fresh Says:sparkles:: ``` this made me sad really sad ```'''), 'position': 3 }, sort_keys=True)) self.assertEqual(4, len(fake_client_session.calls)) self.assertIn(diff_request, fake_client_session.calls) self.assertIn(existing_comments_request, fake_client_session.calls) self.assertIn(first_comment, fake_client_session.calls) self.assertIn(second_comment, fake_client_session.calls)
def parse(contents: str, **kwargs) -> Set[Problem]: return set(Problem('', 0, x) for x in contents.splitlines())
async def report(self, linter_name: str, problems: List[GenericProblem]) -> None: if not problems: grouped_problems = {} elif isinstance(list(problems)[0], TestProblem): grouped_problems = TestProblem.group_by_group(problems) else: grouped_problems = Problem.group_by_path_and_line(problems) headers = { 'Authorization': 'token {}'.format(self.auth_token), } with aiohttp.ClientSession(headers=headers) as client_session: (line_map, existing_messages, message_ids) = await asyncio.gather( self.create_line_to_position_map(client_session), self.get_existing_pr_messages(client_session, linter_name), self.get_existing_issue_message_ids(client_session, linter_name)) lint_errors = 0 review_comment_awaitable = [] pr_url = self._get_pr_url() no_matching_line_number = [] for location, problems_for_line in grouped_problems: message_for_line = ['{0} says:'.format(linter_name), ''] reported_problems_for_line = set() path = location[0] line_number = location[1] position = line_map.get(path, {}).get(line_number, None) if position is None and path in line_map: file_map = line_map[path] closest_line = min(file_map.keys(), key=lambda x: abs(x - line_number)) position = file_map[closest_line] message_for_line.append( '(From line {})'.format(line_number)) message_for_line.append('```') if position is not None: for problem in problems_for_line: if problem.message not in reported_problems_for_line: message_for_line.append(problem.message) reported_problems_for_line.add(problem.message) message_for_line.append('```') message = '\n'.join(message_for_line) try: existing_messages.remove( ExistingGithubMessage(None, path, position, message)) except KeyError: lint_errors += 1 if lint_errors <= MAX_LINT_ERROR_REPORTS: data = json.dumps( { 'body': message, 'commit_id': self.commit, 'path': path, 'position': position, }, sort_keys=True) review_comment_awaitable.append( client_session.post(pr_url, data=data)) else: no_matching_line_number.append( (location, problems_for_line)) if lint_errors > MAX_LINT_ERROR_REPORTS: message = """{0} says: Too many lint errors to report inline! {1} lines have a problem. Only reporting the first {2}.""".format(linter_name, lint_errors, MAX_LINT_ERROR_REPORTS) data = json.dumps({'body': message}) review_comment_awaitable.append( asyncio.ensure_future( client_session.post(self._get_issue_url(), data=data))) if self.delete_previous_comments: for message_id in message_ids: review_comment_awaitable.append( asyncio.ensure_future( client_session.delete( self._get_delete_issue_comment_url( message_id)))) for message in existing_messages: review_comment_awaitable.append( asyncio.ensure_future( client_session.delete( self._get_delete_pr_comment_url( message.comment_id)))) if no_matching_line_number: no_matching_line_messages = [] for location, problems_for_line in no_matching_line_number: lint_errors += 1 path = location[0] line_number = location[1] no_matching_line_messages.append('{0}:{1}:'.format( path, line_number)) for problem in problems_for_line: no_matching_line_messages.append('\t{0}'.format( problem.message)) message = ('{0} says: I found some problems with lines not ' 'modified by this commit:\n```\n{1}\n```'.format( linter_name, '\n'.join(no_matching_line_messages))) data = json.dumps({'body': message}) review_comment_awaitable.append( asyncio.ensure_future( client_session.post(self._get_issue_url(), data=data))) responses = await asyncio.gather( *review_comment_awaitable ) # type: List[aiohttp.ClientResponse] for response in responses: response.close() if lint_errors > 0: raise HadLintErrorsException()
async def report(self, problems: List[Problem]) -> None: grouped_problems = Problem.group_by_path_and_line(problems) headers = { 'Authorization': 'token {}'.format(self.auth_token), } with aiohttp.ClientSession(headers=headers) as client_session: (line_map, existing_messages) = await asyncio.gather( self.create_line_to_position_map(client_session), self.get_existing_messages(client_session)) lint_errors = 0 review_comment_awaitable = [] pr_url = self._get_pr_url() no_matching_line_number = [] for location, problems_for_line in grouped_problems: message_for_line = [ ':sparkles:Linty Fresh Says:sparkles::', '' ] reported_problems_for_line = set() path = location[0] line_number = location[1] position = line_map.get(path, {}).get(line_number, None) if position is None and path in line_map: file_map = line_map[path] closest_line = min(file_map.keys(), key=lambda x: abs(x - line_number)) position = file_map[closest_line] message_for_line.append( '(From line {})'.format(line_number)) message_for_line.append('```') if position is not None: for problem in problems_for_line: if problem.message not in reported_problems_for_line: message_for_line.append(problem.message) reported_problems_for_line.add(problem.message) message_for_line.append('```') message = '\n'.join(message_for_line) if (path, position, message) not in existing_messages: lint_errors += 1 if lint_errors <= MAX_LINT_ERROR_REPORTS: data = json.dumps( { 'body': message, 'commit_id': self.commit, 'path': path, 'position': position, }, sort_keys=True) review_comment_awaitable.append( client_session.post(pr_url, data=data)) else: no_matching_line_number.append( (location, problems_for_line)) if lint_errors > MAX_LINT_ERROR_REPORTS: message = ''':sparkles:Linty Fresh Says:sparkles:: Too many lint errors to report inline! {0} lines have a problem. Only reporting the first {1}.'''.format(lint_errors, MAX_LINT_ERROR_REPORTS) data = json.dumps({'body': message}) review_comment_awaitable.append( asyncio.ensure_future( client_session.post(self._get_issue_url(), data=data))) if no_matching_line_number: no_matching_line_messages = [] for location, problems_for_line in no_matching_line_number: path = location[0] line_number = location[1] no_matching_line_messages.append('{0}:{1}:'.format( path, line_number)) for problem in problems_for_line: no_matching_line_messages.append('\t{0}'.format( problem.message)) message = ('Linters found some problems with lines not ' 'modified by this commit:\n```\n{0}\n```'.format( '\n'.join(no_matching_line_messages))) data = json.dumps({'body': message}) review_comment_awaitable.append( asyncio.ensure_future( client_session.post(self._get_issue_url(), data=data))) responses = await asyncio.gather( *review_comment_awaitable ) # type: List[aiohttp.ClientResponse] for response in responses: response.close()