GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

ComplexTagMatcher::createAttributeMatchers()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 2
dl 0
loc 17
ccs 12
cts 12
cp 1
crap 4
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
namespace WMDE\HamcrestHtml;
4
5
use Hamcrest\Core\AllOf;
6
use Hamcrest\Core\IsEqual;
7
use InvalidArgumentException;
8
use Hamcrest\Description;
9
use Hamcrest\Matcher;
10
11
class ComplexTagMatcher extends TagMatcher {
12
13
	/**
14
	 * @link http://www.xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors
15
	 * @link https://github.com/Chronic-Dev/libxml2/blob/683f296a905710ff285c28b8644ef3a3d8be9486/include/libxml/xmlerror.h#L257
16
	 */
17
	const XML_UNKNOWN_TAG_ERROR_CODE = 801;
18
19
	/**
20
	 * @var string
21
	 */
22
	private $tagHtmlOutline;
23
24
	/**
25
	 * @var Matcher
26
	 */
27
	private $matcher;
28
29
	/**
30
	 * @param string $htmlOutline
31
	 *
32
	 * @return self
33
	 */
34 13
	public static function tagMatchingOutline( $htmlOutline ) {
35 13
		return new self( $htmlOutline );
36
	}
37
38
	/**
39
	 * @param string $tagHtmlRepresentation
40
	 */
41 13
	public function __construct( $tagHtmlRepresentation ) {
42 13
		parent::__construct();
43
44 13
		$this->tagHtmlOutline = $tagHtmlRepresentation;
45 13
		$this->matcher = $this->createMatcherFromHtml( $tagHtmlRepresentation );
46 10
	}
47
48 6
	public function describeTo( Description $description ) {
49 6
		$description->appendText( 'tag matching outline `' )
50 6
			->appendText( $this->tagHtmlOutline )
51 6
			->appendText( '` ' );
52 6
	}
53
54
	/**
55
	 * @param \DOMElement $item
56
	 * @param Description $mismatchDescription
57
	 *
58
	 * @return bool
59
	 */
60 12
	protected function matchesSafelyWithDiagnosticDescription( $item, Description $mismatchDescription ) {
61 12
		if ( $this->matcher->matches( $item ) ) {
62 6
			return true;
63
		}
64
65 7
		$mismatchDescription->appendText( 'was `' )
66 7
			->appendText( $this->elementToString( $item ) )
67 7
			->appendText( '`' );
68 7
		return false;
69
	}
70
71
	/**
72
	 * @param string $htmlOutline
73
	 *
74
	 * @return Matcher
75
	 */
76 13
	private function createMatcherFromHtml( $htmlOutline ) {
77 13
		$document = $this->parseHtml( $htmlOutline );
78 12
		$targetTag = $this->getSingleTagFromThe( $document );
79
80 11
		$this->assertTagDoesNotContainChildren( $targetTag );
81
82 10
		$attributeMatchers = $this->createAttributeMatchers( $htmlOutline, $targetTag );
83 10
		$classMatchers = $this->createClassMatchers( $targetTag );
84
85 10
		return AllOf::allOf(
86 10
			new TagNameMatcher( IsEqual::equalTo( $targetTag->tagName ) ),
87 10
			call_user_func_array( [ AllOf::class, 'allOf' ], $attributeMatchers ),
88 10
			call_user_func_array( [ AllOf::class, 'allOf' ], $classMatchers )
89 10
		);
90
	}
91
92
	/**
93
	 * @param \LibXMLError $error
94
	 *
95
	 * @return bool
96
	 */
97
	private function isUnknownTagError( \LibXMLError $error ) {
98
		return $error->code === self::XML_UNKNOWN_TAG_ERROR_CODE;
99
	}
100
101
	/**
102
	 * @param string $inputHtml
103
	 * @param string $attributeName
104
	 *
105
	 * @return bool
106
	 */
107 5
	private function isBooleanAttribute( $inputHtml, $attributeName ) {
108 5
		$quotedName = preg_quote( $attributeName, '/' );
109
110 5
		$attributeHasValueAssigned = preg_match( "/\b{$quotedName}\s*=/ui", $inputHtml );
111 5
		return !$attributeHasValueAssigned;
112
	}
113
114
	/**
115
	 * @param string $html
116
	 *
117
	 * @return \DOMDocument
118
	 * @throws \InvalidArgumentException
119
	 */
120 13
	private function parseHtml( $html ) {
121 13
		$internalErrors = libxml_use_internal_errors( true );
122 13
		$document = new \DOMDocument();
123
124
		// phpcs:ignore Generic.PHP.NoSilencedErrors
125 13
		if ( !@$document->loadHTML( $html ) ) {
126 1
			throw new \InvalidArgumentException( "There was some parsing error of `$html`" );
127
		}
128
129 12
		$errors = libxml_get_errors();
130 12
		libxml_clear_errors();
131 12
		libxml_use_internal_errors( $internalErrors );
132
133
		/** @var \LibXMLError $error */
134 12
		foreach ( $errors as $error ) {
135
			if ( $this->isUnknownTagError( $error ) ) {
136
				continue;
137
			}
138
139
			throw new \InvalidArgumentException(
140
				'There was parsing error: ' . trim( $error->message ) . ' on line ' . $error->line
141
			);
142 12
		}
143
144 12
		return $document;
145
	}
146
147
	/**
148
	 * @param \DOMDocument $document
149
	 *
150
	 * @return \DOMElement
151
	 * @throws \InvalidArgumentException
152
	 */
153 12
	private function getSingleTagFromThe( \DOMDocument $document ) {
154 12
		$directChildren = $document->documentElement->childNodes->item( 0 )->childNodes;
155
156 12
		if ( $directChildren->length !== 1 ) {
157 1
			throw new InvalidArgumentException(
158 1
				'Expected exactly 1 tag description, got ' . $directChildren->length
159 1
			);
160
		}
161
162 11
		return $directChildren->item( 0 );
163
	}
164
165 11
	private function assertTagDoesNotContainChildren( \DOMElement $targetTag ) {
166 11
		if ( $targetTag->childNodes->length > 0 ) {
167 1
			throw new InvalidArgumentException( 'Nested elements are not allowed' );
168
		}
169 10
	}
170
171
	/**
172
	 * @param string $inputHtml
173
	 * @param \DOMElement $targetTag
174
	 *
175
	 * @return AttributeMatcher[]
176
	 */
177 10
	private function createAttributeMatchers( $inputHtml, \DOMElement $targetTag ) {
178 10
		$attributeMatchers = [];
179
		/** @var \DOMAttr $attribute */
180 10
		foreach ( $targetTag->attributes as $attribute ) {
181 8
			if ( $attribute->name === 'class' ) {
182 3
				continue;
183
			}
184
185 5
			$attributeMatcher = new AttributeMatcher( IsEqual::equalTo( $attribute->name ) );
186 5
			if ( !$this->isBooleanAttribute( $inputHtml, $attribute->name ) ) {
187 4
				$attributeMatcher = $attributeMatcher->havingValue( IsEqual::equalTo( $attribute->value ) );
188 4
			}
189
190 5
			$attributeMatchers[] = $attributeMatcher;
191 10
		}
192 10
		return $attributeMatchers;
193
	}
194
195
	/**
196
	 * @param \DOMElement $targetTag
197
	 *
198
	 * @return ClassMatcher[]
199
	 */
200 10
	private function createClassMatchers( \DOMElement $targetTag ) {
201 10
		$classMatchers = [];
202 10
		$classValue = $targetTag->getAttribute( 'class' );
203 10
		foreach ( explode( ' ', $classValue ) as $expectedClass ) {
204 10
			if ( $expectedClass === '' ) {
205 8
				continue;
206
			}
207 3
			$classMatchers[] = new ClassMatcher( IsEqual::equalTo( $expectedClass ) );
208 10
		}
209 10
		return $classMatchers;
210
	}
211
212
	/**
213
	 * @param \DOMElement $element
214
	 *
215
	 * @return string
216
	 */
217 7
	private function elementToString( \DOMElement $element ) {
218 7
		$newDocument = new \DOMDocument();
219 7
		$cloned = $element->cloneNode( true );
220 7
		$newDocument->appendChild( $newDocument->importNode( $cloned, true ) );
221 7
		return trim( $newDocument->saveHTML() );
222
	}
223
224
}
225