Skip to content

Commit

Permalink
PHP 8.4 Support: Property hooks (Part 6)
Browse files Browse the repository at this point in the history
- apache#8035
- https://wiki.php.net/rfc#php_84
- https://wiki.php.net/rfc/property-hooks

- Fix the indentation that is inserted when a new line is added
- Fix an existing bug for a lambda function
- Add unit tests

Existing bug:
```php
// example
test(
        function(): void {^
);
```

```php
// before
test(
        function(): void {
    ^
        }
);

// after
test(
        function(): void {
            ^
        }
);
```
  • Loading branch information
junichi11 committed Mar 6, 2025
1 parent 3abcc5e commit baf05ae
Show file tree
Hide file tree
Showing 212 changed files with 3,110 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@

package org.netbeans.modules.php.editor.indent;

import java.util.Collection;
import java.util.Set;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.lexer.TokenUtilities;
import org.netbeans.editor.BaseDocument;
import org.netbeans.lib.editor.util.ArrayUtilities;
import org.netbeans.modules.php.editor.lexer.LexUtilities;
import org.netbeans.modules.php.editor.lexer.PHPTokenId;
import org.netbeans.modules.php.editor.lexer.utils.LexerUtils;

/**
* This class will be unnecessary when issue #192289 is fixed.
Expand All @@ -40,7 +42,24 @@ public final class IndentUtils {
}

private static final int MAX_CACHED_TAB_SIZE = 8; // Should mostly be <= 8

private static final Collection<PHPTokenId> BRACE_PLACEMENT_START_TOKENS = Set.of(
PHPTokenId.PHP_CLASS,
PHPTokenId.PHP_FUNCTION,
PHPTokenId.PHP_IF,
PHPTokenId.PHP_ELSE,
PHPTokenId.PHP_ELSEIF,
PHPTokenId.PHP_FOR,
PHPTokenId.PHP_FOREACH,
PHPTokenId.PHP_WHILE,
PHPTokenId.PHP_DO,
PHPTokenId.PHP_SWITCH,
PHPTokenId.PHP_PUBLIC,
PHPTokenId.PHP_PROTECTED,
PHPTokenId.PHP_PRIVATE,
PHPTokenId.PHP_PUBLIC_SET,
PHPTokenId.PHP_PROTECTED_SET,
PHPTokenId.PHP_PRIVATE_SET
);
/**
* Cached indentation string containing tabs.
* <br/>
Expand Down Expand Up @@ -132,24 +151,13 @@ public static int countIndent(BaseDocument doc, int offset, int previousIndent)
Token<? extends PHPTokenId> token = ts.token();
while (token.id() != PHPTokenId.PHP_CURLY_OPEN
&& token.id() != PHPTokenId.PHP_SEMICOLON
&& !(token.id() == PHPTokenId.PHP_TOKEN
&& (TokenUtilities.textEquals(token.text(), "(") // NOI18N
|| TokenUtilities.textEquals(token.text(), "["))) // NOI18N
&& !LexerUtils.isOpenParen(token)
&& !LexerUtils.isOpenBracket(token)
&& ts.movePrevious()) {
token = ts.token();
}
if (token.id() == PHPTokenId.PHP_CURLY_OPEN) {
while (token.id() != PHPTokenId.PHP_CLASS
&& token.id() != PHPTokenId.PHP_FUNCTION
&& token.id() != PHPTokenId.PHP_IF
&& token.id() != PHPTokenId.PHP_ELSE
&& token.id() != PHPTokenId.PHP_ELSEIF
&& token.id() != PHPTokenId.PHP_FOR
&& token.id() != PHPTokenId.PHP_FOREACH
&& token.id() != PHPTokenId.PHP_WHILE
&& token.id() != PHPTokenId.PHP_DO
&& token.id() != PHPTokenId.PHP_SWITCH
&& ts.movePrevious()) {
while (!BRACE_PLACEMENT_START_TOKENS.contains(token.id()) && ts.movePrevious()) {
token = ts.token();
}
CodeStyle codeStyle = CodeStyle.get(doc);
Expand All @@ -166,6 +174,8 @@ public static int countIndent(BaseDocument doc, int offset, int previousIndent)
bracePlacement = codeStyle.getWhileBracePlacement();
} else if (token.id() == PHPTokenId.PHP_SWITCH) {
bracePlacement = codeStyle.getSwitchBracePlacement();
} else if (LexerUtils.isGetOrSetVisibilityToken(token)) {
bracePlacement = codeStyle.getFieldDeclBracePlacement();
}
value = bracePlacement == CodeStyle.BracePlacement.NEW_LINE_INDENTED ? previousIndent + codeStyle.getIndentSize() : previousIndent;
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -505,11 +505,15 @@ public static int findStartTokenOfExpression(TokenSequence ts) {
int start = -1;
int origOffset = ts.offset();

Token<? extends PHPTokenId> token;
Token<? extends PHPTokenId> token = null;
Token<? extends PHPTokenId> lastTokenWithoutWSAndComments = null;
int balance = 0;
int curlyBalance = 0;
boolean isInQuotes = false; // GH-6731 for checking a variable in string
do {
if (!LexerUtils.isWhitespaceOrCommentToken(token)) {
lastTokenWithoutWSAndComments = token;
}
token = ts.token();
if (token.id() == PHPTokenId.PHP_TOKEN && !LexerUtils.isDollarCurlyOpen(token)) {
switch (token.text().charAt(0)) {
Expand All @@ -530,14 +534,10 @@ public static int findStartTokenOfExpression(TokenSequence ts) {
} else if ((token.id() == PHPTokenId.PHP_SEMICOLON || token.id() == PHPTokenId.PHP_OPENTAG)
&& ts.moveNext()) {
// we found previous end of expression => find begin of the current.
LexUtilities.findNext(ts, Arrays.asList(
PHPTokenId.WHITESPACE,
PHPTokenId.PHPDOC_COMMENT, PHPTokenId.PHPDOC_COMMENT_END, PHPTokenId.PHPDOC_COMMENT_START,
PHPTokenId.PHP_COMMENT, PHPTokenId.PHP_COMMENT_END, PHPTokenId.PHP_COMMENT_START,
PHPTokenId.PHP_LINE_COMMENT));
LexUtilities.findNext(ts, LexerUtils.getWSCommentTokens());
start = ts.offset();
break;
} else if (token.id() == PHPTokenId.PHP_IF) {
} else if (token.id() == PHPTokenId.PHP_IF && curlyBalance == 0) {
// we are at a beginning of if .... withouth curly?
// need to find end of the condition.
int offsetIf = ts.offset(); // remember the if offset
Expand Down Expand Up @@ -569,11 +569,7 @@ public static int findStartTokenOfExpression(TokenSequence ts) {
}
}
if (parentBalance == 1 && ts.movePrevious()) {
LexUtilities.findPrevious(ts, Arrays.asList(
PHPTokenId.WHITESPACE,
PHPTokenId.PHPDOC_COMMENT, PHPTokenId.PHPDOC_COMMENT_END, PHPTokenId.PHPDOC_COMMENT_START,
PHPTokenId.PHP_COMMENT, PHPTokenId.PHP_COMMENT_END, PHPTokenId.PHP_COMMENT_START,
PHPTokenId.PHP_LINE_COMMENT));
LexUtilities.findPrevious(ts, LexerUtils.getWSCommentTokens());
start = ts.offset();
}
break;
Expand All @@ -586,41 +582,51 @@ public static int findStartTokenOfExpression(TokenSequence ts) {
ts.move(offsetIf);
ts.movePrevious();
}
} else if (token.id() == PHPTokenId.PHP_CASE || token.id() == PHPTokenId.PHP_DEFAULT) {
} else if ((token.id() == PHPTokenId.PHP_CASE || token.id() == PHPTokenId.PHP_DEFAULT) && curlyBalance == 0) {
start = ts.offset();
break;
} else if (token.id() == PHPTokenId.PHP_CURLY_CLOSE) {
curlyBalance--;
if (!isInQuotes && curlyBalance == -1 && ts.moveNext()) {
// we are after previous blog close
LexUtilities.findNext(ts, Arrays.asList(
PHPTokenId.WHITESPACE,
PHPTokenId.PHPDOC_COMMENT, PHPTokenId.PHPDOC_COMMENT_END, PHPTokenId.PHPDOC_COMMENT_START,
PHPTokenId.PHP_COMMENT, PHPTokenId.PHP_COMMENT_END, PHPTokenId.PHP_COMMENT_START,
PHPTokenId.PHP_LINE_COMMENT));
if (ts.offset() <= origOffset) {
start = ts.offset();
} else {
start = origOffset;
if (lastTokenWithoutWSAndComments == null || !LexerUtils.isComma(lastTokenWithoutWSAndComments)) {
// check },
// e.g. hooked property (CPP), lambda functions
// public __construct(
// public int $prop {get {} set {}},
// ) {}
// myFunc(
// function() {},
// )
if (!isInQuotes && curlyBalance == -1 && ts.moveNext()) {
// we are after previous block close
LexUtilities.findNext(ts, LexerUtils.getWSCommentTokens());
if (ts.offset() <= origOffset) {
start = ts.offset();
} else {
start = origOffset;
}
break;
}
break;
}
} else if (LexerUtils.hasCurlyOpen(token)) {
curlyBalance++;
if (!isInQuotes && curlyBalance == 1 && ts.moveNext()) {
// we are at the begining of a blog
LexUtilities.findNext(ts, List.of(
PHPTokenId.WHITESPACE,
PHPTokenId.PHPDOC_COMMENT, PHPTokenId.PHPDOC_COMMENT_END, PHPTokenId.PHPDOC_COMMENT_START,
PHPTokenId.PHP_COMMENT, PHPTokenId.PHP_COMMENT_END, PHPTokenId.PHP_COMMENT_START,
PHPTokenId.PHP_LINE_COMMENT));
// we are at the begining of a block
LexUtilities.findNext(ts, LexerUtils.getWSCommentTokens());
if (ts.offset() <= origOffset) {
start = ts.offset();
} else {
start = origOffset;
}
break;
}
} else if (curlyBalance == 1
&& (LexerUtils.isGetOrSetVisibilityToken(token) || token.id() == PHPTokenId.PHP_FUNCTION)) {
// e.g. CPP, lambda function
// func(
// function() {^
// )
start = ts.offset();
break;
} else if (balance == 1 && token.id() == PHPTokenId.PHP_STRING) {
// probably there is a function call insede the expression
start = ts.offset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,41 @@
*/
package org.netbeans.modules.php.editor.lexer.utils;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenUtilities;
import org.netbeans.modules.php.editor.lexer.PHPTokenId;

public final class LexerUtils {

private static final Collection<PHPTokenId> WS_COMMENT_TOKENS = Set.of(
PHPTokenId.WHITESPACE,
PHPTokenId.PHPDOC_COMMENT, PHPTokenId.PHPDOC_COMMENT_END, PHPTokenId.PHPDOC_COMMENT_START,
PHPTokenId.PHP_COMMENT, PHPTokenId.PHP_COMMENT_END, PHPTokenId.PHP_COMMENT_START,
PHPTokenId.PHP_LINE_COMMENT
);
private static final Collection<PHPTokenId> VISIBILITY_TOKENS = Set.of(
PHPTokenId.PHP_PUBLIC,
PHPTokenId.PHP_PROTECTED,
PHPTokenId.PHP_PRIVATE
);
private static final Collection<PHPTokenId> SET_VISIBILITY_TOKENS = Set.of(
PHPTokenId.PHP_PUBLIC_SET,
PHPTokenId.PHP_PROTECTED_SET,
PHPTokenId.PHP_PRIVATE_SET
);
private static final Collection<PHPTokenId> ALL_VISIBILITY_TOKENS = Set.of(
PHPTokenId.PHP_PUBLIC,
PHPTokenId.PHP_PROTECTED,
PHPTokenId.PHP_PRIVATE,
PHPTokenId.PHP_PUBLIC_SET,
PHPTokenId.PHP_PROTECTED_SET,
PHPTokenId.PHP_PRIVATE_SET
);

private LexerUtils() {
}

Expand All @@ -46,4 +75,149 @@ public static boolean hasCurlyOpen(Token<? extends PHPTokenId> token) {
public static boolean isDollarCurlyOpen(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), "${"); // NOI18N
}

/**
* Check whether a token is the open parenthesis ("(").
*
* @param token a token
* @return {@code true} if a token is "(", {@code false} otherwise
*/
public static boolean isOpenParen(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), "("); // NOI18N
}

/**
* Check whether a token is the close parenthesis (")").
*
* @param token a token
* @return {@code true} if a token is ")", {@code false} otherwise
*/
public static boolean isCloseParen(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), ")"); // NOI18N
}

/**
* Check whether a token is the open bracket ("[").
*
* @param token a token
* @return {@code true} if a token is "[", {@code false} otherwise
*/
public static boolean isOpenBracket(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), "["); // NOI18N
}

/**
* Check whether a token is the close bracket ("]").
*
* @param token a token
* @return {@code true} if a token is "]", {@code false} otherwise
*/
public static boolean isCloseBracket(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), "]"); // NOI18N
}

/**
* Check whether a token is the comma (",").
*
* @param token a token
* @return {@code true} if a token is ",", {@code false} otherwise
*/
public static boolean isComma(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), ","); // NOI18N
}

/**
* Check whether a token is the colon (":").
*
* @param token a token
* @return {@code true} if a token is ":", {@code false} otherwise
*/
public static boolean isColon(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), ":"); // NOI18N
}

/**
* Check whether a token is the colon (":").
*
* @param token a token
* @return {@code true} if a token is ":", {@code false} otherwise
*/
public static boolean isEqual(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_OPERATOR && TokenUtilities.textEquals(token.text(), "="); // NOI18N
}

/**
* Check whether a token is the double arrow operator ("=>").
*
* @param token a token
* @return {@code true} if a token is "=>", {@code false} otherwise
*/
public static boolean isDoubleArrow(Token<? extends PHPTokenId> token) {
return token.id() == PHPTokenId.PHP_OPERATOR && TokenUtilities.textEquals(token.text(), "=>"); // NOI18N
}

/**
* Check whether a token is a whitespace or a comments.
*
* @param token a token
* @return {@code true} if a token is a whitespace or a comment,
* {@code false} otherwise
*/
public static boolean isWhitespaceOrCommentToken(Token<? extends PHPTokenId> token) {
return token == null ? false : WS_COMMENT_TOKENS.contains(token.id());
}

/**
* Check whether a token is a visibility token ({@code public},
* {@code protected}, {@code private}).
*
* @param token a token can be {@code null}
* @return {@code true} if it is a visibility token, {@code false} otherwise
*/
public static boolean isVisibilityToken(@NullAllowed Token<? extends PHPTokenId> token) {
return token == null ? false : VISIBILITY_TOKENS.contains(token.id());
}

/**
* Check whether a token is a set visibility token ({@code public(set)},
* {@code protected(set)}, {@code private(set)}).
*
* @param token a token can be {@code null}
* @return {@code true} if it is a set visibility token, {@code false}
* otherwise
*/
public static boolean isSetVisibilityToken(@NullAllowed Token<? extends PHPTokenId> token) {
return token == null ? false : SET_VISIBILITY_TOKENS.contains(token.id());
}

/**
* Check whether a token is one of all visibility tokens ({@code public},
* {@code protected}, {@code private}), ({@code public(set)},
* {@code protected(set)}, {@code private(set)}).
*
* @param token a token can be {@code null}
* @return {@code true} if it is one of all visibility tokens, {@code false}
* otherwise
*/
public static boolean isGetOrSetVisibilityToken(@NullAllowed Token<? extends PHPTokenId> token) {
return token == null ? false : ALL_VISIBILITY_TOKENS.contains(token.id());
}

/**
* Get whitespace and comment token ids.
*
* @return whitespace and comment token ids.
*/
public static List<PHPTokenId> getWSCommentTokens() {
return List.copyOf(WS_COMMENT_TOKENS);
}

/**
* Get all visibility token ids.
*
* @return all visibility token ids
*/
public static List<PHPTokenId> getAllVisibilityTokens() {
return List.copyOf(ALL_VISIBILITY_TOKENS);
}
}
Loading

0 comments on commit baf05ae

Please sign in to comment.