Skip to content

Commit 53c24d3

Browse files
committed
Options to ignore type names that look like exceptions but are not (closes #52)
1 parent fd9de4b commit 53c24d3

File tree

7 files changed

+395
-51
lines changed

7 files changed

+395
-51
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ try {
112112

113113
Exceptions with different names can be configured in `specialExceptionNames` property.
114114

115+
If your codebase uses classes that look like exceptions (because they have `Exception` or `Error` suffixes) but aren't,
116+
you can add them to `ignoredNames` property and the sniff won't enforce them to be fully qualified. Classes with `Error`
117+
suffix has to be added to ignored only if they are in the root namespace (like `LibXMLError`).
118+
115119
#### SlevomatCodingStandard.Namespaces.MultipleUsesPerLine
116120

117121
Prohibits multiple uses separated by commas:
@@ -126,7 +130,7 @@ Enforces to use all referenced names with configurable omissions:
126130

127131
`fullyQualifiedKeywords` - allows fully qualified names after certain keywords. Useful in tandem with FullyQualifiedClassNameAfterKeyword sniff.
128132

129-
`allowFullyQualifiedExceptions` & `specialExceptionNames` - allows fully qualified exceptions. Useful in tandem with FullyQualifiedExceptions sniff.
133+
`allowFullyQualifiedExceptions`, `specialExceptionNames` & `ignoredNames` - allows fully qualified exceptions. Useful in tandem with FullyQualifiedExceptions sniff.
130134

131135
`allowPartialUses` - allows using and referencing whole namespaces:
132136

SlevomatCodingStandard/Sniffs/Namespaces/FullyQualifiedExceptionsSniff.php

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ class FullyQualifiedExceptionsSniff implements \PHP_CodeSniffer_Sniff
2121
/** @var string[] */
2222
private $normalizedSpecialExceptionNames;
2323

24+
/** @var string[] */
25+
public $ignoredNames = [];
26+
27+
/** @var string[] */
28+
private $normalizedIgnoredNames;
29+
2430
/**
2531
* @return integer[]
2632
*/
@@ -43,6 +49,18 @@ private function getSpecialExceptionNames()
4349
return $this->normalizedSpecialExceptionNames;
4450
}
4551

52+
/**
53+
* @return string[]
54+
*/
55+
private function getIgnoredNames()
56+
{
57+
if ($this->normalizedIgnoredNames === null) {
58+
$this->normalizedIgnoredNames = SniffSettingsHelper::normalizeArray($this->ignoredNames);
59+
}
60+
61+
return $this->normalizedIgnoredNames;
62+
}
63+
4664
/**
4765
* @param \PHP_CodeSniffer_File $phpcsFile
4866
* @param integer $openTagPointer
@@ -58,10 +76,13 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $openTagPointer)
5876
if (isset($useStatements[$normalizedName]) && $referencedName->hasSameUseStatementType($useStatements[$normalizedName])) {
5977
$useStatement = $useStatements[$normalizedName];
6078
if (
61-
!StringHelper::endsWith($useStatement->getFullyQualifiedTypeName(), 'Exception')
62-
&& $useStatement->getFullyQualifiedTypeName() !== 'Throwable'
63-
&& (!StringHelper::endsWith($useStatement->getFullyQualifiedTypeName(), 'Error') || NamespaceHelper::hasNamespace($useStatement->getFullyQualifiedTypeName()))
64-
&& !in_array($useStatement->getFullyQualifiedTypeName(), $this->getSpecialExceptionNames(), true)
79+
in_array($useStatement->getFullyQualifiedTypeName(), $this->getIgnoredNames(), true)
80+
|| (
81+
!StringHelper::endsWith($useStatement->getFullyQualifiedTypeName(), 'Exception')
82+
&& $useStatement->getFullyQualifiedTypeName() !== 'Throwable'
83+
&& (!StringHelper::endsWith($useStatement->getFullyQualifiedTypeName(), 'Error') || NamespaceHelper::hasNamespace($useStatement->getFullyQualifiedTypeName()))
84+
&& !in_array($useStatement->getFullyQualifiedTypeName(), $this->getSpecialExceptionNames(), true)
85+
)
6586
) {
6687
continue;
6788
}
@@ -72,10 +93,14 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $openTagPointer)
7293
$canonicalName = sprintf('%s%s%s', $fileNamespace, NamespaceHelper::NAMESPACE_SEPARATOR, $name);
7394
}
7495
if (
75-
!StringHelper::endsWith($name, 'Exception')
76-
&& $name !== 'Throwable'
77-
&& (!StringHelper::endsWith($canonicalName, 'Error') || NamespaceHelper::hasNamespace($canonicalName))
78-
&& !in_array($canonicalName, $this->getSpecialExceptionNames(), true)) {
96+
in_array($canonicalName, $this->getIgnoredNames(), true)
97+
|| (
98+
!StringHelper::endsWith($name, 'Exception')
99+
&& $name !== 'Throwable'
100+
&& (!StringHelper::endsWith($canonicalName, 'Error') || NamespaceHelper::hasNamespace($canonicalName))
101+
&& !in_array($canonicalName, $this->getSpecialExceptionNames(), true)
102+
)
103+
) {
79104
continue;
80105
}
81106
}

SlevomatCodingStandard/Sniffs/Namespaces/ReferenceUsedNamesOnlySniff.php

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ class ReferenceUsedNamesOnlySniff implements \PHP_CodeSniffer_Sniff
3333
/** @var string[]|null */
3434
private $normalizedSpecialExceptionNames;
3535

36+
/** @var string[] */
37+
public $ignoredNames = [];
38+
39+
/** @var string[] */
40+
private $normalizedIgnoredNames;
41+
3642
/** @var boolean */
3743
public $allowPartialUses = true;
3844

@@ -68,6 +74,18 @@ private function getSpecialExceptionNames()
6874
return $this->normalizedSpecialExceptionNames;
6975
}
7076

77+
/**
78+
* @return string[]
79+
*/
80+
private function getIgnoredNames()
81+
{
82+
if ($this->normalizedIgnoredNames === null) {
83+
$this->normalizedIgnoredNames = SniffSettingsHelper::normalizeArray($this->ignoredNames);
84+
}
85+
86+
return $this->normalizedIgnoredNames;
87+
}
88+
7189
/**
7290
* @return string[]
7391
*/
@@ -108,22 +126,28 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $openTagPointer)
108126
foreach ($referencedNames as $referencedName) {
109127
$name = $referencedName->getNameAsReferencedInFile();
110128
$pointer = $referencedName->getPointer();
129+
$canonicalName = NamespaceHelper::normalizeToCanonicalName($name);
111130
if (NamespaceHelper::isFullyQualifiedName($name)) {
131+
$isExceptionByName = StringHelper::endsWith($name, 'Exception')
132+
|| $name === '\Throwable'
133+
|| (StringHelper::endsWith($name, 'Error') && !NamespaceHelper::hasNamespace($name))
134+
|| in_array($canonicalName, $this->getSpecialExceptionNames(), true);
135+
$inIgnoredNames = in_array($canonicalName, $this->getIgnoredNames(), true);
112136
if (
137+
$this->isClassRequiredToBeUsed($name) &&
113138
(
114-
!(
115-
StringHelper::endsWith($name, 'Exception')
116-
|| $name === '\Throwable'
117-
|| (StringHelper::endsWith($name, 'Error') && !NamespaceHelper::hasNamespace($name))
118-
|| in_array(NamespaceHelper::normalizeToCanonicalName($name), $this->getSpecialExceptionNames(), true)
119-
) || !$this->allowFullyQualifiedExceptions
120-
) && $this->isClassRequiredToBeUsed($name)
139+
!$this->allowFullyQualifiedExceptions
140+
|| !$isExceptionByName
141+
|| $inIgnoredNames
142+
)
121143
) {
122144
$previousKeywordPointer = TokenHelper::findPreviousExcluding($phpcsFile, array_merge(
123145
TokenHelper::$nameTokenCodes,
124146
[T_WHITESPACE, T_COMMA]
125147
), $pointer - 1);
126-
if (!in_array($tokens[$previousKeywordPointer]['code'], $this->getFullyQualifiedKeywords(), true)) {
148+
if (
149+
!in_array($tokens[$previousKeywordPointer]['code'], $this->getFullyQualifiedKeywords(), true)
150+
) {
127151
if (
128152
!NamespaceHelper::hasNamespace($name)
129153
&& NamespaceHelper::findCurrentNamespaceName($phpcsFile, $pointer) === null

tests/Sniffs/Namespaces/FullyQualifiedExceptionsSniffTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,41 @@ public function testFullyQualifiedExceptionInCatch()
7171
$this->assertNoSniffError($this->getFileReport(), 28);
7272
}
7373

74+
public function testClassSuffixedErrorOrExceptionIsNotAnExceptionButReported()
75+
{
76+
$report = $this->checkFile(__DIR__ . '/data/ignoredNames.php');
77+
$this->assertSniffError($report, 3, FullyQualifiedExceptionsSniff::CODE_NON_FULLY_QUALIFIED_EXCEPTION);
78+
$this->assertSniffError($report, 7, FullyQualifiedExceptionsSniff::CODE_NON_FULLY_QUALIFIED_EXCEPTION);
79+
}
80+
81+
public function testIgnoredNames()
82+
{
83+
$report = $this->checkFile(__DIR__ . '/data/ignoredNames.php', [
84+
'ignoredNames' => [
85+
'LibXMLError',
86+
'LibXMLException',
87+
],
88+
]);
89+
$this->assertNoSniffErrorInFile($report);
90+
}
91+
92+
public function testClassSuffixedErrorOrExceptionIsNotAnExceptionButReportedInNamespace()
93+
{
94+
$report = $this->checkFile(__DIR__ . '/data/ignoredNamesInNamespace.php');
95+
$this->assertNoSniffError($report, 5); // *Error names are reported only with a root namespace
96+
$this->assertSniffError($report, 9, FullyQualifiedExceptionsSniff::CODE_NON_FULLY_QUALIFIED_EXCEPTION);
97+
$this->assertNoSniffError($report, 13); // look like Exception but isn't - handled by ReferenceUsedNamesOnlySniff
98+
$this->assertNoSniffError($report, 17); // dtto
99+
}
100+
101+
public function testIgnoredNamesInNamespace()
102+
{
103+
$report = $this->checkFile(__DIR__ . '/data/ignoredNamesInNamespace.php', [
104+
'ignoredNames' => [
105+
'IgnoredNames\\LibXMLException',
106+
],
107+
]);
108+
$this->assertNoSniffErrorInFile($report);
109+
}
110+
74111
}

0 commit comments

Comments
 (0)