RULE_ELEMENT_TYPES = //
+ PSIElementTypeFactory.getRuleIElementTypes(XLangScriptLanguage.INSTANCE);
+
+ public static final TokenIElementType TOKEN_Identifier = token(XLangLexer.Identifier);
+
+ public static final TokenSet TOKEN_comment = tokenSet(XLangLexer.SingleLineComment, XLangLexer.MultiLineComment);
+ public static final TokenSet TOKEN_whitespace = tokenSet(XLangLexer.WhiteSpaces, XLangLexer.LineTerminator);
+
+ public static final TokenSet TOKEN_literal_string = tokenSet(XLangLexer.StringLiteral,
+ XLangLexer.TemplateStringLiteral);
+
+ public static final TokenIElementType TOKEN_literal_boolean = token(XLangLexer.BooleanLiteral);
+ public static final TokenIElementType TOKEN_literal_decimal = token(XLangLexer.DecimalLiteral);
+ public static final TokenIElementType TOKEN_literal_regex = token(XLangLexer.RegularExpressionLiteral);
+ public static final TokenSet TOKEN_literal_integer = tokenSet(XLangLexer.BinaryIntegerLiteral,
+ XLangLexer.DecimalIntegerLiteral,
+ XLangLexer.HexIntegerLiteral);
+
+ public static final RuleIElementType RULE_ast_identifierOrPattern = rule(XLangParser.RULE_ast_identifierOrPattern);
+ public static final RuleIElementType RULE_expression_initializer = rule(XLangParser.RULE_expression_initializer);
+ public static final RuleIElementType RULE_moduleDeclaration_import
+ = rule(XLangParser.RULE_moduleDeclaration_import);
+ public static final RuleIElementType RULE_objectProperties = rule(XLangParser.RULE_objectProperties_);
+ public static final RuleIElementType RULE_namedTypeNode_annotation
+ = rule(XLangParser.RULE_namedTypeNode_annotation);
+ public static final RuleIElementType RULE_parameterizedTypeNode = rule(XLangParser.RULE_parameterizedTypeNode);
+
+ public static TokenSet tokenSet(int... types) {
+ return PSIElementTypeFactory.createTokenSet(XLangScriptLanguage.INSTANCE, types);
+ }
+
+ public static TokenIElementType token(int type) {
+ return TOKEN_ELEMENT_TYPES.get(type);
+ }
+
+ public static RuleIElementType rule(int type) {
+ return RULE_ELEMENT_TYPES.get(type);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrayExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrayExpressionNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..137d775be5e91269a654d308bc89b6ade1d54128
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrayExpressionNode.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * [a, b, c]
:
+ *
+ * RuleSpecNode(arrayExpression)
+ * PsiElement('[')('[')
+ * RuleSpecNode(elementList_)
+ * RuleSpecNode(ast_arrayElement)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(ast_arrayElement)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(ast_arrayElement)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('c')
+ * PsiElement(']')(']')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-04
+ */
+public class ArrayExpressionNode extends RuleSpecNode {
+
+ public ArrayExpressionNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ /** 获取数组元素类型 */
+ public PsiClass getElementType() {
+ // TODO 返回第一个不为 null 的元素类型
+ return null;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionBodyNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionBodyNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..cfb78195923dc62ac9f94491be015916a4524f9d
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionBodyNode.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class ArrowFunctionBodyNode extends RuleSpecNode {
+
+ public ArrowFunctionBodyNode(@NotNull ASTNode node) {
+ super(node);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..59c4cb6432cf656d19277a06395e75978623a757
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ArrowFunctionNode.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 箭头函数节点
+ *
+ * (a, b) => a + b
:
+ *
+ * ArrowFunctionNode(arrowFunctionExpression)
+ * PsiElement('(')('(')
+ * FunctionParameterListNode(parameterList_)
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(')')(')')
+ * PsiWhiteSpace(' ')
+ * PsiElement('=>')('=>')
+ * PsiWhiteSpace(' ')
+ * ArrowFunctionBodyNode(expression_functionBody)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiWhiteSpace(' ')
+ * PsiElement('+')('+')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class ArrowFunctionNode extends RuleSpecNode {
+
+ public ArrowFunctionNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ /** 获取函数的返回值类型 */
+ public PsiClass getReturnType() {
+ // TODO 分析 return 表达式,得到返回类型
+ return null;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a150ec04c5937c0fefa7078bf788121d0ff5811
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/AssignmentExpressionNode.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 变量赋值 xyz = "234";
:
+ *
+ * RuleSpecNode(assignmentExpression)
+ * RuleSpecNode(expression_leftHandSide)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('xyz')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(assignmentOperator_)
+ * PsiElement('=')('=')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_string)
+ * PsiElement(StringLiteral)('"234"')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ *
+ *
+ * 数组元素赋值 arr[0] = 'a';
:
+ *
+ * AssignmentExpressionNode(assignmentExpression)
+ * RuleSpecNode(expression_leftHandSide)
+ * RuleSpecNode(memberExpression)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('arr')
+ * PsiElement('[')('[')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('0')
+ * PsiElement(']')(']')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(assignmentOperator_)
+ * PsiElement('=')('=')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_string)
+ * PsiElement(StringLiteral)(''a'')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-04
+ */
+public class AssignmentExpressionNode extends RuleSpecNode {
+ private ExpressionNode expression;
+
+ public AssignmentExpressionNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public IdentifierNode getVarNameNode() {
+ RuleSpecNode node = (RuleSpecNode) getFirstChild().getFirstChild();
+ return node instanceof IdentifierNode ? (IdentifierNode) node : null;
+ }
+
+ public PsiClass getVarType() {
+ if (expression == null || !expression.isValid()) {
+ expression = findChildByClass(ExpressionNode.class);
+ }
+ return expression != null ? expression.getResultType() : null;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d253fb2b265d264f099ae9b265ee06d72640633
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/BlockStatementNode.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * { ... }
块节点
+ *
+ * 用于 try
、if
和函数体等节点
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class BlockStatementNode extends RuleSpecNode {
+
+ public BlockStatementNode(@NotNull ASTNode node) {
+ super(node);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..080fbaa1fe7b217d2617402994f9c9050fa7f912
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/CalleeArgumentsNode.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 函数调用的参数列表节点
+ *
+ * 参数列表 (1, 2)
:
+ *
+ * CalleeArgumentsNode(arguments_)
+ * PsiElement('(')('(')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('1')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('2')
+ * PsiElement(')')(')')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class CalleeArgumentsNode extends RuleSpecNode {
+
+ public CalleeArgumentsNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public PsiClass @NotNull [] getArgumentTypes() {
+ ExpressionNode[] exprs = findChildrenByClass(ExpressionNode.class);
+
+ PsiClass[] types = new PsiClass[exprs.length];
+ for (int i = 0; i < exprs.length; i++) {
+ ExpressionNode expr = exprs[i];
+
+ types[i] = expr.getResultType();
+ }
+ return types;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..673f57d9fccc11497d377b9c32ee128f5a92c6f3
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ExpressionNode.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiParameter;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.impl.PsiClassImplUtil;
+import io.nop.idea.plugin.lang.script.reference.IdentifierReference;
+import io.nop.idea.plugin.lang.script.reference.ObjectMemberReference;
+import io.nop.idea.plugin.lang.script.reference.ObjectMethodReference;
+import org.jetbrains.annotations.NotNull;
+
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_parameterizedTypeNode;
+
+/**
+ * 表达式节点
+ *
+ * {@link ObjectMemberNode 对象成员访问表达式}
+ * a.b.c
、a.b().c()
,
+ * 以及{@link ObjectDeclarationNode 对象声明表达式}
+ * {a, b: 2}
均从属于该类型节点
+ *
+ * 其叶子节点可以为引用的变量名(identifier 类型),也可以为字面量(literal 类型)。
+ * 其可以多层嵌套,例如,表达式 a.b.c(1, 2, 3) 包含以下子表达式:
+ *
+ * - a
+ * - a.b
+ * - a.b.c
+ * - 1
+ * - 2
+ * - 3
+ *
+ *
+ * 对象方法调用 a.b(c, d)
:
+ *
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement('.')('.')
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * CalleeArgumentsNode(arguments_)
+ * PsiElement('(')('(')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('c')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('d')
+ * PsiElement(')')(')')
+ *
+ *
+ * 函数调用 a(1, 2)
:
+ *
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * CalleeArgumentsNode(arguments_)
+ * PsiElement('(')('(')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('1')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('2')
+ * PsiElement(')')(')')
+ *
+ *
+ * 构造函数调用 new String("abc")
:
+ *
+ * ExpressionNode(expression_single)
+ * PsiElement('new')('new')
+ * PsiWhiteSpace(' ')
+ * ParameterizedTypeNode(parameterizedTypeNode)
+ * RuleSpecNode(qualifiedName_)
+ * RuleSpecNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('String')
+ * CalleeArgumentsNode(arguments_)
+ * PsiElement('(')('(')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_string)
+ * PsiElement(StringLiteral)('"abc"')
+ * PsiElement(')')(')')
+ *
+ *
+ * 访问对象的成员变量 a.b.c
:
+ *
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement('.')('.')
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement('.')('.')
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('c')
+ *
+ *
+ * 对象声明 {a, b: 1}
:
+ *
+ * ExpressionNode(expression_single)
+ * ObjectDeclarationNode(objectExpression)
+ * PsiElement('{')('{')
+ * RuleSpecNode(objectProperties_)
+ * ObjectPropertyDeclarationNode(ast_objectProperty)
+ * ObjectPropertyAssignmentNode(propertyAssignment)
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * ObjectPropertyDeclarationNode(ast_objectProperty)
+ * ObjectPropertyAssignmentNode(propertyAssignment)
+ * RuleSpecNode(expression_propName)
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(':')(':')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('1')
+ * PsiElement('}')('}')
+ *
+ *
+ * 箭头函数声明 (a, b) => a + b
:
+ *
+ * ExpressionNode(expression_single)
+ * ArrowFunctionNode(arrowFunctionExpression)
+ * PsiElement('(')('(')
+ * RuleSpecNode(parameterList_)
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(')')(')')
+ * PsiWhiteSpace(' ')
+ * PsiElement('=>')('=>')
+ * PsiWhiteSpace(' ')
+ * ArrowFunctionBodyNode(expression_functionBody)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiWhiteSpace(' ')
+ * PsiElement('+')('+')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ *
+ *
+ * 变量运算 a + b
:
+ *
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiWhiteSpace(' ')
+ * PsiElement('+')('+')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ *
+ *
+ * 变量运算 a > 2
:
+ *
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiWhiteSpace(' ')
+ * PsiElement('>')('>')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('2')
+ *
+ *
+ * 构造函数及其方法的调用 new String("def").trim()
:
+ *
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * PsiElement('new')('new')
+ * PsiWhiteSpace(' ')
+ * ParameterizedTypeNode(parameterizedTypeNode)
+ * RuleSpecNode(qualifiedName_)
+ * RuleSpecNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('String')
+ * CalleeArgumentsNode(arguments_)
+ * PsiElement('(')('(')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_string)
+ * PsiElement(StringLiteral)('"def"')
+ * PsiElement(')')(')')
+ * PsiElement('.')('.')
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('trim')
+ * CalleeArgumentsNode(arguments_)
+ * PsiElement('(')('(')
+ * PsiElement(')')(')')
+ *
+ *
+ * 数组声明 [a, b, c]
:
+ *
+ * ExpressionNode(expression_single)
+ * RuleSpecNode(arrayExpression)
+ * PsiElement('[')('[')
+ * RuleSpecNode(elementList_)
+ * RuleSpecNode(ast_arrayElement)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(ast_arrayElement)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(ast_arrayElement)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('c')
+ * PsiElement(']')(']')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class ExpressionNode extends RuleSpecNode {
+
+ public ExpressionNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ protected PsiReference @NotNull [] doGetReferences() {
+ // Note:
+ // - 仅识别当前表达式的最后一个有效元素的引用,其余部分,由其子表达式做识别处理
+ // - 对象声明节点 ObjectDeclarationNode 的相关引用,由其自身负责构造
+ // - 对构造函数中的 QualifiedNameRootNode 的相关引用,由其自身负责构造
+
+ PsiElement firstChild = getFirstChild();
+ // 变量引用:abc
+ if (firstChild instanceof IdentifierNode identifier) {
+ TextRange textRange = identifier.getTextRangeInParent();
+ IdentifierReference ref = new IdentifierReference(this, textRange, identifier);
+
+ return new PsiReference[] { ref };
+ }
+ // 对象方法调用:a.b.c(1, 2)
+ else if (isObjectMethodCall()) {
+ ObjectMethodReference ref = new ObjectMethodReference(this);
+
+ return new PsiReference[] { ref };
+ }
+ // 对象属性访问:a.b.c
+ else if (isObjectMemberAccess()) {
+ ObjectMemberReference ref = new ObjectMemberReference(this);
+
+ return new PsiReference[] { ref };
+ }
+ // 函数调用:fn1(1, 2, 3)
+ else if (isFunctionCall()) {
+ ExpressionNode callee = (ExpressionNode) firstChild;
+
+ TextRange textRange = callee.getTextRangeInParent();
+ IdentifierNode fn = (IdentifierNode) callee.getFirstChild();
+
+ IdentifierReference ref = new IdentifierReference(this, textRange, fn);
+
+ return new PsiReference[] { ref };
+ }
+
+ return PsiReference.EMPTY_ARRAY;
+ }
+
+ /**
+ * 获取表达式结果的类型
+ *
+ * @return null
为有效值
+ */
+ public PsiClass getResultType() {
+ PsiElement firstChild = getFirstChild();
+
+ if (isObjectConstructorCall()) {
+ RuleSpecNode ptn = findChildByType(RULE_parameterizedTypeNode);
+ QualifiedNameRootNode cons = ptn != null ? (QualifiedNameRootNode) ptn.getFirstChild() : null;
+
+ return cons != null ? cons.getQualifiedType() : null;
+ } //
+ else if (isObjectMethodCall()) {
+ PsiMethod method = getObjectMethod();
+ PsiType returnType = method != null ? method.getReturnType() : null;
+
+ return getPsiClassByPsiType(returnType);
+ } //
+ else if (isObjectMemberAccess()) {
+ PsiElement member = getObjectMember();
+ if (!(member instanceof PsiField prop)) {
+ return null;
+ }
+
+ PsiType propType = prop.getType();
+
+ return getPsiClassByPsiType(propType);
+ } //
+ else if (isFunctionCall()) {
+ ExpressionNode callee = (ExpressionNode) firstChild;
+ IdentifierNode fn = (IdentifierNode) callee.getFirstChild();
+
+ // Note: 对应的是函数的返回值类型
+ return fn.getVarType();
+ } //
+ else if (isArrowFunction()) {
+ ArrowFunctionNode fn = (ArrowFunctionNode) firstChild;
+
+ return fn.getReturnType();
+ } //
+ else if (isArrayInit()) {
+ ArrayExpressionNode array = (ArrayExpressionNode) firstChild;
+
+ return array.getElementType();
+ }
+
+ List types = new ArrayList<>();
+ PsiElement element = firstChild;
+ while (element != null) {
+ if (element instanceof LiteralNode l) {
+ types.add(l.getDataType());
+ } //
+ else if (element instanceof IdentifierNode i) {
+ types.add(i.getVarType());
+ } //
+ else if (element instanceof ExpressionNode e) {
+ types.add(e.getResultType());
+ }
+
+ element = element.getNextSibling();
+ }
+
+ if (types.size() == 1) {
+ return types.get(0);
+ }
+
+ // TODO 运算表达式,如 a + b
+ return null;
+ }
+
+ /**
+ * 当前表达式是否为对象成员(属性或方法)访问
+ *
+ * 从最后一个对象成员的视角向上观察
+ *
+ * 在父节点未包含 {@link CalleeArgumentsNode} 节点时,
+ * 当前对象成员可能是变量,也可能是方法
+ */
+ public boolean isObjectMemberAccess() {
+ // a.b.c
+ if (getFirstChild() instanceof ExpressionNode) {
+ return getLastChild() instanceof ObjectMemberNode //
+ && !(getParent().getLastChild() instanceof CalleeArgumentsNode);
+ }
+ return false;
+ }
+
+ /**
+ * 当前表达式是否为对象方法调用
+ *
+ * 从最后一个对象成员的视角向上观察
+ */
+ public boolean isObjectMethodCall() {
+ // a.b.c()
+ if (getFirstChild() instanceof ExpressionNode) {
+ return getLastChild() instanceof ObjectMemberNode //
+ && getParent().getLastChild() instanceof CalleeArgumentsNode;
+ }
+ return false;
+ }
+
+ /** 当前表达式是否为对象构造函数调用 */
+ public boolean isObjectConstructorCall() {
+ // new String("abc")
+ return getFirstChild().getText().equals("new") && getLastChild() instanceof CalleeArgumentsNode;
+ }
+
+ /** 当前表达式是否为函数调用 */
+ public boolean isFunctionCall() {
+ // a(1)
+ if (getFirstChild() instanceof ExpressionNode callee) {
+ return callee.getFirstChild() instanceof IdentifierNode //
+ && getLastChild() instanceof CalleeArgumentsNode;
+ }
+ return false;
+ }
+
+ /** 当前表达式是否为箭头函数 */
+ public boolean isArrowFunction() {
+ // (a, b) => a + b
+ return getFirstChild() instanceof ArrowFunctionNode;
+ }
+
+ /** 当前表达式是否为数组初始化 */
+ public boolean isArrayInit() {
+ // [1, 2, 3]
+ return getFirstChild() instanceof ArrayExpressionNode;
+ }
+
+ /** 获取对象的 class */
+ public PsiClass getObjectClass() {
+ ExpressionNode obj = (ExpressionNode) getFirstChild();
+
+ return obj.getResultType();
+ }
+
+ /** 获取对象的方法 */
+ public PsiMethod getObjectMethod() {
+ PsiMethod[] methods = getObjectMethods();
+
+ return filterMethodByArgs(methods, () -> ((ExpressionNode) getParent()).getObjectMethodArgumentTypes());
+ }
+
+ /** 获取对象的成员(属性或方法) */
+ public PsiElement getObjectMember() {
+ return getObjectMember((objClass, memberName) -> {
+ PsiField prop = PsiClassImplUtil.findFieldByName(objClass, memberName, true);
+ if (prop != null) {
+ return prop;
+ }
+
+ PsiMethod[] methods = PsiClassImplUtil.findMethodsByName(objClass, memberName, true);
+ return methods.length > 0 ? methods[0] : null;
+ }, null);
+ }
+
+ /** 获取对象成员在当前表达式中的 {@link TextRange} */
+ public TextRange getObjectMemberTextRange() {
+ ObjectMemberNode member = (ObjectMemberNode) getLastChild();
+
+ return member.getTextRangeInParent();
+ }
+
+ /** 获取对象的方法 */
+ protected PsiMethod @NotNull [] getObjectMethods() {
+ return getObjectMember((objClass, memberName) -> PsiClassImplUtil.findMethodsByName(objClass, memberName, true),
+ PsiMethod.EMPTY_ARRAY);
+ }
+
+ /** 获取调用参数的类型列表 */
+ protected PsiClass[] getObjectMethodArgumentTypes() {
+ CalleeArgumentsNode calleeArgs = (CalleeArgumentsNode) getLastChild();
+
+ return calleeArgs.getArgumentTypes();
+ }
+
+ protected T getObjectMember(BiFunction consumer, T defaultValue) {
+ PsiClass objClass = getObjectClass();
+ if (objClass == null) {
+ return defaultValue;
+ }
+
+ ObjectMemberNode member = (ObjectMemberNode) getLastChild();
+ String memberName = member.getText();
+
+ return consumer.apply(objClass, memberName);
+ }
+
+ protected PsiMethod filterMethodByArgs(PsiMethod[] methods, Supplier argsGetter) {
+ // 只有唯一的方法,则直接返回
+ if (methods.length == 1) {
+ return methods[0];
+ }
+
+ PsiClass[] args = argsGetter.get();
+ // 优先查找参数列表完全匹配的方法
+ for (PsiMethod method : methods) {
+ PsiParameter[] params = method.getParameterList().getParameters();
+
+ if (matchMethodParams(params, args)) {
+ return method;
+ }
+ }
+
+ // 再查找参数数量一致的方法
+ for (PsiMethod method : methods) {
+ if (method.getParameterList().getParametersCount() == args.length) {
+ return method;
+ }
+ }
+
+ // 若都不匹配,则取第一个
+ return methods.length > 0 ? methods[0] : null;
+ }
+
+ protected boolean matchMethodParams(PsiParameter[] params, PsiClass[] args) {
+ if (params.length != args.length) {
+ return false;
+ }
+
+ for (int i = 0; i < params.length; i++) {
+ PsiClass arg = args[i];
+ PsiClass param = getPsiClassByPsiType(params[i].getType());
+
+ if (arg == param) {
+ continue;
+ }
+
+ if (arg == null || param == null //
+ || !arg.isInheritor(param, true) //
+ ) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac684f68b78972bdce0af01ba7b458c559bd90b9
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionDeclarationNode.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 函数声明节点
+ *
+ * function fn(a, b) { return a + b; }
:
+ *
+ * FunctionDeclarationNode(functionDeclaration)
+ * PsiElement('function')('function')
+ * PsiWhiteSpace(' ')
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('fn')
+ * PsiElement('(')('(')
+ * FunctionParameterListNode(parameterList_)
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(')')(')')
+ * PsiWhiteSpace(' ')
+ * BlockStatementNode(blockStatement)
+ * PsiElement('{')('{')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(statements_)
+ * StatementNode(statement)
+ * ReturnStatementNode(returnStatement)
+ * PsiElement('return')('return')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiWhiteSpace(' ')
+ * PsiElement('+')('+')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ * PsiWhiteSpace(' ')
+ * PsiElement('}')('}')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class FunctionDeclarationNode extends RuleSpecNode {
+
+ public FunctionDeclarationNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public IdentifierNode getFunctionNameNode() {
+ return findChildByClass(IdentifierNode.class);
+ }
+
+ /** 获取函数的返回值类型 */
+ public PsiClass getReturnType() {
+ // TODO 分析函数的 return 表达式,得到返回类型
+ return null;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b536630e895e69290210e1579fd34f064a5525a
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterDeclarationNode.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import org.jetbrains.annotations.NotNull;
+
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_namedTypeNode_annotation;
+
+/**
+ * 函数参数定义节点
+ *
+ *
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ *
+ *
+ *
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * RuleSpecNode(namedTypeNode_annotation)
+ * PsiElement(':')(':')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(namedTypeNode)
+ * RuleSpecNode(typeNameNode_predefined)
+ * PsiElement('string')('string')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class FunctionParameterDeclarationNode extends RuleSpecNode {
+
+ public FunctionParameterDeclarationNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public IdentifierNode getParameterName() {
+ return (IdentifierNode) getFirstChild().getFirstChild();
+ }
+
+ public PsiClass getParameterType() {
+ RuleSpecNode typeNode = findChildByType(RULE_namedTypeNode_annotation);
+ if (typeNode == null) {
+ return null;
+ }
+
+ RuleSpecNode type = (RuleSpecNode) typeNode.getLastChild().getFirstChild();
+
+ if (type instanceof TypeNameNodePredefinedNode tp) {
+ return tp.getPredefinedType();
+ } //
+ else if (type instanceof QualifiedNameRootNode qnr) {
+ return qnr.getQualifiedType();
+ }
+ return null;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb4837ec0de0ceee687a2e9f808dee0c3043700f
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/FunctionParameterListNode.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 函数(含箭头函数)参数列表
+ *
+ * 参数类型未知 a, b
:
+ *
+ * FunctionParameterListNode(parameterList_)
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ *
+ *
+ * 含参数类型 a: string, b: number
:
+ *
+ * FunctionParameterListNode(parameterList_)
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * RuleSpecNode(namedTypeNode_annotation)
+ * PsiElement(':')(':')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(namedTypeNode)
+ * RuleSpecNode(typeNameNode_predefined)
+ * PsiElement('string')('string')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * RuleSpecNode(namedTypeNode_annotation)
+ * PsiElement(':')(':')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(namedTypeNode)
+ * RuleSpecNode(typeNameNode_predefined)
+ * PsiElement('number')('number')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-05
+ */
+public class FunctionParameterListNode extends RuleSpecNode {
+ // Note: 在 FunctionParameterDeclarationNode 中构造对参数类型的引用
+
+ public FunctionParameterListNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ /** 参数列表为函数内可访问的变量 */
+ @Override
+ public @NotNull Map getVars() {
+ FunctionParameterDeclarationNode[] paramDecls = findChildrenByClass(FunctionParameterDeclarationNode.class);
+
+ Map vars = new HashMap<>();
+ for (FunctionParameterDeclarationNode paramDecl : paramDecls) {
+ IdentifierNode paramName = paramDecl.getParameterName();
+ PsiClass paramType = paramDecl.getParameterType();
+
+ vars.put(paramName.getText(), new XLangVarDecl(paramType, paramName));
+ }
+
+ return vars;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..6ba36be011cc2f62140e98f498de92ef5f7b4c66
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/Identifier.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 标识符
+ *
+ * 一般为变量名字或导入的类名
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class Identifier extends LeafPsiElement {
+
+ public Identifier(@NotNull IElementType type, @NotNull CharSequence text) {
+ super(type, text);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..0cab09759e8d321907c79a234b026dd6cb3ac280
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/IdentifierNode.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.ElementManipulator;
+import com.intellij.psi.ElementManipulators;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import com.intellij.util.IncorrectOperationException;
+import io.nop.commons.util.StringHelper;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import io.nop.idea.plugin.utils.PsiClassHelper;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * {@link Identifier} 节点
+ *
+ * 该节点 AST 树为:
+ *
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-02
+ */
+public class IdentifierNode extends RuleSpecNode implements PsiNamedElement {
+
+ public IdentifierNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ /** Note: 只有返回非 null
值,才支持在 idea 中重命名该节点 */
+ @Override
+ public String getName() {
+ return getText();
+ }
+
+ @Override
+ public PsiElement setName(@NotNull String name) throws IncorrectOperationException {
+ ElementManipulator manipulator = ElementManipulators.getManipulator(this);
+
+ TextRange textRange = getTextRangeInParent().shiftLeft(getStartOffsetInParent());
+
+ return manipulator.handleContentChange(this, textRange, name);
+ }
+
+ /** 获取变量的类型 */
+ public PsiClass getVarType() {
+ XLangVarDecl varDecl = getVarDecl();
+
+ return varDecl != null ? varDecl.type() : null;
+ }
+
+ /** 获取变量的定义信息 */
+ public XLangVarDecl getVarDecl() {
+ String varName = getText();
+ XLangVarDecl decl = findVisibleVar(varName);
+
+ if (decl == null //
+ && varName.indexOf('.') < 0 //
+ && varName.indexOf('$') < 0 //
+ && StringHelper.isValidClassName(varName) //
+ ) {
+ // Note: java.lang 中的类不需要显式导入
+ PsiClass clazz = PsiClassHelper.findClass(this, "java.lang." + varName);
+ if (clazz != null) {
+ decl = new XLangVarDecl(clazz, clazz);
+ }
+ }
+
+ return decl;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..69fad2b832300cfc3ce182d81e97263f15b03bd7
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportDeclarationNode.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.Map;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import io.nop.idea.plugin.utils.PsiClassHelper;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * import xxx;
:
+ *
+ * ImportDeclarationNode(importAsDeclaration)
+ * PsiElement('import')('import')
+ * ImportSourceNode(ast_importSource)
+ * RuleSpecNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('java')
+ * PsiElement('.')('.')
+ * RuleSpecNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('lang')
+ * PsiElement('.')('.')
+ * RuleSpecNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('String')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-29
+ */
+public class ImportDeclarationNode extends RuleSpecNode {
+
+ public ImportDeclarationNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public @NotNull Map getVars() {
+ ImportSourceNode imp = findChildByClass(ImportSourceNode.class);
+
+ QualifiedNameNode qualifiedName = imp != null ? imp.getQualifiedName() : null;
+ if (qualifiedName == null) {
+ return Map.of();
+ }
+
+ String classFQN = qualifiedName.getFullyName();
+ PsiClass clazz = PsiClassHelper.findClass(this, classFQN);
+ if (clazz == null) {
+ return Map.of();
+ }
+
+ String varName = qualifiedName.getLastName();
+ XLangVarDecl varDecl = new XLangVarDecl(clazz, clazz);
+
+ return Map.of(varName, varDecl);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..86b28dbc79b4eb3fe9420ec8d2a2e3c82f62ab71
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ImportSourceNode.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiReference;
+import io.nop.idea.plugin.utils.PsiClassHelper;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * import
语句中导入的类名的节点
+ *
+ * java.lang.String
:
+ *
+ * ImportSourceNode(ast_importSource)
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('java')
+ * PsiElement('.')('.')
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('lang')
+ * PsiElement('.')('.')
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('String')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-29
+ */
+public class ImportSourceNode extends RuleSpecNode {
+
+ public ImportSourceNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public QualifiedNameNode getQualifiedName() {
+ return (QualifiedNameNode) getFirstChild();
+ }
+
+ /** 构造 Java 相关的引用对象,从而支持自动补全、引用跳转、文档显示等 */
+ @Override
+ protected PsiReference @NotNull [] doGetReferences() {
+ QualifiedNameNode qnn = getQualifiedName();
+ String fqn = qnn.getFullyName();
+
+ return PsiClassHelper.createJavaClassReferences(this, fqn, 0);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..539344a3db03dc3c80841c07a925d5265ff7986c
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/LiteralNode.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.antlr.intellij.adaptor.lexer.TokenIElementType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 字面量节点
+ *
+ * 该节点 AST 树为:
+ *
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('3')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-02
+ */
+public class LiteralNode extends RuleSpecNode {
+ private LeafPsiElement literal;
+
+ public LiteralNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public LeafPsiElement getLiteral() {
+ if (literal == null || !literal.isValid()) {
+ literal = (LeafPsiElement) PsiTreeUtil.getDeepestLast(this);
+ }
+ return literal;
+ }
+
+ /** 获取字面量的数据类型 */
+ public PsiClass getDataType() {
+ LeafPsiElement target = getLiteral();
+ TokenIElementType token = (TokenIElementType) target.getElementType();
+
+ return getPsiClassByToken(token);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..8173822583ac304749f6fd4937175a2a7ff8d8f8
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectDeclarationNode.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import io.nop.idea.plugin.lang.script.reference.IdentifierReference;
+import org.jetbrains.annotations.NotNull;
+
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_objectProperties;
+
+/**
+ * 对象声明节点
+ *
+ * {a, b: 1}
:
+ *
+ * ObjectDeclarationNode(objectExpression)
+ * PsiElement('{')('{')
+ * RuleSpecNode(objectProperties_)
+ * ObjectPropertyDeclarationNode(ast_objectProperty)
+ * ObjectPropertyAssignmentNode(propertyAssignment)
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * ObjectPropertyDeclarationNode(ast_objectProperty)
+ * ObjectPropertyAssignmentNode(propertyAssignment)
+ * RuleSpecNode(expression_propName)
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(':')(':')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('1')
+ * PsiElement('}')('}')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-02
+ */
+public class ObjectDeclarationNode extends RuleSpecNode {
+
+ public ObjectDeclarationNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ protected PsiReference @NotNull [] doGetReferences() {
+ RuleSpecNode props = findChildByType(RULE_objectProperties);
+ if (props == null) {
+ return PsiReference.EMPTY_ARRAY;
+ }
+
+ List result = new ArrayList<>();
+ for (PsiElement child : props.getChildren()) {
+ if (!(child instanceof ObjectPropertyDeclarationNode propDecl)) {
+ continue;
+ }
+
+ ObjectPropertyAssignmentNode prop = (ObjectPropertyAssignmentNode) propDecl.getFirstChild();
+ if (!prop.isShorthand()) {
+ continue;
+ }
+
+ TextRange textRange = propDecl.getTextRangeInParent();
+ IdentifierNode propNameNode = prop.getPropNameNode();
+
+ IdentifierReference ref = new IdentifierReference(this, textRange, propNameNode);
+
+ result.add(ref);
+ }
+
+ return result.toArray(PsiReference.EMPTY_ARRAY);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectMemberNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectMemberNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..e3c32f1037e8647c702296cb968057f17eb738fe
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectMemberNode.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 对象成员(包含成员变量和方法)节点
+ *
+ * 如 a.b.c()
、{b, c: 1}
中,
+ * b
和 c
均为该类型节点
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class ObjectMemberNode extends RuleSpecNode {
+
+ public ObjectMemberNode(@NotNull ASTNode node) {
+ super(node);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..47383a40fbaaa2189ad99e822aec26cd53fbbf49
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyAssignmentNode.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 对象的属性赋值节点
+ *
+ * 如 {a, b: 1}
中,a
和 b: 1
均为该类型节点
+ *
+ * a
:
+ *
+ * ObjectPropertyAssignmentNode(propertyAssignment)
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ *
+ *
+ * b: 1
:
+ *
+ * ObjectPropertyAssignmentNode(propertyAssignment)
+ * RuleSpecNode(expression_propName)
+ * ObjectMemberNode(identifier_ex)
+ * RuleSpecNode(identifierOrKeyword_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(':')(':')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('1')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class ObjectPropertyAssignmentNode extends RuleSpecNode {
+
+ public ObjectPropertyAssignmentNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public IdentifierNode getPropNameNode() {
+ PsiElement child = getFirstChild();
+
+ while (child != null) {
+ if (child instanceof IdentifierNode i) {
+ return i;
+ }
+ child = child.getFirstChild();
+ }
+ return null;
+ }
+
+ /** 是否为属性名与变量名相同时的简写形式 */
+ public boolean isShorthand() {
+ return getFirstChild() instanceof ObjectMemberNode;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..e445293dff5e8cfee02ddaa41078e8e74456a3c8
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ObjectPropertyDeclarationNode.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 对象属性声明节点
+ *
+ * 如 {a, b: 1}
中的
+ * a
、b: 1
均为该类型节点
+ *
+ * @author flytreeleft
+ * @date 2025-07-02
+ */
+public class ObjectPropertyDeclarationNode extends RuleSpecNode {
+
+ public ObjectPropertyDeclarationNode(@NotNull ASTNode node) {
+ super(node);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..e801cde2be08bddb0ee78d96953dcdf3397cc168
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ProgramNode.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 注意,当 XScript 是嵌入在 <c:script/> 标签中时,
+ * {@link #getContext()} 或 {@link #getParent()}
+ * 的结果为该标签节点,通过该节点向上可得到在 xlib 函数中定义的参数,
+ * 从而可将其用于代码补全、引用跳转等
+ *
+ * @author flytreeleft
+ * @date 2025-06-29
+ */
+public class ProgramNode extends RuleSpecNode {
+
+ public ProgramNode(@NotNull ASTNode node) {
+ super(node);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e375adfc8525416b1122d9d2ce122395cb0aa08
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameNode.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * java.lang.String
:
+ *
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('java')
+ * PsiElement('.')('.')
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('lang')
+ * PsiElement('.')('.')
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('String')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-04
+ */
+public class QualifiedNameNode extends RuleSpecNode {
+
+ public QualifiedNameNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public IdentifierNode getIdentifier() {
+ return (IdentifierNode) getFirstChild().getFirstChild();
+ }
+
+ public String getLastName() {
+ PsiElement last = PsiTreeUtil.getDeepestLast(this);
+
+ return last.getText();
+ }
+
+ public String getFullyName() {
+ return getText();
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..992e3299fcf26a4898861f4486ce7cd5462a0751
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/QualifiedNameRootNode.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import io.nop.idea.plugin.lang.script.reference.QualifiedNameReference;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 类名节点
+ *
+ * String
:
+ *
+ * QualifiedNameRootNode(qualifiedName_)
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('String')
+ *
+ *
+ * Abc.Def
:
+ *
+ * QualifiedNameRootNode(qualifiedName_)
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('Abc')
+ * PsiElement('.')('.')
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('Def')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class QualifiedNameRootNode extends RuleSpecNode {
+
+ public QualifiedNameRootNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public QualifiedNameNode getQualifiedName() {
+ return (QualifiedNameNode) getFirstChild();
+ }
+
+ @Override
+ protected PsiReference @NotNull [] doGetReferences() {
+ List result = createReferences();
+
+ return result.toArray(PsiReference.EMPTY_ARRAY);
+ }
+
+ public PsiClass getQualifiedType() {
+ List result = createReferences();
+ if (result.isEmpty()) {
+ return null;
+ }
+
+ String fqn = getText().replace(" ", "");
+ QualifiedNameReference ref = result.get(result.size() - 1);
+ PsiElement element = ref.resolve();
+
+ if (!(element instanceof PsiClass clazz)) {
+ return null;
+ }
+
+ String className = clazz.getQualifiedName();
+
+ return className != null //
+ && (fqn.equals(className) //
+ || className.endsWith('.' + fqn)) //
+ ? clazz : null;
+ }
+
+ protected List createReferences() {
+ QualifiedNameNode qnn = getQualifiedName();
+
+ List result = new ArrayList<>();
+ createReferences(null, qnn, 0, result);
+
+ return result;
+ }
+
+ protected void createReferences(
+ QualifiedNameReference parentReference, QualifiedNameNode qnn, int offset,
+ List result
+ ) {
+ IdentifierNode identifier = qnn.getIdentifier();
+ // Note: 取相对于 qnn 的 TextRange 并做偏移
+ TextRange textRange = identifier.getParent().getTextRangeInParent().shiftRight(offset);
+
+ QualifiedNameReference ref = new QualifiedNameReference(this, textRange, identifier, parentReference);
+ result.add(ref);
+
+ PsiElement sub = qnn.getLastChild();
+ if (!(sub instanceof QualifiedNameNode subQnn)) {
+ return;
+ }
+
+ createReferences(ref, subQnn, subQnn.getStartOffsetInParent() + offset, result);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..e2f98177292ce07a4e6c5997f4300cb972424de9
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/ReturnStatementNode.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * return
语句节点
+ *
+ * @author flytreeleft
+ * @date 2025-07-03
+ */
+public class ReturnStatementNode extends RuleSpecNode {
+
+ public ReturnStatementNode(@NotNull ASTNode node) {
+ super(node);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..3dcb530cc216d3f5ff1aa4124bb8411d4b773c78
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/RuleSpecNode.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.regex.Pattern;
+
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiType;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import io.nop.idea.plugin.lang.XLangVarScope;
+import io.nop.idea.plugin.utils.PsiClassHelper;
+import org.antlr.intellij.adaptor.lexer.RuleIElementType;
+import org.antlr.intellij.adaptor.lexer.TokenIElementType;
+import org.antlr.intellij.adaptor.psi.Trees;
+import org.jetbrains.annotations.NotNull;
+
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_boolean;
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_decimal;
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_integer;
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_regex;
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.TOKEN_literal_string;
+
+/**
+ * @author flytreeleft
+ * @date 2025-06-29
+ */
+public class RuleSpecNode extends ASTWrapperPsiElement implements XLangVarScope {
+
+ public RuleSpecNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ /** @return 不含注释和空白节点 */
+ @Override
+ public PsiElement @NotNull [] getChildren() {
+ return Trees.getChildren(this);
+ }
+
+ @Override
+ public PsiReference @NotNull [] getReferences() {
+ // Note: 不能直接缓存 PsiReference,否则,容易造成数据不一致
+
+ // 在没有写入动作时,才执行函数并返回结果,从而避免阻塞编辑操作
+ return ReadAction.compute(this::doGetReferences);
+ }
+
+ protected PsiReference @NotNull [] doGetReferences() {
+ return PsiReference.EMPTY_ARRAY;
+ }
+
+ public boolean isRuleType(RuleIElementType type) {
+ return getNode().getElementType() == type;
+ }
+
+ /** 获取在当前节点上可见的指定变量 */
+ public XLangVarDecl findVisibleVar(String varName) {
+ PsiElement node = this;
+
+ while (node instanceof RuleSpecNode) {
+ PsiElement parent = node.getParent();
+
+ // Note: 下层的变量优先于上层的变量
+ if (parent instanceof RuleSpecNode) {
+ PsiElement prev = node;
+
+ // 从当前节点开始往前查找,并选择靠得最近的变量
+ while (prev != null) {
+ prev = prev.getPrevSibling();
+ if (!(prev instanceof XLangVarScope scope)) {
+ continue;
+ }
+
+ XLangVarDecl var = scope.getVars().get(varName);
+ if (var != null) {
+ return var;
+ }
+ }
+ }
+ // 从所在的 标签中获取 xlib 函数的参数列表以及内置变量列表
+ else if (parent instanceof XLangVarScope scope) {
+ XLangVarDecl var = scope.getVars().get(varName);
+ if (var != null) {
+ return var;
+ }
+ }
+
+ node = parent;
+ }
+ return null;
+ }
+
+ protected PsiClass getPsiClassByPsiType(PsiType type) {
+ return PsiClassHelper.getTypeClass(this, type);
+ }
+
+ protected PsiClass getPsiClassByToken(TokenIElementType token) {
+ Class> clazz = null;
+
+ if (token == TOKEN_literal_boolean) {
+ clazz = Boolean.class;
+ } else if (token == TOKEN_literal_decimal) {
+ clazz = Float.class;
+ } else if (token == TOKEN_literal_regex) {
+ clazz = Pattern.class;
+ } else if (TOKEN_literal_integer.contains(token)) {
+ clazz = Integer.class;
+ } else if (TOKEN_literal_string.contains(token)) {
+ clazz = String.class;
+ }
+ return clazz != null ? PsiClassHelper.findClass(this, clazz.getName()) : null;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..58a6d928ce3de47c079539007cfd9f529cc290c7
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/StatementNode.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.Map;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 赋值、声明、调用、if/switch 等语句
+ *
+ * 定义变量 let b = 3;
:
+ *
+ * StatementNode(statement)
+ * VariableDeclarationNode(variableDeclaration)
+ * RuleSpecNode(varModifier_)
+ * PsiElement('let')('let')
+ * VariableDeclaratorsNode(variableDeclarators_)
+ * VariableDeclaratorNode(variableDeclarator)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * RuleSpecNode(expression_initializer)
+ * PsiElement('=')('=')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('3')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ *
+ *
+ * 函数调用 a(1, 2);
:
+ *
+ * StatementNode(statement)
+ * RuleSpecNode(expressionStatement)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * CalleeArgumentsNode(arguments_)
+ * PsiElement('(')('(')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('1')
+ * PsiElement(',')(',')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('2')
+ * PsiElement(')')(')')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ *
+ *
+ * 返回语句 return a + b + c;
:
+ *
+ * StatementNode(statement)
+ * ReturnStatementNode(returnStatement)
+ * PsiElement('return')('return')
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement('+')('+')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement('+')('+')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('c')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ *
+ *
+ * 函数定义 function fn2(a, b) { return a + b; }
:
+ *
+ * StatementNode(statement)
+ * FunctionDeclarationNode(functionDeclaration)
+ * PsiElement('function')('function')
+ * PsiWhiteSpace(' ')
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('fn2')
+ * PsiElement('(')('(')
+ * RuleSpecNode(parameterList_)
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * FunctionParameterDeclarationNode(parameterDeclaration)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * PsiElement(')')(')')
+ * PsiWhiteSpace(' ')
+ * BlockStatementNode(blockStatement)
+ * PsiElement('{')('{')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(statements_)
+ * StatementNode(statement)
+ * ReturnStatementNode(returnStatement)
+ * PsiElement('return')('return')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('a')
+ * PsiWhiteSpace(' ')
+ * PsiElement('+')('+')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('b')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ * PsiWhiteSpace(' ')
+ * PsiElement('}')('}')
+ *
+ *
+ * 赋值语句 xyz = "234";
:
+ *
+ * StatementNode(statement)
+ * RuleSpecNode(assignmentExpression)
+ * RuleSpecNode(expression_leftHandSide)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('xyz')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(assignmentOperator_)
+ * PsiElement('=')('=')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_string)
+ * PsiElement(StringLiteral)('"234"')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-03
+ */
+public class StatementNode extends RuleSpecNode {
+
+ public StatementNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ /** 声明的变量,或者函数及其返回值类型,均为可访问变量 */
+ @Override
+ public @NotNull Map getVars() {
+ PsiElement firstChild = getFirstChild();
+
+ if (firstChild instanceof VariableDeclarationNode varDecl) {
+ return varDecl.getVars();
+ } //
+ else if (firstChild instanceof FunctionDeclarationNode fnDecl) {
+ IdentifierNode varNameNode = fnDecl.getFunctionNameNode();
+ String varName = varNameNode.getText();
+ PsiClass varType = fnDecl.getReturnType();
+
+ XLangVarDecl varDecl = new XLangVarDecl(varType, varNameNode);
+
+ return Map.of(varName, varDecl);
+ } //
+ else if (firstChild instanceof AssignmentExpressionNode ass) {
+ IdentifierNode varNameNode = ass.getVarNameNode();
+ // Note: 对于数组元素的赋值,不做处理
+ if (varNameNode != null) {
+ String varName = varNameNode.getText();
+ PsiClass varType = ass.getVarType();
+
+ XLangVarDecl varDecl = new XLangVarDecl(varType, varNameNode);
+
+ return Map.of(varName, varDecl);
+ }
+ }
+
+ return Map.of();
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b441f72494cc836d2200d0584a6cf5bb5f5d9c7
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TopLevelStatementNode.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.Map;
+
+import com.intellij.lang.ASTNode;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import org.jetbrains.annotations.NotNull;
+
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_moduleDeclaration_import;
+
+/**
+ * 各种语句的根节点
+ *
+ * 赋值、函数、导入、try 等语句,各自均有唯一的根节点:
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ * RuleSpecNode(moduleDeclaration_import)
+ * ImportDeclarationNode(importAsDeclaration)
+ *
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ * StatementNode(statement)
+ * VariableDeclarationNode(variableDeclaration)
+ *
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ * StatementNode(statement)
+ * FunctionDeclarationNode(functionDeclaration)
+ *
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ * StatementNode(statement)
+ * RuleSpecNode(ifStatement)
+ *
+ *
+ * TopLevelStatementNode(ast_topLevelStatement)
+ * StatementNode(statement)
+ * RuleSpecNode(expressionStatement)
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class TopLevelStatementNode extends RuleSpecNode {
+
+ public TopLevelStatementNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public @NotNull Map getVars() {
+ RuleSpecNode firstChild = (RuleSpecNode) getFirstChild();
+
+ if (firstChild instanceof StatementNode s) {
+ return s.getVars();
+ } //
+ else if (firstChild.isRuleType(RULE_moduleDeclaration_import)) {
+ return ((ImportDeclarationNode) firstChild.getFirstChild()).getVars();
+ }
+ return Map.of();
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..c12ea3472161cab0c2458894d0d1c4fd138f21d6
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/TypeNameNodePredefinedNode.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import io.nop.idea.plugin.lang.script.reference.PredefinedTypeReference;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 预定义类型节点
+ *
+ *
+ * RuleSpecNode(typeNameNode_predefined)
+ * PsiElement('string')('string')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-05
+ */
+public class TypeNameNodePredefinedNode extends RuleSpecNode {
+
+ public TypeNameNodePredefinedNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public PsiElement getTypeName() {
+ return getLastChild();
+ }
+
+ public PsiClass getPredefinedType() {
+ String typeName = getTypeName().getText();
+
+ return PredefinedTypeReference.getPredefinedType(this, typeName);
+ }
+
+ @Override
+ protected PsiReference @NotNull [] doGetReferences() {
+ PsiElement typeName = getTypeName();
+ TextRange textRange = typeName.getTextRangeInParent();
+
+ PredefinedTypeReference ref = new PredefinedTypeReference(this, textRange, typeName);
+
+ return new PsiReference[] { ref };
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..c02e0733ad67ed2ca8cbfa2ec092f1f9ff13a32d
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclarationNode.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.Map;
+
+import com.intellij.lang.ASTNode;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 含 const
和 let
语句
+ *
+ * 多变量声明 let abc = 123, def = 456;
:
+ *
+ * VariableDeclarationNode(variableDeclaration)
+ * RuleSpecNode(varModifier_)
+ * PsiElement('let')('let')
+ * VariableDeclaratorsNode(variableDeclarators_)
+ * VariableDeclaratorNode(variableDeclarator)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('abc')
+ * RuleSpecNode(expression_initializer)
+ * PsiElement('=')('=')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('123')
+ * PsiElement(',')(',')
+ * VariableDeclaratorNode(variableDeclarator)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('def')
+ * RuleSpecNode(expression_initializer)
+ * PsiElement('=')('=')
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('456')
+ * RuleSpecNode(eos__)
+ * PsiElement(';')(';')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-06-30
+ */
+public class VariableDeclarationNode extends RuleSpecNode {
+ private VariableDeclaratorsNode declarators;
+
+ public VariableDeclarationNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public @NotNull Map getVars() {
+ if (declarators == null) {
+ declarators = findChildByClass(VariableDeclaratorsNode.class);
+ }
+ return declarators == null ? Map.of() : declarators.getVars();
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f20301a7490835b01d56bbf89d26087b49441c6
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorNode.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.Map;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiClass;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import org.jetbrains.annotations.NotNull;
+
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_ast_identifierOrPattern;
+import static io.nop.idea.plugin.lang.script.XLangScriptTokenTypes.RULE_expression_initializer;
+
+/**
+ * abc = 123
:
+ *
+ * VariableDeclaratorNode(variableDeclarator)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('abc')
+ * RuleSpecNode(expression_initializer)
+ * PsiElement('=')('=')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('123')
+ *
+ *
+ * map = new HashMap<String, List>()
:
+ *
+ * VariableDeclaratorNode(variableDeclarator)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('map')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(expression_initializer)
+ * PsiElement('=')('=')
+ * PsiWhiteSpace(' ')
+ * ExpressionNode(expression_single)
+ * PsiElement('new')('new')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(parameterizedTypeNode)
+ * QualifiedNameRootNode(qualifiedName_)
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('HashMap')
+ * RuleSpecNode(typeArguments_)
+ * PsiElement('<')('<')
+ * RuleSpecNode(namedTypeNode)
+ * QualifiedNameRootNode(qualifiedName_)
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('String')
+ * PsiElement(',')(',')
+ * PsiWhiteSpace(' ')
+ * RuleSpecNode(namedTypeNode)
+ * QualifiedNameRootNode(qualifiedName_)
+ * QualifiedNameNode(qualifiedName)
+ * RuleSpecNode(qualifiedName_name_)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('List')
+ * PsiElement('>')('>')
+ * CalleeArgumentsNode(arguments_)
+ * PsiElement('(')('(')
+ * PsiElement(')')(')')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-03
+ */
+public class VariableDeclaratorNode extends RuleSpecNode {
+ private IdentifierNode identifier;
+ private ExpressionNode expression;
+
+ public VariableDeclaratorNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ protected IdentifierNode getIdentifier() {
+ if (identifier == null || !identifier.isValid()) {
+ RuleSpecNode node = findChildByType(RULE_ast_identifierOrPattern);
+
+ identifier = node != null ? (IdentifierNode) node.getFirstChild() : null;
+ }
+ return identifier;
+ }
+
+ protected ExpressionNode getExpression() {
+ if (expression == null || !expression.isValid()) {
+ RuleSpecNode node = findChildByType(RULE_expression_initializer);
+
+ expression = node != null ? (ExpressionNode) node.getLastChild() : null;
+ }
+ return expression;
+ }
+
+ @Override
+ public @NotNull Map getVars() {
+ IdentifierNode identifier = getIdentifier();
+ ExpressionNode expression = getExpression();
+
+ String varName = identifier != null ? identifier.getText() : null;
+ PsiClass varType = expression != null ? expression.getResultType() : null;
+ // Note: 变量类型可以是 null,表示未知类型
+ if (varName == null) {
+ return Map.of();
+ }
+
+ XLangVarDecl varDecl = new XLangVarDecl(varType, identifier);
+
+ return Map.of(varName, varDecl);
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..8aa12cd08e7eb783b9cf4a3a2415676f2733cefb
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/psi/VariableDeclaratorsNode.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.psi;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 由逗号分隔的多个变量声明的节点
+ *
+ * 如 abc = 123, def = 456
:
+ *
+ * VariableDeclaratorsNode(variableDeclarators_)
+ * VariableDeclaratorNode(variableDeclarator)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('abc')
+ * RuleSpecNode(expression_initializer)
+ * PsiElement('=')('=')
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('123')
+ * PsiElement(',')(',')
+ * VariableDeclaratorNode(variableDeclarator)
+ * RuleSpecNode(ast_identifierOrPattern)
+ * IdentifierNode(identifier)
+ * PsiElement(Identifier)('def')
+ * RuleSpecNode(expression_initializer)
+ * PsiElement('=')('=')
+ * ExpressionNode(expression_single)
+ * ExpressionNode(expression_single)
+ * LiteralNode(literal)
+ * RuleSpecNode(literal_numeric)
+ * PsiElement(DecimalIntegerLiteral)('456')
+ *
+ *
+ * @author flytreeleft
+ * @date 2025-07-03
+ */
+public class VariableDeclaratorsNode extends RuleSpecNode {
+
+ public VariableDeclaratorsNode(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ @Override
+ public @NotNull Map getVars() {
+ Map vars = new HashMap<>();
+
+ for (PsiElement child : getChildren()) {
+ if (child instanceof VariableDeclaratorNode declarator) {
+ vars.putAll(declarator.getVars());
+ }
+ }
+ return vars;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java
new file mode 100644
index 0000000000000000000000000000000000000000..209c28002e0ec81c83def52cbaef73b84712732b
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/IdentifierReference.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.reference;
+
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import io.nop.idea.plugin.lang.XLangVarDecl;
+import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
+import io.nop.idea.plugin.lang.script.psi.IdentifierNode;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * {@link IdentifierNode} 的引用
+ *
+ * 涉及对变量和类名的引用
+ *
+ * @author flytreeleft
+ * @date 2025-07-06
+ */
+public class IdentifierReference extends XLangReferenceBase {
+ private final IdentifierNode identifier;
+
+ public IdentifierReference(PsiElement myElement, TextRange myRangeInElement, IdentifierNode identifier) {
+ super(myElement, myRangeInElement);
+ this.identifier = identifier;
+ }
+
+ public IdentifierNode getIdentifier() {
+ return this.identifier;
+ }
+
+ @Override
+ public @Nullable PsiElement resolveInner() {
+ if (!identifier.isValid()) {
+ return null;
+ }
+
+ XLangVarDecl varDecl = identifier.getVarDecl();
+
+ return varDecl != null ? varDecl.element() : null;
+ }
+}
diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0dbc723fb481be3399d5e025c9a159d3421c595
--- /dev/null
+++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/script/reference/ObjectMemberReference.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2017-2025 Nop Platform. All rights reserved.
+ * Author: canonical_entropy@163.com
+ * Blog: https://www.zhihu.com/people/canonical-entropy
+ * Gitee: https://gitee.com/canonical-entropy/nop-entropy
+ * Github: https://github.com/entropy-cloud/nop-entropy
+ */
+
+package io.nop.idea.plugin.lang.script.reference;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import com.intellij.codeInsight.completion.InsertHandler;
+import com.intellij.codeInsight.completion.JavaLookupElementBuilder;
+import com.intellij.codeInsight.completion.PrioritizedLookupElement;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.CommonClassNames;
+import com.intellij.psi.ElementManipulator;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiField;
+import com.intellij.psi.PsiMember;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil;
+import com.intellij.psi.util.MethodSignatureBackedByPsiMethod;
+import com.intellij.util.IncorrectOperationException;
+import io.nop.idea.plugin.lang.reference.XLangReferenceBase;
+import io.nop.idea.plugin.lang.script.psi.ExpressionNode;
+import one.util.streamex.StreamEx;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil.getMethodSignature;
+import static com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil.isClassWithName;
+
+/**
+ * 对象成员(属性或方法)引用
+ *
+ * @author flytreeleft
+ * @date 2025-07-06
+ */
+public class ObjectMemberReference extends XLangReferenceBase {
+
+ public ObjectMemberReference(ExpressionNode myElement) {
+ super(myElement, null);
+ }
+
+ @Override
+ protected TextRange calculateDefaultRangeInElement() {
+ return ((ExpressionNode) myElement).getObjectMemberTextRange();
+ }
+
+ @Override
+ public @Nullable PsiElement resolveInner() {
+ return ((ExpressionNode) myElement).getObjectMember();
+ }
+
+ @Override
+ public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
+ PsiElement element = myElement;
+ TextRange rangeInElement = getRangeInElement();
+
+ ElementManipulator manipulator = getManipulator(element);
+ element = manipulator.handleContentChange(element, rangeInElement, newElementName);
+
+ rangeInElement = ((ExpressionNode) element).getObjectMemberTextRange();
+ setRangeInElement(rangeInElement);
+
+ return element;
+ }
+
+ @Override
+ public Object @NotNull [] getVariants() {
+ // Note: 只有在当前引用的结果不存在时,才需要补全,而其可补全项由上层引用结果确定
+ PsiClass clazz = ((ExpressionNode) myElement).getObjectClass();
+ if (clazz == null) {
+ return LookupElement.EMPTY_ARRAY;
+ }
+
+ List