From ac5f5705e030d385f2adc972323054b90bae68d2 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Wed, 22 Oct 2025 22:37:11 +0300 Subject: [PATCH] [python] refactoring: Introduce PyAstNamedParameter.isPositionalOnly GitOrigin-RevId: f072b2135128fdfb1d46dca68232968b20a6b078 --- .../python/ast/PyAstNamedParameter.java | 43 +++++++++++++++ .../PyMiscellaneousPsiOperationsTest.java | 53 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/python/python-ast/src/com/jetbrains/python/ast/PyAstNamedParameter.java b/python/python-ast/src/com/jetbrains/python/ast/PyAstNamedParameter.java index f7c6b64a9633..57da256a4127 100644 --- a/python/python-ast/src/com/jetbrains/python/ast/PyAstNamedParameter.java +++ b/python/python-ast/src/com/jetbrains/python/ast/PyAstNamedParameter.java @@ -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()) { diff --git a/python/testSrc/com/jetbrains/python/PyMiscellaneousPsiOperationsTest.java b/python/testSrc/com/jetbrains/python/PyMiscellaneousPsiOperationsTest.java index 2fd99640ae6b..f122e18428db 100644 --- a/python/testSrc/com/jetbrains/python/PyMiscellaneousPsiOperationsTest.java +++ b/python/testSrc/com/jetbrains/python/PyMiscellaneousPsiOperationsTest.java @@ -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 expected, String testData) { + myFixture.configureByText("a.py", testData); + PyFunction func = myFixture.findElementByText("func", PyFunction.class); + assertNotNull(func); + List actual = ContainerUtil.map(func.getParameterList().getParameters(), + p -> p instanceof PyNamedParameter np && np.isPositionalOnly()); + assertOrderedEquals(actual, expected); + } }