[python] refactoring: Introduce PyAstNamedParameter.isPositionalOnly

GitOrigin-RevId: f072b2135128fdfb1d46dca68232968b20a6b078
This commit is contained in:
Mikhail Golubev
2025-11-25 18:58:25 +00:00
committed by intellij-monorepo-bot
parent 47c7fff641
commit ac5f5705e0
2 changed files with 96 additions and 0 deletions
@@ -18,6 +18,7 @@ package com.jetbrains.python.ast;
import com.intellij.lang.ASTNode;
import com.intellij.psi.*;
import com.intellij.util.ObjectUtils;
import com.jetbrains.python.PyNamesKt;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonDialectsTokenSetProvider;
import com.jetbrains.python.ast.impl.ParamHelperCore;
@@ -136,6 +137,48 @@ public interface PyAstNamedParameter extends PyAstParameter, PsiNamedElement, Ps
return varargSeen;
}
default boolean isPositionalOnly() {
if (isSelf() ||
isKeywordContainer() ||
isPositionalContainer() ||
this instanceof PyAstSingleStarParameter ||
this instanceof PyAstSlashParameter) {
return false;
}
final PyAstParameterList parameters = getStubOrPsiParentOfType(PyAstParameterList.class);
if (parameters == null) {
return false;
}
boolean thisSeen = false;
boolean allBeforeThisPrivate = true;
for (PyAstParameter param : parameters.getParameters()) {
if (param.isSelf()) {
continue;
}
if (param instanceof PyAstSingleStarParameter ||
(param instanceof PyAstNamedParameter np && (np.isPositionalContainer() || np.isKeywordContainer()))) {
// None of the positional-only parameter kinds can follow these
if (!thisSeen) {
return false;
}
// No need to continue, as `/` can't follow them either
break;
}
// New-style positional-only syntax, decide purely by the position
if (param instanceof PyAstSlashParameter) {
return thisSeen;
}
if (param == this) {
thisSeen = true;
}
else if (!thisSeen) {
allBeforeThisPrivate &= param.getName() != null && PyNamesKt.isPrivate(param.getName());
}
}
// Legacy-style positional only
return thisSeen && allBeforeThisPrivate && getName() != null && PyNamesKt.isPrivate(getName());
}
@Override
default boolean isSelf() {
if (isPositionalContainer() || isKeywordContainer()) {
@@ -152,4 +152,57 @@ public class PyMiscellaneousPsiOperationsTest extends PyTestCase {
final PyQualifiedExpression expr = (PyQualifiedExpression)generator.createExpressionFromText(LanguageLevel.PYTHON27, expression);
assertEquals(expectedQualifiedName, expr.asQualifiedName());
}
public void testNamedParameterIsPositionalOnly() {
doTestParameterIsPositionalOnly(List.of(false, true, true, false, false), """
class C:
def func(__self, __a, __b, c, __d):
pass
""");
doTestParameterIsPositionalOnly(List.of(true, true), """
class C:
@staticmethod
def func(__self, __a):
pass
""");
doTestParameterIsPositionalOnly(List.of(false, true), """
class C:
@classmethod
def func(__self, __a):
pass
""");
doTestParameterIsPositionalOnly(List.of(true, true), """
def func(__self, __a):
pass
""");
doTestParameterIsPositionalOnly(List.of(false, true, false, false), """
class C:
def func(self, __a, *__args, __b):
pass
""");
doTestParameterIsPositionalOnly(List.of(false, true, false, false), """
class C:
def func(self, __a, *, __b):
pass
""");
doTestParameterIsPositionalOnly(List.of(false, true, false), """
class C:
def func(self, __a, **__kwargs):
pass
""");
doTestParameterIsPositionalOnly(List.of(false, true, false, false), """
class C:
def func(self, a, /, __b):
pass
""");
}
private void doTestParameterIsPositionalOnly(List<Boolean> expected, String testData) {
myFixture.configureByText("a.py", testData);
PyFunction func = myFixture.findElementByText("func", PyFunction.class);
assertNotNull(func);
List<Boolean> actual = ContainerUtil.map(func.getParameterList().getParameters(),
p -> p instanceof PyNamedParameter np && np.isPositionalOnly());
assertOrderedEquals(actual, expected);
}
}