|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* This file is part of PHP-Typography. |
|
4
|
|
|
* |
|
5
|
|
|
* Copyright 2017 Peter Putzer. |
|
6
|
|
|
* |
|
7
|
|
|
* This program is free software; you can redistribute it and/or |
|
8
|
|
|
* modify it under the terms of the GNU General Public License |
|
9
|
|
|
* as published by the Free Software Foundation; either version 2 |
|
10
|
|
|
* of the License, or ( at your option ) any later version. |
|
11
|
|
|
* |
|
12
|
|
|
* This program is distributed in the hope that it will be useful, |
|
13
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
14
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
15
|
|
|
* GNU General Public License for more details. |
|
16
|
|
|
* |
|
17
|
|
|
* You should have received a copy of the GNU General Public License |
|
18
|
|
|
* along with this program; if not, write to the Free Software |
|
19
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
20
|
|
|
* |
|
21
|
|
|
* @package mundschenk-at/php-typography/tests |
|
22
|
|
|
* @license http://www.gnu.org/licenses/gpl-2.0.html |
|
23
|
|
|
*/ |
|
24
|
|
|
|
|
25
|
|
|
namespace PHP_Typography\Tests; |
|
26
|
|
|
|
|
27
|
|
|
use \PHP_Typography\Strings; |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* Abstract base class for \PHP_Typography\* unit tests. |
|
31
|
|
|
*/ |
|
32
|
|
|
abstract class PHP_Typography_Testcase extends \PHPUnit\Framework\TestCase { |
|
33
|
|
|
/** |
|
34
|
|
|
* Return encoded HTML string (everything except <>"'). |
|
35
|
|
|
* |
|
36
|
|
|
* @param string $html A HTML fragment. |
|
37
|
|
|
*/ |
|
38
|
|
|
protected function clean_html( $html ) { |
|
39
|
|
|
// Convert everything except Latin and Cyrillic and Thai. |
|
40
|
|
|
static $convmap = [ |
|
41
|
|
|
// Simple Latin characters. |
|
42
|
|
|
0x80, 0x03ff, 0, 0xffffff, // @codingStandardsIgnoreLine. |
|
43
|
|
|
// Cyrillic characters. |
|
44
|
|
|
0x0514, 0x0dff, 0, 0xffffff, // @codingStandardsIgnoreLine. |
|
45
|
|
|
// Thai characters. |
|
46
|
|
|
0x0e7f, 0x10ffff, 0, 0xffffff, // @codingStandardsIgnoreLine. |
|
47
|
|
|
]; |
|
48
|
|
|
|
|
49
|
|
|
return str_replace( [ '<', '>' ], [ '<', '>' ], mb_encode_numericentity( htmlentities( $html, ENT_NOQUOTES, 'UTF-8', false ), $convmap, 'UTF-8' ) ); |
|
50
|
|
|
} |
|
51
|
|
|
|
|
52
|
|
|
/** |
|
53
|
|
|
* Call protected/private method of a class. |
|
54
|
|
|
* |
|
55
|
|
|
* @param object $object Instantiated object that we will run method on. |
|
56
|
|
|
* @param string $method_name Method name to call. |
|
57
|
|
|
* @param array $parameters Array of parameters to pass into method. |
|
58
|
|
|
* |
|
59
|
|
|
* @return mixed Method return. |
|
60
|
|
|
*/ |
|
61
|
|
View Code Duplication |
protected function invokeMethod( $object, $method_name, array $parameters = [] ) { |
|
|
|
|
|
|
62
|
|
|
$reflection = new \ReflectionClass( get_class( $object ) ); |
|
63
|
|
|
$method = $reflection->getMethod( $method_name ); |
|
64
|
|
|
$method->setAccessible( true ); |
|
65
|
|
|
|
|
66
|
|
|
return $method->invokeArgs( $object, $parameters ); |
|
67
|
|
|
} |
|
68
|
|
|
|
|
69
|
|
|
/** |
|
70
|
|
|
* Call protected/private method of a class. |
|
71
|
|
|
* |
|
72
|
|
|
* @param string $classname A class that we will run the method on. |
|
73
|
|
|
* @param string $method_name Method name to call. |
|
74
|
|
|
* @param array $parameters Array of parameters to pass into method. |
|
75
|
|
|
* |
|
76
|
|
|
* @return mixed Method return. |
|
77
|
|
|
*/ |
|
78
|
|
View Code Duplication |
protected function invokeStaticMethod( $classname, $method_name, array $parameters = [] ) { |
|
|
|
|
|
|
79
|
|
|
$reflection = new \ReflectionClass( $classname ); |
|
80
|
|
|
$method = $reflection->getMethod( $method_name ); |
|
81
|
|
|
$method->setAccessible( true ); |
|
82
|
|
|
|
|
83
|
|
|
return $method->invokeArgs( null, $parameters ); |
|
84
|
|
|
} |
|
85
|
|
|
|
|
86
|
|
|
/** |
|
87
|
|
|
* Sets the value of a private/protected property of a class. |
|
88
|
|
|
* |
|
89
|
|
|
* @param string $classname A class whose property we will access. |
|
90
|
|
|
* @param string $property_name Property to set. |
|
91
|
|
|
* @param mixed|null $value The new value. |
|
92
|
|
|
*/ |
|
93
|
|
View Code Duplication |
protected function setStaticValue( $classname, $property_name, $value ) { |
|
|
|
|
|
|
94
|
|
|
$reflection = new \ReflectionClass( $classname ); |
|
95
|
|
|
$property = $reflection->getProperty( $property_name ); |
|
96
|
|
|
$property->setAccessible( true ); |
|
97
|
|
|
$property->setValue( $value ); |
|
98
|
|
|
} |
|
99
|
|
|
|
|
100
|
|
|
/** |
|
101
|
|
|
* Sets the value of a private/protected property of a class. |
|
102
|
|
|
* |
|
103
|
|
|
* @param object $object Instantiated object that we will run method on. |
|
104
|
|
|
* @param string $property_name Property to set. |
|
105
|
|
|
* @param mixed|null $value The new value. |
|
106
|
|
|
*/ |
|
107
|
|
View Code Duplication |
protected function setValue( $object, $property_name, $value ) { |
|
|
|
|
|
|
108
|
|
|
$reflection = new \ReflectionClass( $classname ); |
|
|
|
|
|
|
109
|
|
|
$property = $reflection->getProperty( $property_name ); |
|
110
|
|
|
$property->setAccessible( true ); |
|
111
|
|
|
$property->setValue( $object, $value ); |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* Retrieves the value of a private/protected property of a class. |
|
116
|
|
|
* |
|
117
|
|
|
* @param string $classname A class whose property we will access. |
|
118
|
|
|
* @param string $property_name Property to set. |
|
119
|
|
|
* |
|
120
|
|
|
* @return mixed |
|
121
|
|
|
*/ |
|
122
|
|
View Code Duplication |
protected function getStaticValue( $classname, $property_name ) { |
|
|
|
|
|
|
123
|
|
|
$reflection = new \ReflectionClass( $classname ); |
|
124
|
|
|
$property = $reflection->getProperty( $property_name ); |
|
125
|
|
|
$property->setAccessible( true ); |
|
126
|
|
|
|
|
127
|
|
|
return $property->getValue(); |
|
128
|
|
|
} |
|
129
|
|
|
|
|
130
|
|
|
/** |
|
131
|
|
|
* Retrieves the value of a private/protected property of a class. |
|
132
|
|
|
* |
|
133
|
|
|
* @param object $object Instantiated object that we will run method on. |
|
134
|
|
|
* @param string $property_name Property to set. |
|
135
|
|
|
* |
|
136
|
|
|
* @return mixed |
|
137
|
|
|
*/ |
|
138
|
|
View Code Duplication |
protected function getValue( $object, $property_name ) { |
|
|
|
|
|
|
139
|
|
|
$reflection = new \ReflectionClass( get_class( $object ) ); |
|
140
|
|
|
$property = $reflection->getProperty( $property_name ); |
|
141
|
|
|
$property->setAccessible( true ); |
|
142
|
|
|
|
|
143
|
|
|
return $property->getValue( $object ); |
|
144
|
|
|
} |
|
145
|
|
|
|
|
146
|
|
|
/** |
|
147
|
|
|
* Helper function to generate a valid token list from strings. |
|
148
|
|
|
* |
|
149
|
|
|
* @param string $value The string to tokenize. |
|
150
|
|
|
* @param string $type Optional. Default 'word'. |
|
151
|
|
|
* |
|
152
|
|
|
* @return array |
|
153
|
|
|
*/ |
|
154
|
|
|
protected function tokenize( $value, $type = \PHP_Typography\Text_Parser\Token::WORD ) { |
|
155
|
|
|
return [ |
|
156
|
|
|
new \PHP_Typography\Text_Parser\Token( $value, $type ), |
|
157
|
|
|
]; |
|
158
|
|
|
} |
|
159
|
|
|
|
|
160
|
|
|
/** |
|
161
|
|
|
* Helper function to generate a valid word token list from strings. |
|
162
|
|
|
* |
|
163
|
|
|
* @param string $value Token value. |
|
164
|
|
|
* |
|
165
|
|
|
* @return array |
|
166
|
|
|
*/ |
|
167
|
|
|
protected function tokenize_sentence( $value ) { |
|
168
|
|
|
$words = explode( ' ', $value ); |
|
169
|
|
|
$tokens = []; |
|
170
|
|
|
|
|
171
|
|
|
foreach ( $words as $word ) { |
|
172
|
|
|
$tokens[] = new \PHP_Typography\Text_Parser\Token( $word, \PHP_Typography\Text_Parser\Token::WORD ); |
|
173
|
|
|
} |
|
174
|
|
|
|
|
175
|
|
|
return $tokens; |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
/** |
|
179
|
|
|
* Reports an error identified by $message if the combined token values differ from the expected value. |
|
180
|
|
|
* |
|
181
|
|
|
* @param string|array $expected_value Either a word/sentence or a token array. |
|
182
|
|
|
* @param array $actual_tokens A token array. |
|
183
|
|
|
* @param string $message Optional. Default ''. |
|
184
|
|
|
*/ |
|
185
|
|
|
protected function assertTokensSame( $expected_value, array $actual_tokens, $message = '' ) { |
|
186
|
|
|
$this->assertContainsOnlyInstancesOf( \PHP_Typography\Text_Parser\Token::class, $actual_tokens, '$actual_tokens has to be an array of tokens.' ); |
|
187
|
|
|
foreach ( $actual_tokens as $index => $token ) { |
|
188
|
|
|
$actual_tokens[ $index ] = $token->with_value( $this->clean_html( $token->value ) ); |
|
189
|
|
|
} |
|
190
|
|
|
|
|
191
|
|
View Code Duplication |
if ( is_scalar( $expected_value ) ) { |
|
|
|
|
|
|
192
|
|
|
if ( false !== strpos( $expected_value, ' ' ) ) { |
|
193
|
|
|
$expected_value = $this->tokenize_sentence( $expected_value ); |
|
194
|
|
|
} else { |
|
195
|
|
|
$expected_value = $this->tokenize( $expected_value ); |
|
196
|
|
|
} |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
// Ensure clean HTML even when a scalar was passed. |
|
200
|
|
|
$this->assertContainsOnlyInstancesOf( \PHP_Typography\Text_Parser\Token::class, $expected_value, '$expected_value has to be a string or an array of tokens.' ); |
|
201
|
|
|
foreach ( $expected_value as $index => $token ) { |
|
202
|
|
|
$expected[ $index ] = $token->with_value( $this->clean_html( $token->value ) ); |
|
|
|
|
|
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
$this->assertSame( count( $expected ), count( $actual_tokens ) ); |
|
|
|
|
|
|
206
|
|
|
|
|
207
|
|
|
foreach ( $actual_tokens as $key => $token ) { |
|
208
|
|
|
$this->assertSame( $expected[ $key ]->value, $token->value, $message ); |
|
209
|
|
|
$this->assertSame( $expected[ $key ]->type, $token->type, $message ); |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
return true; |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
/** |
|
216
|
|
|
* Reports an error identified by $message if the combined token values do |
|
217
|
|
|
* not differ from the expected value. |
|
218
|
|
|
* |
|
219
|
|
|
* @param string|array $expected_value Either a word/sentence or a token array. |
|
220
|
|
|
* @param array $actual_tokens A token array. |
|
221
|
|
|
* @param string $message Optional. Default ''. |
|
222
|
|
|
*/ |
|
223
|
|
|
protected function assertTokensNotSame( $expected_value, array $actual_tokens, $message = '' ) { |
|
224
|
|
|
$this->assertContainsOnlyInstancesOf( \PHP_Typography\Text_Parser\Token::class, $actual_tokens, '$actual_tokens has to be an array of tokens.' ); |
|
225
|
|
|
foreach ( $actual_tokens as $index => $token ) { |
|
226
|
|
|
$actual_tokens[ $index ] = $token->with_value( $this->clean_html( $token->value ) ); |
|
227
|
|
|
} |
|
228
|
|
|
|
|
229
|
|
|
if ( is_scalar( $expected_value ) ) { |
|
230
|
|
View Code Duplication |
if ( false !== strpos( $expected_value, ' ' ) ) { |
|
|
|
|
|
|
231
|
|
|
$expected = $this->tokenize_sentence( $expected_value ); |
|
232
|
|
|
} else { |
|
233
|
|
|
$expected = $this->tokenize( $expected_value ); |
|
234
|
|
|
} |
|
235
|
|
|
} else { |
|
236
|
|
|
$this->assertContainsOnlyInstancesOf( \PHP_Typography\Text_Parser\Token::class, $expected_value, '$expected_value has to be a string or an array of tokens.' ); |
|
237
|
|
|
$expected = $expected_value; |
|
238
|
|
|
} |
|
239
|
|
|
|
|
240
|
|
|
$this->assertSame( count( $expected ), count( $actual_tokens ) ); |
|
241
|
|
|
|
|
242
|
|
|
$result = false; |
|
243
|
|
|
foreach ( $actual_tokens as $key => $token ) { |
|
244
|
|
|
if ( $expected[ $key ]->value !== $token->value || $expected[ $key ]->type !== $token->type ) { |
|
245
|
|
|
$result = true; |
|
246
|
|
|
} |
|
247
|
|
|
} |
|
248
|
|
|
|
|
249
|
|
|
return $this->assertTrue( $result, $message ); |
|
250
|
|
|
} |
|
251
|
|
|
|
|
252
|
|
|
/** |
|
253
|
|
|
* Reports an error identified by $message if $attribute in $object does not have the $key. |
|
254
|
|
|
* |
|
255
|
|
|
* @param string $key The array key. |
|
256
|
|
|
* @param string $attribute The attribute name. |
|
257
|
|
|
* @param object $object The object. |
|
258
|
|
|
* @param string $message Optional. Default ''. |
|
259
|
|
|
*/ |
|
260
|
|
View Code Duplication |
protected function assertAttributeArrayHasKey( $key, $attribute, $object, $message = '' ) { |
|
|
|
|
|
|
261
|
|
|
$ref = new \ReflectionClass( get_class( $object ) ); |
|
262
|
|
|
$prop = $ref->getProperty( $attribute ); |
|
263
|
|
|
$prop->setAccessible( true ); |
|
264
|
|
|
|
|
265
|
|
|
return $this->assertArrayHasKey( $key, $prop->getValue( $object ), $message ); |
|
266
|
|
|
} |
|
267
|
|
|
|
|
268
|
|
|
/** |
|
269
|
|
|
* Reports an error identified by $message if $attribute in $object does have the $key. |
|
270
|
|
|
* |
|
271
|
|
|
* @param string $key The array key. |
|
272
|
|
|
* @param string $attribute The attribute name. |
|
273
|
|
|
* @param object $object The object. |
|
274
|
|
|
* @param string $message Optional. Default ''. |
|
275
|
|
|
*/ |
|
276
|
|
View Code Duplication |
protected function assertAttributeArrayNotHasKey( $key, $attribute, $object, $message = '' ) { |
|
|
|
|
|
|
277
|
|
|
$ref = new \ReflectionClass( get_class( $object ) ); |
|
278
|
|
|
$prop = $ref->getProperty( $attribute ); |
|
279
|
|
|
$prop->setAccessible( true ); |
|
280
|
|
|
|
|
281
|
|
|
return $this->assertArrayNotHasKey( $key, $prop->getValue( $object ), $message ); |
|
282
|
|
|
} |
|
283
|
|
|
|
|
284
|
|
|
/** |
|
285
|
|
|
* Assert that the given quote styles match. |
|
286
|
|
|
* |
|
287
|
|
|
* @param string $style Style name. |
|
288
|
|
|
* @param string $open Opening quote character. |
|
289
|
|
|
* @param string $close Closing quote character. |
|
290
|
|
|
*/ |
|
291
|
|
|
protected function assertSmartQuotesStyle( $style, $open, $close ) { |
|
292
|
|
|
switch ( $style ) { |
|
293
|
|
View Code Duplication |
case 'doubleCurled': |
|
|
|
|
|
|
294
|
|
|
$this->assertSame( Strings::_uchr( 8220 ), $open, "Opening quote $open did not match quote style $style." ); |
|
295
|
|
|
$this->assertSame( Strings::_uchr( 8221 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
296
|
|
|
break; |
|
297
|
|
|
|
|
298
|
|
View Code Duplication |
case 'doubleCurledReversed': |
|
|
|
|
|
|
299
|
|
|
$this->assertSame( Strings::_uchr( 8221 ), $open, "Opening quote $open did not match quote style $style." ); |
|
300
|
|
|
$this->assertSame( Strings::_uchr( 8221 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
301
|
|
|
break; |
|
302
|
|
|
|
|
303
|
|
View Code Duplication |
case 'doubleLow9': |
|
|
|
|
|
|
304
|
|
|
$this->assertSame( Strings::_uchr( 8222 ), $open, "Opening quote $open did not match quote style $style." ); |
|
305
|
|
|
$this->assertSame( Strings::_uchr( 8221 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
306
|
|
|
break; |
|
307
|
|
|
|
|
308
|
|
View Code Duplication |
case 'doubleLow9Reversed': |
|
|
|
|
|
|
309
|
|
|
$this->assertSame( Strings::_uchr( 8222 ), $open, "Opening quote $open did not match quote style $style." ); |
|
310
|
|
|
$this->assertSame( Strings::_uchr( 8220 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
311
|
|
|
break; |
|
312
|
|
|
|
|
313
|
|
View Code Duplication |
case 'singleCurled': |
|
|
|
|
|
|
314
|
|
|
$this->assertSame( Strings::_uchr( 8216 ), $open, "Opening quote $open did not match quote style $style." ); |
|
315
|
|
|
$this->assertSame( Strings::_uchr( 8217 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
316
|
|
|
break; |
|
317
|
|
|
|
|
318
|
|
View Code Duplication |
case 'singleCurledReversed': |
|
|
|
|
|
|
319
|
|
|
$this->assertSame( Strings::_uchr( 8217 ), $open, "Opening quote $open did not match quote style $style." ); |
|
320
|
|
|
$this->assertSame( Strings::_uchr( 8217 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
321
|
|
|
break; |
|
322
|
|
|
|
|
323
|
|
View Code Duplication |
case 'singleLow9': |
|
|
|
|
|
|
324
|
|
|
$this->assertSame( Strings::_uchr( 8218 ), $open, "Opening quote $open did not match quote style $style." ); |
|
325
|
|
|
$this->assertSame( Strings::_uchr( 8217 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
326
|
|
|
break; |
|
327
|
|
|
|
|
328
|
|
View Code Duplication |
case 'singleLow9Reversed': |
|
|
|
|
|
|
329
|
|
|
$this->assertSame( Strings::_uchr( 8218 ), $open, "Opening quote $open did not match quote style $style." ); |
|
330
|
|
|
$this->assertSame( Strings::_uchr( 8216 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
331
|
|
|
break; |
|
332
|
|
|
|
|
333
|
|
|
case 'doubleGuillemetsFrench': |
|
334
|
|
|
$this->assertSame( Strings::_uchr( 171 ) . Strings::_uchr( 160 ), $open, "Opening quote $open did not match quote style $style." ); |
|
335
|
|
|
$this->assertSame( Strings::_uchr( 160 ) . Strings::_uchr( 187 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
336
|
|
|
break; |
|
337
|
|
|
|
|
338
|
|
View Code Duplication |
case 'doubleGuillemets': |
|
|
|
|
|
|
339
|
|
|
$this->assertSame( Strings::_uchr( 171 ), $open, "Opening quote $open did not match quote style $style." ); |
|
340
|
|
|
$this->assertSame( Strings::_uchr( 187 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
341
|
|
|
break; |
|
342
|
|
|
|
|
343
|
|
View Code Duplication |
case 'doubleGuillemetsReversed': |
|
|
|
|
|
|
344
|
|
|
$this->assertSame( Strings::_uchr( 187 ), $open, "Opening quote $open did not match quote style $style." ); |
|
345
|
|
|
$this->assertSame( Strings::_uchr( 171 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
346
|
|
|
break; |
|
347
|
|
|
|
|
348
|
|
View Code Duplication |
case 'singleGuillemets': |
|
|
|
|
|
|
349
|
|
|
$this->assertSame( Strings::_uchr( 8249 ), $open, "Opening quote $open did not match quote style $style." ); |
|
350
|
|
|
$this->assertSame( Strings::_uchr( 8250 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
351
|
|
|
break; |
|
352
|
|
|
|
|
353
|
|
View Code Duplication |
case 'singleGuillemetsReversed': |
|
|
|
|
|
|
354
|
|
|
$this->assertSame( Strings::_uchr( 8250 ), $open, "Opening quote $open did not match quote style $style." ); |
|
355
|
|
|
$this->assertSame( Strings::_uchr( 8249 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
356
|
|
|
break; |
|
357
|
|
|
|
|
358
|
|
View Code Duplication |
case 'cornerBrackets': |
|
|
|
|
|
|
359
|
|
|
$this->assertSame( Strings::_uchr( 12300 ), $open, "Opening quote $open did not match quote style $style." ); |
|
360
|
|
|
$this->assertSame( Strings::_uchr( 12301 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
361
|
|
|
break; |
|
362
|
|
|
|
|
363
|
|
View Code Duplication |
case 'whiteCornerBracket': |
|
|
|
|
|
|
364
|
|
|
$this->assertSame( Strings::_uchr( 12302 ), $open, "Opening quote $open did not match quote style $style." ); |
|
365
|
|
|
$this->assertSame( Strings::_uchr( 12303 ), $close, "Closeing quote $close did not match quote style $style." ); |
|
366
|
|
|
break; |
|
367
|
|
|
|
|
368
|
|
|
default: |
|
369
|
|
|
$this->assertTrue( false, "Invalid quote style $style." ); |
|
370
|
|
|
} |
|
371
|
|
|
} |
|
372
|
|
|
} |
|
373
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.