Skip to content
This repository was archived by the owner on Dec 15, 2024. It is now read-only.

Commit 6411c6e

Browse files
committed
feat(snake_case): Detection utilities ans Sniffs for functions and variables
1 parent ab08936 commit 6411c6e

File tree

11 files changed

+454
-1
lines changed

11 files changed

+454
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
/vendor/
22
/composer.phar
33
/composer.lock
4+
/.phpunit.cache/
5+
/phpunit.xml

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [Unreleased]
1010

11+
## [1.2.0] - 2021-08-24
12+
13+
### Added
14+
15+
- `snake_case` Detection Utilies + Unit Tests
16+
- Sniffs `SCS1.NamingConventions.SnakeCaseFunctionName` and `SCS1.NamingConventions.SnakeCaseVariableName` for SCS1
17+
- Taking account of special functions like `balise_*`
18+
- Class methods excluded
19+
1120
## [1.1.0] - 2021-08-21
1221

1322
### Added

composer.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,30 @@
1111
}
1212
],
1313
"require": {
14+
"php": ">=7.0",
15+
"ext-mbstring": "*",
1416
"squizlabs/php_codesniffer": "^3.6",
1517
"phpcompatibility/php-compatibility": "^9.3"
1618
},
19+
"require-dev": {
20+
"phpunit/phpunit": "^9.5"
21+
},
1722
"suggest" : {
1823
"dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically."
1924
},
25+
"autoload": {
26+
"psr-4": {
27+
"Spip\\CodingStandards\\": "src/"
28+
}
29+
},
30+
"autoload-dev": {
31+
"psr-4": {
32+
"Spip\\CodingStandards\\Test\\": "tests/"
33+
}
34+
},
2035
"extra": {
2136
"branch-alias": {
22-
"dev-main": "1.1.x-dev"
37+
"dev-main": "1.2.x-dev"
2338
}
2439
}
2540
}

phpunit.xml.dist

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
4+
bootstrap="tests/bootstrap.php"
5+
cacheResultFile=".phpunit.cache/test-results"
6+
executionOrder="depends,defects"
7+
forceCoversAnnotation="true"
8+
beStrictAboutCoversAnnotation="true"
9+
beStrictAboutOutputDuringTests="true"
10+
beStrictAboutTodoAnnotatedTests="true"
11+
failOnRisky="true"
12+
failOnWarning="true"
13+
verbose="true">
14+
<testsuites>
15+
<testsuite name="default">
16+
<directory>tests</directory>
17+
</testsuite>
18+
</testsuites>
19+
20+
<coverage cacheDirectory=".phpunit.cache/code-coverage"
21+
processUncoveredFiles="true">
22+
<include>
23+
<directory suffix=".php">src</directory>
24+
</include>
25+
<report>
26+
<html outputDirectory=".phpunit.cache/html"/>
27+
<text outputFile="php://stdout"/>
28+
</report>
29+
</coverage>
30+
</phpunit>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Spip\CodingStandards\SCS1\Sniffs\NamingConventions;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Standards\PEAR\Sniffs\NamingConventions\ValidFunctionNameSniff as PEARValidFunctionNameSniff;
7+
use Spip\CodingStandards\Strings\SnakeCase;
8+
9+
class SnakeCaseFunctionNameSniff extends PEARValidFunctionNameSniff
10+
{
11+
/**
12+
* {@inheritdoc}
13+
*/
14+
public function processTokenOutsideScope(File $phpcsFile, $stackPtr)
15+
{
16+
$functionName = $phpcsFile->getDeclarationName($stackPtr);
17+
if ($functionName === null) {
18+
return;
19+
}
20+
21+
$errorData = [$functionName];
22+
23+
// Does this function claim to be magical?
24+
if (preg_match('|^__[^_]|', $functionName) !== 0) {
25+
$error = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
26+
$phpcsFile->addError($error, $stackPtr, 'DoubleUnderscore', $errorData);
27+
28+
$functionName = ltrim($functionName, '_');
29+
}
30+
31+
if(preg_match(',^(balise|boucle|critere|iterateur)_([a-z0-9_]+),i', $functionName, $matches)) {
32+
$fullName = $functionName;
33+
34+
$functionPrefix = strtolower($matches[1]);
35+
if ($functionPrefix !== $matches[1]) {
36+
$error = 'Special function name "%s" is invalid; Prefix %s must be in lowercase"';
37+
$phpcsFile->addError($error, $stackPtr, 'PrefixLowerCase', [$fullName, $functionPrefix]);
38+
}
39+
40+
$functionName = $matches[2];
41+
$functionSuffix = '';
42+
if (preg_match(',_(contexte|dist|dyn|stat)$,i', $functionName, $matches2)) {
43+
$functionSuffix = strtolower($matches2[1]);
44+
if ($functionSuffix !== $matches2[1]) {
45+
$error = 'Special function name "%s" is invalid; Suffix %s must be in lowercase"';
46+
$phpcsFile->addError($error, $stackPtr, 'SuffixLowerCase', [$fullName, $functionSuffix]);
47+
}
48+
}
49+
50+
if ($functionSuffix) {
51+
if ($functionPrefix !== 'balise' && $functionSuffix !== 'dist') {
52+
$error = 'Special function name "%s" is invalid; Suffix %s is not allowed with prefix %s"';
53+
$phpcsFile->addError($error, $stackPtr, 'SuffixNotAllowed', [$fullName, $functionSuffix, $functionPrefix]);
54+
}
55+
$functionName = preg_replace(",_$matches2[1]$,", '', $functionName);
56+
}
57+
58+
if ($functionPrefix == 'critere') {
59+
return;
60+
}
61+
62+
if ($functionName !== strtoupper($functionName)) {
63+
$error = 'Special function name "%s" is invalid; Body %s must be in uppercase"';
64+
$phpcsFile->addError($error, $stackPtr, 'ScreamingSnakeCase', [$fullName, $functionName]);
65+
}
66+
} elseif (!SnakeCase::isSnakeCase($functionName)) {
67+
$phpcsFile->addError(
68+
'Function name "%s" is not in snake case format (Suggested name: %s)',
69+
$stackPtr,
70+
'NotSnakeCase',
71+
[$functionName, SnakeCase::toSnakeCase($functionName)]
72+
);
73+
}
74+
}
75+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
namespace Spip\CodingStandards\SCS1\Sniffs\NamingConventions;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;
7+
use PHP_CodeSniffer\Util\Tokens;
8+
use Spip\CodingStandards\Strings\SnakeCase;
9+
10+
class SnakeCaseVariableNameSniff extends AbstractVariableSniff
11+
{
12+
protected function processVariable(File $phpcsFile, $stackPtr)
13+
{
14+
$tokens = $phpcsFile->getTokens();
15+
$varName = ltrim($tokens[$stackPtr]['content'], '$');
16+
17+
// If it's a php reserved var, then its ok.
18+
if (isset($this->phpReservedVars[$varName]) === true) {
19+
return;
20+
}
21+
22+
$objOperator = $phpcsFile->findNext([T_WHITESPACE], ($stackPtr + 1), null, true);
23+
if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR
24+
|| $tokens[$objOperator]['code'] === T_NULLSAFE_OBJECT_OPERATOR
25+
) {
26+
// Check to see if we are using a variable from an object.
27+
$var = $phpcsFile->findNext([T_WHITESPACE], ($objOperator + 1), null, true);
28+
if ($tokens[$var]['code'] === T_STRING) {
29+
$bracket = $phpcsFile->findNext([T_WHITESPACE], ($var + 1), null, true);
30+
if ($tokens[$bracket]['code'] !== T_OPEN_PARENTHESIS) {
31+
$objVarName = $tokens[$var]['content'];
32+
33+
// There is no way for us to know if the var is public or
34+
// private, so we have to ignore a leading underscore if there is
35+
// one and just check the main part of the variable name.
36+
$originalVarName = $objVarName;
37+
if (substr($objVarName, 0, 1) === '_') {
38+
$objVarName = substr($objVarName, 1);
39+
}
40+
41+
if (!SnakeCase::isSnakeCase($objVarName)) {
42+
$suggestedName = SnakeCase::toSnakeCase($originalVarName);
43+
$error = 'Member variable "%s" is not in valid snake_case format (Suggested name: %s)';
44+
$data = [$originalVarName, $suggestedName];
45+
$phpcsFile->addError($error, $var, 'MemberNotSnakeCase', $data);
46+
}
47+
}//end if
48+
}//end if
49+
}//end if
50+
51+
$objOperator = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
52+
if ($tokens[$objOperator]['code'] === T_DOUBLE_COLON) {
53+
// The variable lives within a class, and is referenced like
54+
// this: MyClass::$_variable, so we don't know its scope.
55+
$objVarName = $varName;
56+
if (substr($objVarName, 0, 1) === '_') {
57+
$objVarName = substr($objVarName, 1);
58+
}
59+
60+
if (!SnakeCase::isSnakeCase($objVarName)) {
61+
$suggestedName = SnakeCase::toSnakeCase($varName);
62+
$error = 'Member variable "%s" is not in valid snake_case format (Suggested name: %s)';
63+
$data = [$tokens[$stackPtr]['content'], $suggestedName];
64+
$phpcsFile->addError($error, $stackPtr, 'MemberNotSnakeCase', $data);
65+
}
66+
67+
return;
68+
}
69+
70+
// There is no way for us to know if the var is public or private,
71+
// so we have to ignore a leading underscore if there is one and just
72+
// check the main part of the variable name.
73+
$originalVarName = $varName;
74+
if (substr($varName, 0, 1) === '_') {
75+
$inClass = $phpcsFile->hasCondition($stackPtr, Tokens::$ooScopeTokens);
76+
if ($inClass === true) {
77+
$varName = substr($varName, 1);
78+
}
79+
}
80+
81+
if (!SnakeCase::isSnakeCase($varName)) {
82+
$suggestedName = SnakeCase::toSnakeCase($originalVarName);
83+
$error = 'Variable "%s" is not in valid snake case format (Suggested name: %s)';
84+
$data = [$originalVarName, $suggestedName];
85+
$phpcsFile->addError($error, $stackPtr, 'NotSnakeCase', $data);
86+
}
87+
}
88+
89+
protected function processMemberVar(File $phpcsFile, $stackPtr)
90+
{
91+
// We don't care about member vars
92+
}
93+
94+
protected function processVariableInString(File $phpcsFile, $stackPtr)
95+
{
96+
// We don't care about member vars
97+
}
98+
}

src/Strings/KebabCase.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Spip\CodingStandards\Strings;
4+
5+
class KebabCase
6+
{
7+
public static function isKebabCase(string $string): bool
8+
{
9+
return (bool) preg_match('/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/', $string);
10+
}
11+
12+
public static function toKebabCase(string $string): string
13+
{
14+
return ltrim(
15+
mb_strtolower(
16+
preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '-$0', $string) ?? ''
17+
), '-'
18+
);
19+
}
20+
}

src/Strings/SnakeCase.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Spip\CodingStandards\Strings;
4+
5+
class SnakeCase
6+
{
7+
public static function isSnakeCase(string $string): bool
8+
{
9+
return (bool) preg_match('/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/', $string);
10+
}
11+
12+
public static function toSnakeCase(string $string): string
13+
{
14+
return ltrim(
15+
mb_strtolower(
16+
preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $string) ?? ''
17+
), '_'
18+
);
19+
}
20+
}

tests/Strings/KebabCaseTest.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace Spip\CodingStandards\Test\Strings;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Spip\CodingStandards\Strings\KebabCase;
7+
8+
/**
9+
* @covers Spip\CodingStandards\Strings\KebabCase
10+
*/
11+
class KebabCaseTest extends TestCase
12+
{
13+
public function dataIsKebabCase()
14+
{
15+
return [
16+
'empty string' => [
17+
false,
18+
'',
19+
],
20+
'underscore starting string' => [
21+
false,
22+
'_kebab_case',
23+
],
24+
'underscore ending string' => [
25+
false,
26+
'kebab_case_',
27+
],
28+
'uppercase string' => [
29+
false,
30+
'NotKebabCase',
31+
],
32+
'minimal kebab_case string' => [
33+
true,
34+
's-c',
35+
],
36+
'kebab_case string' => [
37+
true,
38+
'kebab-case',
39+
],
40+
];
41+
}
42+
43+
/**
44+
* @dataProvider dataIsKebabCase
45+
*/
46+
public function testIsKebabCase($expected, $string)
47+
{
48+
$this->assertSame($expected, KebabCase::isKebabCase($string));
49+
}
50+
51+
public function dataToKebabCase()
52+
{
53+
return [
54+
'PascalCase' => [
55+
'kebab-case',
56+
'KebabCase',
57+
],
58+
'camelCase' => [
59+
'kebab-case',
60+
'kebabCase',
61+
],
62+
'UPPERCASE' => [
63+
'kebabcase',
64+
'KEBABCASE',
65+
],
66+
'kebab-withmiddle-case' => [
67+
'weird-kebab-case',
68+
'WeirdKEBABCase',
69+
],
70+
'BringAKebab' => [
71+
'bring-a-kebab',
72+
'BringAKebab',
73+
],
74+
];
75+
}
76+
77+
/**
78+
* @dataProvider dataToKebabCase
79+
*/
80+
public function testToKebabCase($expected, $string)
81+
{
82+
$this->assertSame($expected, KebabCase::toKebabCase($string));
83+
}
84+
}

0 commit comments

Comments
 (0)