1<?php
2/**
3 * Squiz_Sniffs_CSS_ShorthandSizeSniff.
4 *
5 * PHP version 5
6 *
7 * @category  PHP
8 * @package   PHP_CodeSniffer
9 * @author    Greg Sherwood <gsherwood@squiz.net>
10 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
11 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
12 * @link      http://pear.php.net/package/PHP_CodeSniffer
13 */
14
15/**
16 * Squiz_Sniffs_CSS_ShorthandSizeSniff.
17 *
18 * Ensure sizes are defined using shorthand notation where possible, except in the
19 * case where shorthand becomes 3 values.
20 *
21 * @category  PHP
22 * @package   PHP_CodeSniffer
23 * @author    Greg Sherwood <gsherwood@squiz.net>
24 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
25 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
26 * @version   Release: @package_version@
27 * @link      http://pear.php.net/package/PHP_CodeSniffer
28 */
29class Squiz_Sniffs_CSS_ShorthandSizeSniff implements PHP_CodeSniffer_Sniff
30{
31
32    /**
33     * A list of tokenizers this sniff supports.
34     *
35     * @var array
36     */
37    public $supportedTokenizers = array('CSS');
38
39    /**
40     * A list of styles that we shouldn't check.
41     *
42     * These have values that looks like sizes, but are not.
43     *
44     * @var array
45     */
46    protected $excludeStyles = array(
47                                'background-position'      => 'background-position',
48                                'box-shadow'               => 'box-shadow',
49                                'transform-origin'         => 'transform-origin',
50                                '-webkit-transform-origin' => '-webkit-transform-origin',
51                                '-ms-transform-origin'     => '-ms-transform-origin',
52                               );
53
54
55    /**
56     * Returns the token types that this sniff is interested in.
57     *
58     * @return int[]
59     */
60    public function register()
61    {
62        return array(T_STYLE);
63
64    }//end register()
65
66
67    /**
68     * Processes the tokens that this sniff is interested in.
69     *
70     * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found.
71     * @param int                  $stackPtr  The position in the stack where
72     *                                        the token was found.
73     *
74     * @return void
75     */
76    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
77    {
78        $tokens = $phpcsFile->getTokens();
79
80        // Some styles look like shorthand but are not actually a set of 4 sizes.
81        $style = strtolower($tokens[$stackPtr]['content']);
82        if (isset($this->excludeStyles[$style]) === true) {
83            return;
84        }
85
86        // Get the whole style content.
87        $end         = $phpcsFile->findNext(T_SEMICOLON, ($stackPtr + 1));
88        $origContent = $phpcsFile->getTokensAsString(($stackPtr + 1), ($end - $stackPtr - 1));
89        $origContent = trim($origContent, ': ');
90
91        // Account for a !important annotation.
92        $content = $origContent;
93        if (substr($content, -10) === '!important') {
94            $content = substr($content, 0, -10);
95            $content = trim($content);
96        }
97
98        // Check if this style value is a set of numbers with optional prefixes.
99        $content = preg_replace('/\s+/', ' ', $content);
100        $values  = array();
101        $num     = preg_match_all(
102            '/([0-9]+)([a-zA-Z]{2}\s+|%\s+|\s+)/',
103            $content.' ',
104            $values,
105            PREG_SET_ORDER
106        );
107
108        // Only interested in styles that have multiple sizes defined.
109        if ($num < 2) {
110            return;
111        }
112
113        // Rebuild the content we matched to ensure we got everything.
114        $matched = '';
115        foreach ($values as $value) {
116            $matched .= $value[0];
117        }
118
119        if ($content !== trim($matched)) {
120            return;
121        }
122
123        if ($num === 3) {
124            $expected = trim($content.' '.$values[1][1].$values[1][2]);
125            $error    = 'Shorthand syntax not allowed here; use %s instead';
126            $data     = array($expected);
127            $fix      = $phpcsFile->addFixableError($error, $stackPtr, 'NotAllowed', $data);
128
129            if ($fix === true) {
130                $phpcsFile->fixer->beginChangeset();
131                if (substr($origContent, -10) === '!important') {
132                    $expected .= ' !important';
133                }
134
135                $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 2), null, true);
136                $phpcsFile->fixer->replaceToken($next, $expected);
137                for ($next++; $next < $end; $next++) {
138                    $phpcsFile->fixer->replaceToken($next, '');
139                }
140
141                $phpcsFile->fixer->endChangeset();
142            }
143
144            return;
145        }//end if
146
147        if ($num === 2) {
148            if ($values[0][0] !== $values[1][0]) {
149                // Both values are different, so it is already shorthand.
150                return;
151            }
152        } else if ($values[0][0] !== $values[2][0] || $values[1][0] !== $values[3][0]) {
153            // Can't shorthand this.
154            return;
155        }
156
157        if ($values[0][0] === $values[1][0]) {
158            // All values are the same.
159            $expected = $values[0][0];
160        } else {
161            $expected = $values[0][0].' '.$values[1][0];
162        }
163
164        $expected = preg_replace('/\s+/', ' ', trim($expected));
165
166        $error = 'Size definitions must use shorthand if available; expected "%s" but found "%s"';
167        $data  = array(
168                  $expected,
169                  $content,
170                 );
171
172        $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NotUsed', $data);
173        if ($fix === true) {
174            $phpcsFile->fixer->beginChangeset();
175            if (substr($origContent, -10) === '!important') {
176                $expected .= ' !important';
177            }
178
179            $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 2), null, true);
180            $phpcsFile->fixer->replaceToken($next, $expected);
181            for ($next++; $next < $end; $next++) {
182                $phpcsFile->fixer->replaceToken($next, '');
183            }
184
185            $phpcsFile->fixer->endChangeset();
186        }
187
188    }//end process()
189
190
191}//end class
192