pylinter.py 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. import argparse
  6. import collections
  7. import contextlib
  8. import functools
  9. import os
  10. import sys
  11. import tokenize
  12. import traceback
  13. import warnings
  14. from collections import defaultdict
  15. from collections.abc import Callable, Iterator, Sequence
  16. from io import TextIOWrapper
  17. from pathlib import Path
  18. from re import Pattern
  19. from types import ModuleType
  20. from typing import Any
  21. import astroid
  22. from astroid import nodes
  23. from pylint import checkers, exceptions, interfaces, reporters
  24. from pylint.checkers.base_checker import BaseChecker
  25. from pylint.config.arguments_manager import _ArgumentsManager
  26. from pylint.constants import (
  27. MAIN_CHECKER_NAME,
  28. MSG_TYPES,
  29. MSG_TYPES_STATUS,
  30. WarningScope,
  31. )
  32. from pylint.interfaces import HIGH
  33. from pylint.lint.base_options import _make_linter_options
  34. from pylint.lint.caching import load_results, save_results
  35. from pylint.lint.expand_modules import (
  36. _is_ignored_file,
  37. discover_package_path,
  38. expand_modules,
  39. )
  40. from pylint.lint.message_state_handler import _MessageStateHandler
  41. from pylint.lint.parallel import check_parallel
  42. from pylint.lint.report_functions import (
  43. report_messages_by_module_stats,
  44. report_messages_stats,
  45. report_total_messages_stats,
  46. )
  47. from pylint.lint.utils import (
  48. _is_relative_to,
  49. augmented_sys_path,
  50. get_fatal_error_message,
  51. prepare_crash_report,
  52. )
  53. from pylint.message import Message, MessageDefinition, MessageDefinitionStore
  54. from pylint.reporters.base_reporter import BaseReporter
  55. from pylint.reporters.text import TextReporter
  56. from pylint.reporters.ureports import nodes as report_nodes
  57. from pylint.typing import (
  58. DirectoryNamespaceDict,
  59. FileItem,
  60. ManagedMessage,
  61. MessageDefinitionTuple,
  62. MessageLocationTuple,
  63. ModuleDescriptionDict,
  64. Options,
  65. )
  66. from pylint.utils import ASTWalker, FileState, LinterStats, utils
  67. if sys.version_info >= (3, 8):
  68. from typing import Protocol
  69. else:
  70. from typing_extensions import Protocol
  71. MANAGER = astroid.MANAGER
  72. class GetAstProtocol(Protocol):
  73. def __call__(
  74. self, filepath: str, modname: str, data: str | None = None
  75. ) -> nodes.Module:
  76. ...
  77. def _read_stdin() -> str:
  78. # See https://github.com/python/typeshed/pull/5623 for rationale behind assertion
  79. assert isinstance(sys.stdin, TextIOWrapper)
  80. sys.stdin = TextIOWrapper(sys.stdin.detach(), encoding="utf-8")
  81. return sys.stdin.read()
  82. def _load_reporter_by_class(reporter_class: str) -> type[BaseReporter]:
  83. qname = reporter_class
  84. module_part = astroid.modutils.get_module_part(qname)
  85. module = astroid.modutils.load_module_from_name(module_part)
  86. class_name = qname.split(".")[-1]
  87. klass = getattr(module, class_name)
  88. assert issubclass(klass, BaseReporter), f"{klass} is not a BaseReporter"
  89. return klass # type: ignore[no-any-return]
  90. # Python Linter class #########################################################
  91. # pylint: disable-next=consider-using-namedtuple-or-dataclass
  92. MSGS: dict[str, MessageDefinitionTuple] = {
  93. "F0001": (
  94. "%s",
  95. "fatal",
  96. "Used when an error occurred preventing the analysis of a \
  97. module (unable to find it for instance).",
  98. {"scope": WarningScope.LINE},
  99. ),
  100. "F0002": (
  101. "%s: %s",
  102. "astroid-error",
  103. "Used when an unexpected error occurred while building the "
  104. "Astroid representation. This is usually accompanied by a "
  105. "traceback. Please report such errors !",
  106. {"scope": WarningScope.LINE},
  107. ),
  108. "F0010": (
  109. "error while code parsing: %s",
  110. "parse-error",
  111. "Used when an exception occurred while building the Astroid "
  112. "representation which could be handled by astroid.",
  113. {"scope": WarningScope.LINE},
  114. ),
  115. "F0011": (
  116. "error while parsing the configuration: %s",
  117. "config-parse-error",
  118. "Used when an exception occurred while parsing a pylint configuration file.",
  119. {"scope": WarningScope.LINE},
  120. ),
  121. "I0001": (
  122. "Unable to run raw checkers on built-in module %s",
  123. "raw-checker-failed",
  124. "Used to inform that a built-in module has not been checked "
  125. "using the raw checkers.",
  126. {"scope": WarningScope.LINE},
  127. ),
  128. "I0010": (
  129. "Unable to consider inline option %r",
  130. "bad-inline-option",
  131. "Used when an inline option is either badly formatted or can't "
  132. "be used inside modules.",
  133. {"scope": WarningScope.LINE},
  134. ),
  135. "I0011": (
  136. "Locally disabling %s (%s)",
  137. "locally-disabled",
  138. "Used when an inline option disables a message or a messages category.",
  139. {"scope": WarningScope.LINE},
  140. ),
  141. "I0013": (
  142. "Ignoring entire file",
  143. "file-ignored",
  144. "Used to inform that the file will not be checked",
  145. {"scope": WarningScope.LINE},
  146. ),
  147. "I0020": (
  148. "Suppressed %s (from line %d)",
  149. "suppressed-message",
  150. "A message was triggered on a line, but suppressed explicitly "
  151. "by a disable= comment in the file. This message is not "
  152. "generated for messages that are ignored due to configuration "
  153. "settings.",
  154. {"scope": WarningScope.LINE},
  155. ),
  156. "I0021": (
  157. "Useless suppression of %s",
  158. "useless-suppression",
  159. "Reported when a message is explicitly disabled for a line or "
  160. "a block of code, but never triggered.",
  161. {"scope": WarningScope.LINE},
  162. ),
  163. "I0022": (
  164. 'Pragma "%s" is deprecated, use "%s" instead',
  165. "deprecated-pragma",
  166. "Some inline pylint options have been renamed or reworked, "
  167. "only the most recent form should be used. "
  168. "NOTE:skip-all is only available with pylint >= 0.26",
  169. {
  170. "old_names": [("I0014", "deprecated-disable-all")],
  171. "scope": WarningScope.LINE,
  172. },
  173. ),
  174. "E0001": (
  175. "%s",
  176. "syntax-error",
  177. "Used when a syntax error is raised for a module.",
  178. {"scope": WarningScope.LINE},
  179. ),
  180. "E0011": (
  181. "Unrecognized file option %r",
  182. "unrecognized-inline-option",
  183. "Used when an unknown inline option is encountered.",
  184. {"scope": WarningScope.LINE},
  185. ),
  186. "W0012": (
  187. "Unknown option value for '%s', expected a valid pylint message and got '%s'",
  188. "unknown-option-value",
  189. "Used when an unknown value is encountered for an option.",
  190. {
  191. "scope": WarningScope.LINE,
  192. "old_names": [("E0012", "bad-option-value")],
  193. },
  194. ),
  195. "R0022": (
  196. "Useless option value for '%s', %s",
  197. "useless-option-value",
  198. "Used when a value for an option that is now deleted from pylint"
  199. " is encountered.",
  200. {
  201. "scope": WarningScope.LINE,
  202. "old_names": [("E0012", "bad-option-value")],
  203. },
  204. ),
  205. "E0013": (
  206. "Plugin '%s' is impossible to load, is it installed ? ('%s')",
  207. "bad-plugin-value",
  208. "Used when a bad value is used in 'load-plugins'.",
  209. {"scope": WarningScope.LINE},
  210. ),
  211. "E0014": (
  212. "Out-of-place setting encountered in top level configuration-section '%s' : '%s'",
  213. "bad-configuration-section",
  214. "Used when we detect a setting in the top level of a toml configuration that"
  215. " shouldn't be there.",
  216. {"scope": WarningScope.LINE},
  217. ),
  218. "E0015": (
  219. "Unrecognized option found: %s",
  220. "unrecognized-option",
  221. "Used when we detect an option that we do not recognize.",
  222. {"scope": WarningScope.LINE},
  223. ),
  224. }
  225. # pylint: disable=too-many-instance-attributes,too-many-public-methods
  226. class PyLinter(
  227. _ArgumentsManager,
  228. _MessageStateHandler,
  229. reporters.ReportsHandlerMixIn,
  230. checkers.BaseChecker,
  231. ):
  232. """Lint Python modules using external checkers.
  233. This is the main checker controlling the other ones and the reports
  234. generation. It is itself both a raw checker and an astroid checker in order
  235. to:
  236. * handle message activation / deactivation at the module level
  237. * handle some basic but necessary stats' data (number of classes, methods...)
  238. IDE plugin developers: you may have to call
  239. `astroid.MANAGER.clear_cache()` across runs if you want
  240. to ensure the latest code version is actually checked.
  241. This class needs to support pickling for parallel linting to work. The exception
  242. is reporter member; see check_parallel function for more details.
  243. """
  244. name = MAIN_CHECKER_NAME
  245. msgs = MSGS
  246. # Will be used like this : datetime.now().strftime(crash_file_path)
  247. crash_file_path: str = "pylint-crash-%Y-%m-%d-%H-%M-%S.txt"
  248. option_groups_descs = {
  249. "Messages control": "Options controlling analysis messages",
  250. "Reports": "Options related to output formatting and reporting",
  251. }
  252. def __init__(
  253. self,
  254. options: Options = (),
  255. reporter: reporters.BaseReporter | reporters.MultiReporter | None = None,
  256. option_groups: tuple[tuple[str, str], ...] = (),
  257. # TODO: Deprecate passing the pylintrc parameter
  258. pylintrc: str | None = None, # pylint: disable=unused-argument
  259. ) -> None:
  260. _ArgumentsManager.__init__(self, prog="pylint")
  261. _MessageStateHandler.__init__(self, self)
  262. # Some stuff has to be done before initialization of other ancestors...
  263. # messages store / checkers / reporter / astroid manager
  264. # Attributes for reporters
  265. self.reporter: reporters.BaseReporter | reporters.MultiReporter
  266. if reporter:
  267. self.set_reporter(reporter)
  268. else:
  269. self.set_reporter(TextReporter())
  270. self._reporters: dict[str, type[reporters.BaseReporter]] = {}
  271. """Dictionary of possible but non-initialized reporters."""
  272. # Attributes for checkers and plugins
  273. self._checkers: defaultdict[
  274. str, list[checkers.BaseChecker]
  275. ] = collections.defaultdict(list)
  276. """Dictionary of registered and initialized checkers."""
  277. self._dynamic_plugins: dict[str, ModuleType | ModuleNotFoundError | bool] = {}
  278. """Set of loaded plugin names."""
  279. # Attributes related to stats
  280. self.stats = LinterStats()
  281. # Attributes related to (command-line) options and their parsing
  282. self.options: Options = options + _make_linter_options(self)
  283. for opt_group in option_groups:
  284. self.option_groups_descs[opt_group[0]] = opt_group[1]
  285. self._option_groups: tuple[tuple[str, str], ...] = option_groups + (
  286. ("Messages control", "Options controlling analysis messages"),
  287. ("Reports", "Options related to output formatting and reporting"),
  288. )
  289. self.fail_on_symbols: list[str] = []
  290. """List of message symbols on which pylint should fail, set by --fail-on."""
  291. self._error_mode = False
  292. reporters.ReportsHandlerMixIn.__init__(self)
  293. checkers.BaseChecker.__init__(self, self)
  294. # provided reports
  295. self.reports = (
  296. ("RP0001", "Messages by category", report_total_messages_stats),
  297. (
  298. "RP0002",
  299. "% errors / warnings by module",
  300. report_messages_by_module_stats,
  301. ),
  302. ("RP0003", "Messages", report_messages_stats),
  303. )
  304. # Attributes related to registering messages and their handling
  305. self.msgs_store = MessageDefinitionStore(self.config.py_version)
  306. self.msg_status = 0
  307. self._by_id_managed_msgs: list[ManagedMessage] = []
  308. # Attributes related to visiting files
  309. self.file_state = FileState("", self.msgs_store, is_base_filestate=True)
  310. self.current_name: str | None = None
  311. self.current_file: str | None = None
  312. self._ignore_file = False
  313. self._ignore_paths: list[Pattern[str]] = []
  314. self.register_checker(self)
  315. @property
  316. def option_groups(self) -> tuple[tuple[str, str], ...]:
  317. # TODO: 3.0: Remove deprecated attribute
  318. warnings.warn(
  319. "The option_groups attribute has been deprecated and will be removed in pylint 3.0",
  320. DeprecationWarning,
  321. stacklevel=2,
  322. )
  323. return self._option_groups
  324. @option_groups.setter
  325. def option_groups(self, value: tuple[tuple[str, str], ...]) -> None:
  326. warnings.warn(
  327. "The option_groups attribute has been deprecated and will be removed in pylint 3.0",
  328. DeprecationWarning,
  329. stacklevel=2,
  330. )
  331. self._option_groups = value
  332. def load_default_plugins(self) -> None:
  333. checkers.initialize(self)
  334. reporters.initialize(self)
  335. def load_plugin_modules(self, modnames: list[str]) -> None:
  336. """Check a list of pylint plugins modules, load and register them.
  337. If a module cannot be loaded, never try to load it again and instead
  338. store the error message for later use in ``load_plugin_configuration``
  339. below.
  340. """
  341. for modname in modnames:
  342. if modname in self._dynamic_plugins:
  343. continue
  344. try:
  345. module = astroid.modutils.load_module_from_name(modname)
  346. module.register(self)
  347. self._dynamic_plugins[modname] = module
  348. except ModuleNotFoundError as mnf_e:
  349. self._dynamic_plugins[modname] = mnf_e
  350. def load_plugin_configuration(self) -> None:
  351. """Call the configuration hook for plugins.
  352. This walks through the list of plugins, grabs the "load_configuration"
  353. hook, if exposed, and calls it to allow plugins to configure specific
  354. settings.
  355. The result of attempting to load the plugin of the given name
  356. is stored in the dynamic plugins dictionary in ``load_plugin_modules`` above.
  357. ..note::
  358. This function previously always tried to load modules again, which
  359. led to some confusion and silent failure conditions as described
  360. in GitHub issue #7264. Making it use the stored result is more efficient, and
  361. means that we avoid the ``init-hook`` problems from before.
  362. """
  363. for modname, module_or_error in self._dynamic_plugins.items():
  364. if isinstance(module_or_error, ModuleNotFoundError):
  365. self.add_message(
  366. "bad-plugin-value", args=(modname, module_or_error), line=0
  367. )
  368. elif hasattr(module_or_error, "load_configuration"):
  369. module_or_error.load_configuration(self)
  370. # We re-set all the dictionary values to True here to make sure the dict
  371. # is pickle-able. This is only a problem in multiprocessing/parallel mode.
  372. # (e.g. invoking pylint -j 2)
  373. self._dynamic_plugins = {
  374. modname: not isinstance(val, ModuleNotFoundError)
  375. for modname, val in self._dynamic_plugins.items()
  376. }
  377. def _load_reporters(self, reporter_names: str) -> None:
  378. """Load the reporters if they are available on _reporters."""
  379. if not self._reporters:
  380. return
  381. sub_reporters = []
  382. output_files = []
  383. with contextlib.ExitStack() as stack:
  384. for reporter_name in reporter_names.split(","):
  385. reporter_name, *reporter_output = reporter_name.split(":", 1)
  386. reporter = self._load_reporter_by_name(reporter_name)
  387. sub_reporters.append(reporter)
  388. if reporter_output:
  389. output_file = stack.enter_context(
  390. open(reporter_output[0], "w", encoding="utf-8")
  391. )
  392. reporter.out = output_file
  393. output_files.append(output_file)
  394. # Extend the lifetime of all opened output files
  395. close_output_files = stack.pop_all().close
  396. if len(sub_reporters) > 1 or output_files:
  397. self.set_reporter(
  398. reporters.MultiReporter(
  399. sub_reporters,
  400. close_output_files,
  401. )
  402. )
  403. else:
  404. self.set_reporter(sub_reporters[0])
  405. def _load_reporter_by_name(self, reporter_name: str) -> reporters.BaseReporter:
  406. name = reporter_name.lower()
  407. if name in self._reporters:
  408. return self._reporters[name]()
  409. try:
  410. reporter_class = _load_reporter_by_class(reporter_name)
  411. except (ImportError, AttributeError, AssertionError) as e:
  412. raise exceptions.InvalidReporterError(name) from e
  413. return reporter_class()
  414. def set_reporter(
  415. self, reporter: reporters.BaseReporter | reporters.MultiReporter
  416. ) -> None:
  417. """Set the reporter used to display messages and reports."""
  418. self.reporter = reporter
  419. reporter.linter = self
  420. def register_reporter(self, reporter_class: type[reporters.BaseReporter]) -> None:
  421. """Registers a reporter class on the _reporters attribute."""
  422. self._reporters[reporter_class.name] = reporter_class
  423. def report_order(self) -> list[BaseChecker]:
  424. reports = sorted(self._reports, key=lambda x: getattr(x, "name", ""))
  425. try:
  426. # Remove the current reporter and add it
  427. # at the end of the list.
  428. reports.pop(reports.index(self))
  429. except ValueError:
  430. pass
  431. else:
  432. reports.append(self)
  433. return reports
  434. # checkers manipulation methods ############################################
  435. def register_checker(self, checker: checkers.BaseChecker) -> None:
  436. """This method auto registers the checker."""
  437. self._checkers[checker.name].append(checker)
  438. for r_id, r_title, r_cb in checker.reports:
  439. self.register_report(r_id, r_title, r_cb, checker)
  440. if hasattr(checker, "msgs"):
  441. self.msgs_store.register_messages_from_checker(checker)
  442. for message in checker.messages:
  443. if not message.default_enabled:
  444. self.disable(message.msgid)
  445. # Register the checker, but disable all of its messages.
  446. if not getattr(checker, "enabled", True):
  447. self.disable(checker.name)
  448. def enable_fail_on_messages(self) -> None:
  449. """Enable 'fail on' msgs.
  450. Convert values in config.fail_on (which might be msg category, msg id,
  451. or symbol) to specific msgs, then enable and flag them for later.
  452. """
  453. fail_on_vals = self.config.fail_on
  454. if not fail_on_vals:
  455. return
  456. fail_on_cats = set()
  457. fail_on_msgs = set()
  458. for val in fail_on_vals:
  459. # If value is a category, add category, else add message
  460. if val in MSG_TYPES:
  461. fail_on_cats.add(val)
  462. else:
  463. fail_on_msgs.add(val)
  464. # For every message in every checker, if cat or msg flagged, enable check
  465. for all_checkers in self._checkers.values():
  466. for checker in all_checkers:
  467. for msg in checker.messages:
  468. if msg.msgid in fail_on_msgs or msg.symbol in fail_on_msgs:
  469. # message id/symbol matched, enable and flag it
  470. self.enable(msg.msgid)
  471. self.fail_on_symbols.append(msg.symbol)
  472. elif msg.msgid[0] in fail_on_cats:
  473. # message starts with a category value, flag (but do not enable) it
  474. self.fail_on_symbols.append(msg.symbol)
  475. def any_fail_on_issues(self) -> bool:
  476. return any(x in self.fail_on_symbols for x in self.stats.by_msg.keys())
  477. def disable_reporters(self) -> None:
  478. """Disable all reporters."""
  479. for _reporters in self._reports.values():
  480. for report_id, _, _ in _reporters:
  481. self.disable_report(report_id)
  482. def _parse_error_mode(self) -> None:
  483. """Parse the current state of the error mode.
  484. Error mode: enable only errors; no reports, no persistent.
  485. """
  486. if not self._error_mode:
  487. return
  488. self.disable_noerror_messages()
  489. self.disable("miscellaneous")
  490. self.set_option("reports", False)
  491. self.set_option("persistent", False)
  492. self.set_option("score", False)
  493. # code checking methods ###################################################
  494. def get_checkers(self) -> list[BaseChecker]:
  495. """Return all available checkers as an ordered list."""
  496. return sorted(c for _checkers in self._checkers.values() for c in _checkers)
  497. def get_checker_names(self) -> list[str]:
  498. """Get all the checker names that this linter knows about."""
  499. return sorted(
  500. {
  501. checker.name
  502. for checker in self.get_checkers()
  503. if checker.name != MAIN_CHECKER_NAME
  504. }
  505. )
  506. def prepare_checkers(self) -> list[BaseChecker]:
  507. """Return checkers needed for activated messages and reports."""
  508. if not self.config.reports:
  509. self.disable_reporters()
  510. # get needed checkers
  511. needed_checkers: list[BaseChecker] = [self]
  512. for checker in self.get_checkers()[1:]:
  513. messages = {msg for msg in checker.msgs if self.is_message_enabled(msg)}
  514. if messages or any(self.report_is_enabled(r[0]) for r in checker.reports):
  515. needed_checkers.append(checker)
  516. return needed_checkers
  517. # pylint: disable=unused-argument
  518. @staticmethod
  519. def should_analyze_file(modname: str, path: str, is_argument: bool = False) -> bool:
  520. """Returns whether a module should be checked.
  521. This implementation returns True for all python source file, indicating
  522. that all files should be linted.
  523. Subclasses may override this method to indicate that modules satisfying
  524. certain conditions should not be linted.
  525. :param str modname: The name of the module to be checked.
  526. :param str path: The full path to the source code of the module.
  527. :param bool is_argument: Whether the file is an argument to pylint or not.
  528. Files which respect this property are always
  529. checked, since the user requested it explicitly.
  530. :returns: True if the module should be checked.
  531. """
  532. if is_argument:
  533. return True
  534. return path.endswith(".py")
  535. # pylint: enable=unused-argument
  536. def initialize(self) -> None:
  537. """Initialize linter for linting.
  538. This method is called before any linting is done.
  539. """
  540. self._ignore_paths = self.config.ignore_paths
  541. # initialize msgs_state now that all messages have been registered into
  542. # the store
  543. for msg in self.msgs_store.messages:
  544. if not msg.may_be_emitted(self.config.py_version):
  545. self._msgs_state[msg.msgid] = False
  546. def _discover_files(self, files_or_modules: Sequence[str]) -> Iterator[str]:
  547. """Discover python modules and packages in sub-directory.
  548. Returns iterator of paths to discovered modules and packages.
  549. """
  550. for something in files_or_modules:
  551. if os.path.isdir(something) and not os.path.isfile(
  552. os.path.join(something, "__init__.py")
  553. ):
  554. skip_subtrees: list[str] = []
  555. for root, _, files in os.walk(something):
  556. if any(root.startswith(s) for s in skip_subtrees):
  557. # Skip subtree of already discovered package.
  558. continue
  559. if _is_ignored_file(
  560. root,
  561. self.config.ignore,
  562. self.config.ignore_patterns,
  563. self.config.ignore_paths,
  564. ):
  565. skip_subtrees.append(root)
  566. continue
  567. if "__init__.py" in files:
  568. skip_subtrees.append(root)
  569. yield root
  570. else:
  571. yield from (
  572. os.path.join(root, file)
  573. for file in files
  574. if file.endswith(".py")
  575. )
  576. else:
  577. yield something
  578. def check(self, files_or_modules: Sequence[str] | str) -> None:
  579. """Main checking entry: check a list of files or modules from their name.
  580. files_or_modules is either a string or list of strings presenting modules to check.
  581. """
  582. # 1) Initialize
  583. self.initialize()
  584. # 2) Gather all files
  585. if not isinstance(files_or_modules, (list, tuple)):
  586. # TODO: 3.0: Remove deprecated typing and update docstring
  587. warnings.warn(
  588. "In pylint 3.0, the checkers check function will only accept sequence of string",
  589. DeprecationWarning,
  590. stacklevel=2,
  591. )
  592. files_or_modules = (files_or_modules,) # type: ignore[assignment]
  593. if self.config.recursive:
  594. files_or_modules = tuple(self._discover_files(files_or_modules))
  595. if self.config.from_stdin:
  596. if len(files_or_modules) != 1:
  597. raise exceptions.InvalidArgsError(
  598. "Missing filename required for --from-stdin"
  599. )
  600. extra_packages_paths = list(
  601. {
  602. discover_package_path(file_or_module, self.config.source_roots)
  603. for file_or_module in files_or_modules
  604. }
  605. )
  606. # TODO: Move the parallel invocation into step 5 of the checking process
  607. if not self.config.from_stdin and self.config.jobs > 1:
  608. original_sys_path = sys.path[:]
  609. check_parallel(
  610. self,
  611. self.config.jobs,
  612. self._iterate_file_descrs(files_or_modules),
  613. extra_packages_paths,
  614. )
  615. sys.path = original_sys_path
  616. return
  617. # 3) Get all FileItems
  618. with augmented_sys_path(extra_packages_paths):
  619. if self.config.from_stdin:
  620. fileitems = self._get_file_descr_from_stdin(files_or_modules[0])
  621. data: str | None = _read_stdin()
  622. else:
  623. fileitems = self._iterate_file_descrs(files_or_modules)
  624. data = None
  625. # The contextmanager also opens all checkers and sets up the PyLinter class
  626. with augmented_sys_path(extra_packages_paths):
  627. with self._astroid_module_checker() as check_astroid_module:
  628. # 4) Get the AST for each FileItem
  629. ast_per_fileitem = self._get_asts(fileitems, data)
  630. # 5) Lint each ast
  631. self._lint_files(ast_per_fileitem, check_astroid_module)
  632. def _get_asts(
  633. self, fileitems: Iterator[FileItem], data: str | None
  634. ) -> dict[FileItem, nodes.Module | None]:
  635. """Get the AST for all given FileItems."""
  636. ast_per_fileitem: dict[FileItem, nodes.Module | None] = {}
  637. for fileitem in fileitems:
  638. self.set_current_module(fileitem.name, fileitem.filepath)
  639. try:
  640. ast_per_fileitem[fileitem] = self.get_ast(
  641. fileitem.filepath, fileitem.name, data
  642. )
  643. except astroid.AstroidBuildingError as ex:
  644. template_path = prepare_crash_report(
  645. ex, fileitem.filepath, self.crash_file_path
  646. )
  647. msg = get_fatal_error_message(fileitem.filepath, template_path)
  648. self.add_message(
  649. "astroid-error",
  650. args=(fileitem.filepath, msg),
  651. confidence=HIGH,
  652. )
  653. return ast_per_fileitem
  654. def check_single_file(self, name: str, filepath: str, modname: str) -> None:
  655. warnings.warn(
  656. "In pylint 3.0, the checkers check_single_file function will be removed. "
  657. "Use check_single_file_item instead.",
  658. DeprecationWarning,
  659. stacklevel=2,
  660. )
  661. self.check_single_file_item(FileItem(name, filepath, modname))
  662. def check_single_file_item(self, file: FileItem) -> None:
  663. """Check single file item.
  664. The arguments are the same that are documented in _check_files
  665. initialize() should be called before calling this method
  666. """
  667. with self._astroid_module_checker() as check_astroid_module:
  668. self._check_file(self.get_ast, check_astroid_module, file)
  669. def _lint_files(
  670. self,
  671. ast_mapping: dict[FileItem, nodes.Module | None],
  672. check_astroid_module: Callable[[nodes.Module], bool | None],
  673. ) -> None:
  674. """Lint all AST modules from a mapping.."""
  675. for fileitem, module in ast_mapping.items():
  676. if module is None:
  677. continue
  678. try:
  679. self._lint_file(fileitem, module, check_astroid_module)
  680. except Exception as ex: # pylint: disable=broad-except
  681. template_path = prepare_crash_report(
  682. ex, fileitem.filepath, self.crash_file_path
  683. )
  684. msg = get_fatal_error_message(fileitem.filepath, template_path)
  685. if isinstance(ex, astroid.AstroidError):
  686. self.add_message(
  687. "astroid-error", args=(fileitem.filepath, msg), confidence=HIGH
  688. )
  689. else:
  690. self.add_message("fatal", args=msg, confidence=HIGH)
  691. def _lint_file(
  692. self,
  693. file: FileItem,
  694. module: nodes.Module,
  695. check_astroid_module: Callable[[nodes.Module], bool | None],
  696. ) -> None:
  697. """Lint a file using the passed utility function check_astroid_module).
  698. :param FileItem file: data about the file
  699. :param nodes.Module module: the ast module to lint
  700. :param Callable check_astroid_module: callable checking an AST taking the following
  701. arguments
  702. - ast: AST of the module
  703. :raises AstroidError: for any failures stemming from astroid
  704. """
  705. self.set_current_module(file.name, file.filepath)
  706. self._ignore_file = False
  707. self.file_state = FileState(file.modpath, self.msgs_store, module)
  708. # fix the current file (if the source file was not available or
  709. # if it's actually a c extension)
  710. self.current_file = module.file
  711. try:
  712. check_astroid_module(module)
  713. except Exception as e:
  714. raise astroid.AstroidError from e
  715. # warn about spurious inline messages handling
  716. spurious_messages = self.file_state.iter_spurious_suppression_messages(
  717. self.msgs_store
  718. )
  719. for msgid, line, args in spurious_messages:
  720. self.add_message(msgid, line, None, args)
  721. def _check_file(
  722. self,
  723. get_ast: GetAstProtocol,
  724. check_astroid_module: Callable[[nodes.Module], bool | None],
  725. file: FileItem,
  726. ) -> None:
  727. """Check a file using the passed utility functions (get_ast and
  728. check_astroid_module).
  729. :param callable get_ast: callable returning AST from defined file taking the
  730. following arguments
  731. - filepath: path to the file to check
  732. - name: Python module name
  733. :param callable check_astroid_module: callable checking an AST taking the following
  734. arguments
  735. - ast: AST of the module
  736. :param FileItem file: data about the file
  737. :raises AstroidError: for any failures stemming from astroid
  738. """
  739. self.set_current_module(file.name, file.filepath)
  740. # get the module representation
  741. ast_node = get_ast(file.filepath, file.name)
  742. if ast_node is None:
  743. return
  744. self._ignore_file = False
  745. self.file_state = FileState(file.modpath, self.msgs_store, ast_node)
  746. # fix the current file (if the source file was not available or
  747. # if it's actually a c extension)
  748. self.current_file = ast_node.file
  749. try:
  750. check_astroid_module(ast_node)
  751. except Exception as e: # pragma: no cover
  752. raise astroid.AstroidError from e
  753. # warn about spurious inline messages handling
  754. spurious_messages = self.file_state.iter_spurious_suppression_messages(
  755. self.msgs_store
  756. )
  757. for msgid, line, args in spurious_messages:
  758. self.add_message(msgid, line, None, args)
  759. def _get_file_descr_from_stdin(self, filepath: str) -> Iterator[FileItem]:
  760. """Return file description (tuple of module name, file path, base name) from
  761. given file path.
  762. This method is used for creating suitable file description for _check_files when the
  763. source is standard input.
  764. """
  765. if _is_ignored_file(
  766. filepath,
  767. self.config.ignore,
  768. self.config.ignore_patterns,
  769. self.config.ignore_paths,
  770. ):
  771. return
  772. try:
  773. # Note that this function does not really perform an
  774. # __import__ but may raise an ImportError exception, which
  775. # we want to catch here.
  776. modname = ".".join(astroid.modutils.modpath_from_file(filepath))
  777. except ImportError:
  778. modname = os.path.splitext(os.path.basename(filepath))[0]
  779. yield FileItem(modname, filepath, filepath)
  780. def _iterate_file_descrs(
  781. self, files_or_modules: Sequence[str]
  782. ) -> Iterator[FileItem]:
  783. """Return generator yielding file descriptions (tuples of module name, file
  784. path, base name).
  785. The returned generator yield one item for each Python module that should be linted.
  786. """
  787. for descr in self._expand_files(files_or_modules).values():
  788. name, filepath, is_arg = descr["name"], descr["path"], descr["isarg"]
  789. if self.should_analyze_file(name, filepath, is_argument=is_arg):
  790. yield FileItem(name, filepath, descr["basename"])
  791. def _expand_files(
  792. self, files_or_modules: Sequence[str]
  793. ) -> dict[str, ModuleDescriptionDict]:
  794. """Get modules and errors from a list of modules and handle errors."""
  795. result, errors = expand_modules(
  796. files_or_modules,
  797. self.config.source_roots,
  798. self.config.ignore,
  799. self.config.ignore_patterns,
  800. self._ignore_paths,
  801. )
  802. for error in errors:
  803. message = modname = error["mod"]
  804. key = error["key"]
  805. self.set_current_module(modname)
  806. if key == "fatal":
  807. message = str(error["ex"]).replace(os.getcwd() + os.sep, "")
  808. self.add_message(key, args=message)
  809. return result
  810. def set_current_module(
  811. self, modname: str | None, filepath: str | None = None
  812. ) -> None:
  813. """Set the name of the currently analyzed module and
  814. init statistics for it.
  815. """
  816. if not modname and filepath is None:
  817. return
  818. self.reporter.on_set_current_module(modname or "", filepath)
  819. if modname is None:
  820. # TODO: 3.0: Remove all modname or ""'s in this method
  821. warnings.warn(
  822. (
  823. "In pylint 3.0 modname should be a string so that it can be used to "
  824. "correctly set the current_name attribute of the linter instance. "
  825. "If unknown it should be initialized as an empty string."
  826. ),
  827. DeprecationWarning,
  828. stacklevel=2,
  829. )
  830. self.current_name = modname
  831. self.current_file = filepath or modname
  832. self.stats.init_single_module(modname or "")
  833. # If there is an actual filepath we might need to update the config attribute
  834. if filepath:
  835. namespace = self._get_namespace_for_file(
  836. Path(filepath), self._directory_namespaces
  837. )
  838. if namespace:
  839. self.config = namespace or self._base_config
  840. def _get_namespace_for_file(
  841. self, filepath: Path, namespaces: DirectoryNamespaceDict
  842. ) -> argparse.Namespace | None:
  843. for directory in namespaces:
  844. if _is_relative_to(filepath, directory):
  845. namespace = self._get_namespace_for_file(
  846. filepath, namespaces[directory][1]
  847. )
  848. if namespace is None:
  849. return namespaces[directory][0]
  850. return None
  851. @contextlib.contextmanager
  852. def _astroid_module_checker(
  853. self,
  854. ) -> Iterator[Callable[[nodes.Module], bool | None]]:
  855. """Context manager for checking ASTs.
  856. The value in the context is callable accepting AST as its only argument.
  857. """
  858. walker = ASTWalker(self)
  859. _checkers = self.prepare_checkers()
  860. tokencheckers = [
  861. c for c in _checkers if isinstance(c, checkers.BaseTokenChecker)
  862. ]
  863. # TODO: 3.0: Remove deprecated for-loop
  864. for c in _checkers:
  865. with warnings.catch_warnings():
  866. warnings.filterwarnings("ignore", category=DeprecationWarning)
  867. if (
  868. interfaces.implements(c, interfaces.ITokenChecker)
  869. and c not in tokencheckers
  870. and c is not self
  871. ):
  872. tokencheckers.append(c) # type: ignore[arg-type] # pragma: no cover
  873. warnings.warn( # pragma: no cover
  874. "Checkers should subclass BaseTokenChecker "
  875. "instead of using the __implements__ mechanism. Use of __implements__ "
  876. "will no longer be supported in pylint 3.0",
  877. DeprecationWarning,
  878. )
  879. rawcheckers = [
  880. c for c in _checkers if isinstance(c, checkers.BaseRawFileChecker)
  881. ]
  882. # TODO: 3.0: Remove deprecated if-statement
  883. for c in _checkers:
  884. with warnings.catch_warnings():
  885. warnings.filterwarnings("ignore", category=DeprecationWarning)
  886. if (
  887. interfaces.implements(c, interfaces.IRawChecker)
  888. and c not in rawcheckers
  889. ):
  890. rawcheckers.append(c) # type: ignore[arg-type] # pragma: no cover
  891. warnings.warn( # pragma: no cover
  892. "Checkers should subclass BaseRawFileChecker "
  893. "instead of using the __implements__ mechanism. Use of __implements__ "
  894. "will no longer be supported in pylint 3.0",
  895. DeprecationWarning,
  896. )
  897. # notify global begin
  898. for checker in _checkers:
  899. checker.open()
  900. walker.add_checker(checker)
  901. yield functools.partial(
  902. self.check_astroid_module,
  903. walker=walker,
  904. tokencheckers=tokencheckers,
  905. rawcheckers=rawcheckers,
  906. )
  907. # notify global end
  908. self.stats.statement = walker.nbstatements
  909. for checker in reversed(_checkers):
  910. checker.close()
  911. def get_ast(
  912. self, filepath: str, modname: str, data: str | None = None
  913. ) -> nodes.Module | None:
  914. """Return an ast(roid) representation of a module or a string.
  915. :param filepath: path to checked file.
  916. :param str modname: The name of the module to be checked.
  917. :param str data: optional contents of the checked file.
  918. :returns: the AST
  919. :rtype: astroid.nodes.Module
  920. :raises AstroidBuildingError: Whenever we encounter an unexpected exception
  921. """
  922. try:
  923. if data is None:
  924. return MANAGER.ast_from_file(filepath, modname, source=True)
  925. return astroid.builder.AstroidBuilder(MANAGER).string_build(
  926. data, modname, filepath
  927. )
  928. except astroid.AstroidSyntaxError as ex:
  929. line = getattr(ex.error, "lineno", None)
  930. if line is None:
  931. line = 0
  932. self.add_message(
  933. "syntax-error",
  934. line=line,
  935. col_offset=getattr(ex.error, "offset", None),
  936. args=f"Parsing failed: '{ex.error}'",
  937. confidence=HIGH,
  938. )
  939. except astroid.AstroidBuildingError as ex:
  940. self.add_message("parse-error", args=ex)
  941. except Exception as ex:
  942. traceback.print_exc()
  943. # We raise BuildingError here as this is essentially an astroid issue
  944. # Creating an issue template and adding the 'astroid-error' message is handled
  945. # by caller: _check_files
  946. raise astroid.AstroidBuildingError(
  947. "Building error when trying to create ast representation of module '{modname}'",
  948. modname=modname,
  949. ) from ex
  950. return None
  951. def check_astroid_module(
  952. self,
  953. ast_node: nodes.Module,
  954. walker: ASTWalker,
  955. rawcheckers: list[checkers.BaseRawFileChecker],
  956. tokencheckers: list[checkers.BaseTokenChecker],
  957. ) -> bool | None:
  958. """Check a module from its astroid representation.
  959. For return value see _check_astroid_module
  960. """
  961. before_check_statements = walker.nbstatements
  962. retval = self._check_astroid_module(
  963. ast_node, walker, rawcheckers, tokencheckers
  964. )
  965. # TODO: 3.0: Remove unnecessary assertion
  966. assert self.current_name
  967. self.stats.by_module[self.current_name]["statement"] = (
  968. walker.nbstatements - before_check_statements
  969. )
  970. return retval
  971. def _check_astroid_module(
  972. self,
  973. node: nodes.Module,
  974. walker: ASTWalker,
  975. rawcheckers: list[checkers.BaseRawFileChecker],
  976. tokencheckers: list[checkers.BaseTokenChecker],
  977. ) -> bool | None:
  978. """Check given AST node with given walker and checkers.
  979. :param astroid.nodes.Module node: AST node of the module to check
  980. :param pylint.utils.ast_walker.ASTWalker walker: AST walker
  981. :param list rawcheckers: List of token checkers to use
  982. :param list tokencheckers: List of raw checkers to use
  983. :returns: True if the module was checked, False if ignored,
  984. None if the module contents could not be parsed
  985. """
  986. try:
  987. tokens = utils.tokenize_module(node)
  988. except tokenize.TokenError as ex:
  989. self.add_message("syntax-error", line=ex.args[1][0], args=ex.args[0])
  990. return None
  991. if not node.pure_python:
  992. self.add_message("raw-checker-failed", args=node.name)
  993. else:
  994. # assert astroid.file.endswith('.py')
  995. # Parse module/block level option pragma's
  996. self.process_tokens(tokens)
  997. if self._ignore_file:
  998. return False
  999. # run raw and tokens checkers
  1000. for raw_checker in rawcheckers:
  1001. raw_checker.process_module(node)
  1002. for token_checker in tokencheckers:
  1003. token_checker.process_tokens(tokens)
  1004. # generate events to astroid checkers
  1005. walker.walk(node)
  1006. return True
  1007. def open(self) -> None:
  1008. """Initialize counters."""
  1009. self.stats = LinterStats()
  1010. MANAGER.always_load_extensions = self.config.unsafe_load_any_extension
  1011. MANAGER.max_inferable_values = self.config.limit_inference_results
  1012. MANAGER.extension_package_whitelist.update(self.config.extension_pkg_allow_list)
  1013. if self.config.extension_pkg_whitelist:
  1014. MANAGER.extension_package_whitelist.update(
  1015. self.config.extension_pkg_whitelist
  1016. )
  1017. self.stats.reset_message_count()
  1018. def generate_reports(self) -> int | None:
  1019. """Close the whole package /module, it's time to make reports !
  1020. if persistent run, pickle results for later comparison
  1021. """
  1022. # Display whatever messages are left on the reporter.
  1023. self.reporter.display_messages(report_nodes.Section())
  1024. # TODO: 3.0: Remove second half of if-statement
  1025. if (
  1026. not self.file_state._is_base_filestate
  1027. and self.file_state.base_name is not None
  1028. ):
  1029. # load previous results if any
  1030. previous_stats = load_results(self.file_state.base_name)
  1031. self.reporter.on_close(self.stats, previous_stats)
  1032. if self.config.reports:
  1033. sect = self.make_reports(self.stats, previous_stats)
  1034. else:
  1035. sect = report_nodes.Section()
  1036. if self.config.reports:
  1037. self.reporter.display_reports(sect)
  1038. score_value = self._report_evaluation()
  1039. # save results if persistent run
  1040. if self.config.persistent:
  1041. save_results(self.stats, self.file_state.base_name)
  1042. else:
  1043. self.reporter.on_close(self.stats, LinterStats())
  1044. score_value = None
  1045. return score_value
  1046. def _report_evaluation(self) -> int | None:
  1047. """Make the global evaluation report."""
  1048. # check with at least check 1 statements (usually 0 when there is a
  1049. # syntax error preventing pylint from further processing)
  1050. note = None
  1051. # TODO: 3.0: Remove assertion
  1052. assert self.file_state.base_name is not None
  1053. previous_stats = load_results(self.file_state.base_name)
  1054. if self.stats.statement == 0:
  1055. return note
  1056. # get a global note for the code
  1057. evaluation = self.config.evaluation
  1058. try:
  1059. stats_dict = {
  1060. "fatal": self.stats.fatal,
  1061. "error": self.stats.error,
  1062. "warning": self.stats.warning,
  1063. "refactor": self.stats.refactor,
  1064. "convention": self.stats.convention,
  1065. "statement": self.stats.statement,
  1066. "info": self.stats.info,
  1067. }
  1068. note = eval(evaluation, {}, stats_dict) # pylint: disable=eval-used
  1069. except Exception as ex: # pylint: disable=broad-except
  1070. msg = f"An exception occurred while rating: {ex}"
  1071. else:
  1072. self.stats.global_note = note
  1073. msg = f"Your code has been rated at {note:.2f}/10"
  1074. if previous_stats:
  1075. pnote = previous_stats.global_note
  1076. if pnote is not None:
  1077. msg += f" (previous run: {pnote:.2f}/10, {note - pnote:+.2f})"
  1078. if self.config.score:
  1079. sect = report_nodes.EvaluationSection(msg)
  1080. self.reporter.display_reports(sect)
  1081. return note
  1082. def _add_one_message(
  1083. self,
  1084. message_definition: MessageDefinition,
  1085. line: int | None,
  1086. node: nodes.NodeNG | None,
  1087. args: Any | None,
  1088. confidence: interfaces.Confidence | None,
  1089. col_offset: int | None,
  1090. end_lineno: int | None,
  1091. end_col_offset: int | None,
  1092. ) -> None:
  1093. """After various checks have passed a single Message is
  1094. passed to the reporter and added to stats.
  1095. """
  1096. message_definition.check_message_definition(line, node)
  1097. # Look up "location" data of node if not yet supplied
  1098. if node:
  1099. if node.position:
  1100. if not line:
  1101. line = node.position.lineno
  1102. if not col_offset:
  1103. col_offset = node.position.col_offset
  1104. if not end_lineno:
  1105. end_lineno = node.position.end_lineno
  1106. if not end_col_offset:
  1107. end_col_offset = node.position.end_col_offset
  1108. else:
  1109. if not line:
  1110. line = node.fromlineno
  1111. if not col_offset:
  1112. col_offset = node.col_offset
  1113. if not end_lineno:
  1114. end_lineno = node.end_lineno
  1115. if not end_col_offset:
  1116. end_col_offset = node.end_col_offset
  1117. # should this message be displayed
  1118. if not self.is_message_enabled(message_definition.msgid, line, confidence):
  1119. self.file_state.handle_ignored_message(
  1120. self._get_message_state_scope(
  1121. message_definition.msgid, line, confidence
  1122. ),
  1123. message_definition.msgid,
  1124. line,
  1125. )
  1126. return
  1127. # update stats
  1128. msg_cat = MSG_TYPES[message_definition.msgid[0]]
  1129. self.msg_status |= MSG_TYPES_STATUS[message_definition.msgid[0]]
  1130. self.stats.increase_single_message_count(msg_cat, 1)
  1131. # TODO: 3.0 Should be removable after https://github.com/pylint-dev/pylint/pull/5580
  1132. self.stats.increase_single_module_message_count(
  1133. self.current_name, # type: ignore[arg-type]
  1134. msg_cat,
  1135. 1,
  1136. )
  1137. try:
  1138. self.stats.by_msg[message_definition.symbol] += 1
  1139. except KeyError:
  1140. self.stats.by_msg[message_definition.symbol] = 1
  1141. # Interpolate arguments into message string
  1142. msg = message_definition.msg
  1143. if args is not None:
  1144. msg %= args
  1145. # get module and object
  1146. if node is None:
  1147. module, obj = self.current_name, ""
  1148. abspath = self.current_file
  1149. else:
  1150. module, obj = utils.get_module_and_frameid(node)
  1151. abspath = node.root().file
  1152. if abspath is not None:
  1153. path = abspath.replace(self.reporter.path_strip_prefix, "", 1)
  1154. else:
  1155. path = "configuration"
  1156. # add the message
  1157. self.reporter.handle_message(
  1158. Message(
  1159. message_definition.msgid,
  1160. message_definition.symbol,
  1161. MessageLocationTuple(
  1162. abspath or "",
  1163. path,
  1164. module or "",
  1165. obj,
  1166. line or 1,
  1167. col_offset or 0,
  1168. end_lineno,
  1169. end_col_offset,
  1170. ),
  1171. msg,
  1172. confidence,
  1173. )
  1174. )
  1175. def add_message(
  1176. self,
  1177. msgid: str,
  1178. line: int | None = None,
  1179. node: nodes.NodeNG | None = None,
  1180. args: Any | None = None,
  1181. confidence: interfaces.Confidence | None = None,
  1182. col_offset: int | None = None,
  1183. end_lineno: int | None = None,
  1184. end_col_offset: int | None = None,
  1185. ) -> None:
  1186. """Adds a message given by ID or name.
  1187. If provided, the message string is expanded using args.
  1188. AST checkers must provide the node argument (but may optionally
  1189. provide line if the line number is different), raw and token checkers
  1190. must provide the line argument.
  1191. """
  1192. if confidence is None:
  1193. confidence = interfaces.UNDEFINED
  1194. message_definitions = self.msgs_store.get_message_definitions(msgid)
  1195. for message_definition in message_definitions:
  1196. self._add_one_message(
  1197. message_definition,
  1198. line,
  1199. node,
  1200. args,
  1201. confidence,
  1202. col_offset,
  1203. end_lineno,
  1204. end_col_offset,
  1205. )
  1206. def add_ignored_message(
  1207. self,
  1208. msgid: str,
  1209. line: int,
  1210. node: nodes.NodeNG | None = None,
  1211. confidence: interfaces.Confidence | None = interfaces.UNDEFINED,
  1212. ) -> None:
  1213. """Prepares a message to be added to the ignored message storage.
  1214. Some checks return early in special cases and never reach add_message(),
  1215. even though they would normally issue a message.
  1216. This creates false positives for useless-suppression.
  1217. This function avoids this by adding those message to the ignored msgs attribute
  1218. """
  1219. message_definitions = self.msgs_store.get_message_definitions(msgid)
  1220. for message_definition in message_definitions:
  1221. message_definition.check_message_definition(line, node)
  1222. self.file_state.handle_ignored_message(
  1223. self._get_message_state_scope(
  1224. message_definition.msgid, line, confidence
  1225. ),
  1226. message_definition.msgid,
  1227. line,
  1228. )
  1229. def _emit_stashed_messages(self) -> None:
  1230. for keys, values in self._stashed_messages.items():
  1231. modname, symbol = keys
  1232. self.linter.set_current_module(modname)
  1233. for args in values:
  1234. self.add_message(
  1235. symbol,
  1236. args=args,
  1237. line=0,
  1238. confidence=HIGH,
  1239. )
  1240. self._stashed_messages = collections.defaultdict(list)