Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
NamespaceInfo
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
4 / 4
32
100.00% covered (success)
100.00%
1 / 1
 getNamespaceInfo
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
8
 getClassAliases
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 qualifyClassName
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 unqualifyClassName
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
12
1<?php
2/**
3 * Utility class managing namespace info.
4 *
5 * @package automattic/jetpack-codesniffer
6 */
7
8namespace Automattic\Jetpack\Codesniffer\Utils;
9
10use PHP_CodeSniffer\Files\File;
11use PHPCSUtils\Internal\Cache;
12use PHPCSUtils\Utils\Namespaces;
13use PHPCSUtils\Utils\UseStatements;
14
15/**
16 * Utility class managing namespace info.
17 */
18class NamespaceInfo {
19
20    /**
21     * Get namespace information for a token.
22     *
23     * Every token is part of a namespace, even an implicit one.
24     *
25     * @param File $phpcsFile The file where the token was found.
26     * @param int  $stackPtr The position in the stack where the token was found.
27     * @return array Namespace information.
28     *  - ptr: (?int) Stack pointer for the T_NAMESPACE token, if any.
29     *  - name: (string) Namespace name. May be the empty string.
30     *  - nsStart: (int) Stack pointer for the start of the "body" of the namespace. First T_OPEN_TAG if it's an implicit namespace, or 0 if none.
31     *  - nsEnd: (?int) Stack pointer for the end of the "body" of the namespace.
32     */
33    public static function getNamespaceInfo( File $phpcsFile, $stackPtr ) {
34        $nsptr = Namespaces::findNamespacePtr( $phpcsFile, $stackPtr );
35
36        // Implicit namespace.
37        if ( $nsptr === false ) {
38            return array(
39                'ptr'     => null,
40                'name'    => '',
41                // phpcs:ignore Universal.Operators.DisallowShortTernary.Found -- Used correctly.
42                'nsStart' => $phpcsFile->findNext( T_OPEN_TAG, 0 ) ?: 0,
43                'nsEnd'   => null,
44            );
45        }
46
47        // @phan-suppress-next-line PhanAccessMethodInternal
48        if ( Cache::isCached( $phpcsFile, __METHOD__, $nsptr ) ) {
49            // @phan-suppress-next-line PhanAccessMethodInternal
50            return Cache::get( $phpcsFile, __METHOD__, $nsptr );
51        }
52
53        $tokens = $phpcsFile->getTokens();
54        $nsinfo = array(
55            'ptr' => $nsptr,
56        );
57
58        $nsinfo['ptr']  = $nsptr;
59        $nsname         = Namespaces::getDeclaredName( $phpcsFile, $nsptr );
60        $nsinfo['name'] = $nsname === false ? '' : $nsname;
61        if ( isset( $tokens[ $nsptr ]['scope_opener'] ) ) {
62            $nsinfo['nsStart'] = $tokens[ $nsptr ]['scope_opener'];
63            $nsinfo['nsEnd']   = $tokens[ $nsptr ]['scope_closer'];
64        } else {
65            $nsinfo['nsStart'] = $phpcsFile->findEndOfStatement( $nsptr );
66            $nsinfo['nsEnd']   = null;
67            $idx               = $nsinfo['nsStart'];
68            // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- Intentional.
69            while ( ( $idx = $phpcsFile->findNext( T_NAMESPACE, $idx + 1, null ) ) !== false ) {
70                if ( Namespaces::isDeclaration( $phpcsFile, $idx ) ) {
71                    $nsinfo['nsEnd'] = $phpcsFile->findStartOfStatement( $idx ) - 1;
72                    break;
73                }
74            }
75        }
76
77        // @phan-suppress-next-line PhanAccessMethodInternal
78        Cache::set( $phpcsFile, __METHOD__, $nsptr, $nsinfo );
79        return $nsinfo;
80    }
81
82    /**
83     * Get class alias information for a namespace.
84     *
85     * @param File  $phpcsFile The file where the token was found.
86     * @param array $nsinfo As returned by `::getNamespaceInfo()`.
87     * @return array Mapping of aliases to fully qualified class names.
88     */
89    public static function getClassAliases( File $phpcsFile, array $nsinfo ) {
90        // @phan-suppress-next-line PhanAccessMethodInternal
91        if ( Cache::isCached( $phpcsFile, __METHOD__, $nsinfo['nsptr'] ?? '' ) ) {
92            // @phan-suppress-next-line PhanAccessMethodInternal
93            return Cache::get( $phpcsFile, __METHOD__, $nsinfo['nsptr'] ?? '' );
94        }
95
96        $classAliases = array();
97        $idx          = $nsinfo['nsStart'];
98        // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- Intentional.
99        while ( ( $idx = $phpcsFile->findNext( T_USE, $idx + 1, $nsinfo['nsEnd'] ) ) !== false ) {
100            if ( ! UseStatements::isImportUse( $phpcsFile, $idx ) ) {
101                continue;
102            }
103            $info = UseStatements::splitImportUseStatement( $phpcsFile, $idx );
104            foreach ( $info['name'] as $alias => $name ) {
105                $classAliases[ $alias ] = '\\' . ltrim( $name, '\\' );
106            }
107        }
108
109        // @phan-suppress-next-line PhanAccessMethodInternal
110        Cache::set( $phpcsFile, __METHOD__, $nsinfo['nsptr'] ?? '', $classAliases );
111        return $classAliases;
112    }
113
114    /**
115     * Fully-qualify a class name string.
116     *
117     * @param string $name Class name string.
118     * @param string $nsname Containing namespace name. Pass empty string for the base namespace.
119     * @param array  $aliases Map of class aliases to fully qualified class names.
120     * @return string Fully-qualified class name, with a leading backslash.
121     */
122    public static function qualifyClassName( $name, $nsname, array $aliases ) {
123        // Already qualified?
124        if ( str_starts_with( $name, '\\' ) ) {
125            return $name;
126        }
127
128        // Is a namespace operator?
129        if ( str_starts_with( $name, 'namespace\\' ) ) {
130            $name = substr( $name, 9 );
131            if ( $nsname !== '' ) {
132                $name = '\\' . $nsname . $name;
133            }
134            return $name;
135        }
136
137        // Known alias?
138        $i = strpos( $name, '\\' );
139        if ( $i !== false ) {
140            $first = substr( $name, 0, $i );
141            $rest  = substr( $name, $i );
142        } else {
143            $first = $name;
144            $rest  = '';
145        }
146        if ( isset( $aliases[ $first ] ) ) {
147            return $aliases[ $first ] . $rest;
148        }
149
150        // Otherwise, prefix the namespace (if any) and return it.
151        if ( $nsname !== '' ) {
152            $name = $nsname . '\\' . $name;
153        }
154        return '\\' . $name;
155    }
156
157    /**
158     * Abbreviate a fully-qualified class name string.
159     *
160     * @param string $name Fully-qualified class name string, leading backslash optional.
161     * @param string $nsname Containing namespace name. Pass empty string for the base namespace.
162     * @param array  $aliases Map of class aliases to fully qualified class names.
163     * @return string Abbreviated class name string, if possible. Leading backslash prepended if not possible.
164     */
165    public static function unqualifyClassName( $name, $nsname, array $aliases ) {
166        if ( ! str_starts_with( $name, '\\' ) ) {
167            $name = '\\' . $name;
168        }
169
170        // Try removing the namespace prefix.
171        if ( $nsname === '' ) {
172            $unprefixed = substr( $name, 1 );
173        } elseif ( str_starts_with( $name, '\\' . $nsname . '\\' ) ) {
174            $unprefixed = substr( $name, strlen( $nsname ) + 2 );
175        } else {
176            $unprefixed = null;
177        }
178        // Skip if the shortened version is an alias though.
179        if ( $unprefixed !== null && isset( $aliases[ $unprefixed ] ) ) {
180            $unprefixed = null;
181        }
182        $unprefixedCt = $unprefixed === null ? INF : substr_count( $unprefixed, '\\' );
183
184        // Check if any aliases exactly match or are a prefix. Prefer the one resulting in the fewest backslashes, if any have fewer than the above.
185        foreach ( $aliases as $alias => $fqcn ) {
186            if ( $name === $fqcn ) {
187                return $alias;
188            }
189            if ( str_starts_with( $name, $fqcn . '\\' ) ) {
190                $short = $alias . substr( $name, strlen( $fqcn ) );
191                $ct    = substr_count( $short, '\\' );
192                if ( $ct < $unprefixedCt ) {
193                    $unprefixed   = $short;
194                    $unprefixedCt = $ct;
195                }
196            }
197        }
198
199        if ( $unprefixed !== null ) {
200            return $unprefixed;
201        }
202
203        // None of the above worked, just return the FQCN.
204        return $name;
205    }
206}