mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 21:17:32 +00:00
1 parent
c00ad8e0a7
commit
f01bc0a117
4 files changed
+1166
-49
No files matched your search
+814
@@ -0,0 +1,814 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class for JavaScript object literal (AKA dicts) analysis, parsing, type-shape inference,
|
||||
* and dynamic property access handling.
|
||||
*
|
||||
* <h3>Purpose</h3>
|
||||
* Encapsulates the logic for:
|
||||
* <ul>
|
||||
* <li>Parsing object literal syntax ({@code { key: value, ... }}) into structured property lists.</li>
|
||||
* <li>Inferring synthetic {@link TypeInfo} shapes from object literal contents for hover/autocomplete.</li>
|
||||
* <li>Parsing dynamic property accesses ({@code obj.prop}, {@code obj["prop"]}) on the assignment LHS.</li>
|
||||
* <li>Extending synthetic object shapes when new properties are assigned after initial declaration.</li>
|
||||
* <li>Detecting synthetic object literal display names for UI rendering.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>V1 Scope</h3>
|
||||
* <ul>
|
||||
* <li>Only simple identifier keys and quoted-string keys are supported. Computed keys ({@code [expr]: val})
|
||||
* and spread ({@code ...obj}) are recognized but gracefully skipped.</li>
|
||||
* <li>Nested object literals inside values degrade to {@link TypeInfo#ANY} rather than recursive inference.</li>
|
||||
* <li>Value type inference delegates to the caller-supplied {@link ExpressionTypeResolverFn}; this class
|
||||
* does not resolve types itself.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Degradation Behavior</h3>
|
||||
* When the parser encounters unsupported syntax (computed keys, spread, deeply nested literals),
|
||||
* it sets {@link ObjectLiteralAnalysis#supportsInference} to {@code false} and returns partial
|
||||
* property data. Callers (e.g. mark building) can still use the parsed properties for key highlighting
|
||||
* even when full type inference is unavailable.
|
||||
*
|
||||
* <h3>Architecture Note</h3>
|
||||
* Methods that need access to ScriptDocument's type resolution ({@code resolveExpressionType},
|
||||
* {@code resolveVariable}) accept functional interfaces ({@link ExpressionTypeResolverFn},
|
||||
* {@link VariableResolverFn}) so that this class remains decoupled from ScriptDocument's internals.
|
||||
* ScriptDocument retains ownership of {@code markObjectLiteralKeys} and
|
||||
* {@code findMatchingBraceEndInDocument} which need direct access to document state (excluded ranges, text).
|
||||
*/
|
||||
public final class ObjectLiteralParser {
|
||||
|
||||
private ObjectLiteralParser() {
|
||||
// Static utility class — not instantiable
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Functional interfaces for ScriptDocument callbacks
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Callback to resolve an expression's type given the expression text and its absolute position
|
||||
* within the document. Delegates to {@code ScriptDocument.resolveExpressionType(expr, position)}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ExpressionTypeResolverFn {
|
||||
TypeInfo resolve(String expr, int position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to resolve a variable by name at a given document position.
|
||||
* Delegates to {@code ScriptDocument.resolveVariable(name, position)}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface VariableResolverFn {
|
||||
FieldInfo resolve(String name, int position);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Data classes
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Result of analyzing an object literal expression.
|
||||
*
|
||||
* <p>Contains the parsed properties, whether full type inference is feasible,
|
||||
* and (when inference succeeded) the synthetic {@link TypeInfo} representing the object's shape.</p>
|
||||
*/
|
||||
public static final class ObjectLiteralAnalysis {
|
||||
/** Parsed properties (key name, offsets, optional inferred value type). */
|
||||
public final List<ObjectLiteralProperty> properties;
|
||||
/**
|
||||
* {@code true} when every property was fully parseable and the closing brace was found,
|
||||
* meaning a synthetic type shape can be constructed. {@code false} when unsupported syntax
|
||||
* was encountered or the literal is incomplete.
|
||||
*/
|
||||
public final boolean supportsInference;
|
||||
/**
|
||||
* The synthetic {@link TypeInfo} representing this object literal's shape, or {@code null}
|
||||
* when inference was not requested or not supported.
|
||||
*/
|
||||
public final TypeInfo inferredType;
|
||||
|
||||
ObjectLiteralAnalysis(List<ObjectLiteralProperty> properties, boolean supportsInference, TypeInfo inferredType) {
|
||||
this.properties = properties;
|
||||
this.supportsInference = supportsInference;
|
||||
this.inferredType = inferredType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single property inside an object literal.
|
||||
*
|
||||
* <p>Tracks the key name, whether the key was written as a bare identifier or a quoted string,
|
||||
* the absolute document offsets for both key and value spans, and the inferred value type
|
||||
* (when type inference was requested).</p>
|
||||
*/
|
||||
public static final class ObjectLiteralProperty {
|
||||
/** The property key name (without quotes if originally quoted). */
|
||||
public final String keyName;
|
||||
/** {@code true} if the key was a bare identifier; {@code false} if it was a quoted string. */
|
||||
public final boolean isIdentifierKey;
|
||||
/** Absolute start offset of the key in the document. */
|
||||
public final int keyStartAbs;
|
||||
/** Absolute end offset of the key in the document. */
|
||||
public final int keyEndAbs;
|
||||
/** Absolute start offset of the value expression in the document. */
|
||||
public final int valueStartAbs;
|
||||
/** Absolute end offset of the value expression in the document. */
|
||||
public final int valueEndAbs;
|
||||
/** The inferred type of the value expression, or {@code null} if inference was not requested. */
|
||||
public final TypeInfo valueType;
|
||||
|
||||
ObjectLiteralProperty(String keyName, boolean isIdentifierKey, int keyStartAbs, int keyEndAbs,
|
||||
int valueStartAbs, int valueEndAbs, TypeInfo valueType) {
|
||||
this.keyName = keyName;
|
||||
this.isIdentifierKey = isIdentifierKey;
|
||||
this.keyStartAbs = keyStartAbs;
|
||||
this.keyEndAbs = keyEndAbs;
|
||||
this.valueStartAbs = valueStartAbs;
|
||||
this.valueEndAbs = valueEndAbs;
|
||||
this.valueType = valueType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a dynamic property access on the LHS of an assignment.
|
||||
*
|
||||
* <p>Two forms are supported:</p>
|
||||
* <ul>
|
||||
* <li>Dot access: {@code obj.prop} → {@code receiverExpr="obj", propertyName="prop", bracketAccess=false}</li>
|
||||
* <li>Bracket access: {@code obj["prop"]} → {@code receiverExpr="obj", propertyName="prop", bracketAccess=true}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final class DynamicPropertyAccess {
|
||||
/** The expression for the receiver object (everything before the dot or bracket). */
|
||||
public final String receiverExpr;
|
||||
/** The property name being accessed. */
|
||||
public final String propertyName;
|
||||
/** Absolute document offset where the receiver expression starts. */
|
||||
public final int receiverStart;
|
||||
/** {@code true} if this is bracket notation ({@code obj["prop"]}); {@code false} for dot notation. */
|
||||
public final boolean bracketAccess;
|
||||
|
||||
DynamicPropertyAccess(String receiverExpr, String propertyName, int receiverStart, boolean bracketAccess) {
|
||||
this.receiverExpr = receiverExpr;
|
||||
this.propertyName = propertyName;
|
||||
this.receiverStart = receiverStart;
|
||||
this.bracketAccess = bracketAccess;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Object literal analysis
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Analyze an object literal string and extract its properties.
|
||||
*
|
||||
* <p>The input is expected to start with {@code '{'} (after optional leading whitespace) and
|
||||
* may or may not be a complete literal. Properties are parsed into {@link ObjectLiteralProperty}
|
||||
* records. When {@code inferTypes} is {@code true} and inference is feasible, a synthetic
|
||||
* {@link TypeInfo} shape is constructed with fields matching the parsed properties.</p>
|
||||
*
|
||||
* @param objectLiteral the source text of the object literal (starting at or near '{')
|
||||
* @param baseOffset the absolute document offset corresponding to the start of {@code objectLiteral}
|
||||
* @param inferTypes if {@code true}, attempt to resolve value types and build a synthetic shape
|
||||
* @param assumeComplete if {@code true}, treat the entire string as a complete literal (no brace matching needed)
|
||||
* @param typeResolver callback for resolving value expression types (may be {@code null} if {@code inferTypes} is false)
|
||||
* @return the analysis result, or {@code null} if the input is not a valid object literal start
|
||||
*/
|
||||
public static ObjectLiteralAnalysis parse(String objectLiteral, int baseOffset,
|
||||
boolean inferTypes, boolean assumeComplete,
|
||||
ExpressionTypeResolverFn typeResolver) {
|
||||
if (objectLiteral == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int leading = 0;
|
||||
while (leading < objectLiteral.length() && Character.isWhitespace(objectLiteral.charAt(leading))) {
|
||||
leading++;
|
||||
}
|
||||
int trailing = objectLiteral.length();
|
||||
while (trailing > leading && Character.isWhitespace(objectLiteral.charAt(trailing - 1))) {
|
||||
trailing--;
|
||||
}
|
||||
|
||||
String src = objectLiteral.substring(leading, trailing);
|
||||
int absBase = baseOffset + leading;
|
||||
|
||||
if (src.isEmpty() || src.charAt(0) != '{') {
|
||||
return null;
|
||||
}
|
||||
|
||||
int endExclusive;
|
||||
if (assumeComplete) {
|
||||
endExclusive = src.length();
|
||||
} else {
|
||||
endExclusive = findMatchingObjectLiteralEnd(src);
|
||||
if (endExclusive <= 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
boolean supportsInference = (endExclusive == src.length());
|
||||
int bodyStart = 1;
|
||||
int bodyEnd = endExclusive - 1;
|
||||
|
||||
List<ObjectLiteralProperty> props = new ArrayList<>();
|
||||
boolean sawUnsupported = false;
|
||||
|
||||
int i = bodyStart;
|
||||
while (i < bodyEnd) {
|
||||
while (i < bodyEnd) {
|
||||
char c = src.charAt(i);
|
||||
if (Character.isWhitespace(c) || c == ',') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (i >= bodyEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
int keyStart = i;
|
||||
String keyName = null;
|
||||
boolean isIdentifierKey = false;
|
||||
|
||||
char kc = src.charAt(i);
|
||||
if (kc == '"' || kc == '\'') {
|
||||
int keyEnd = scanStringLiteralEnd(src, i);
|
||||
if (keyEnd < 0 || keyEnd > bodyEnd) {
|
||||
sawUnsupported = true;
|
||||
break;
|
||||
}
|
||||
keyName = src.substring(i + 1, keyEnd - 1);
|
||||
isIdentifierKey = false;
|
||||
if (!isSimpleIdentifier(keyName)) {
|
||||
sawUnsupported = true;
|
||||
i = scanToNextTopLevelComma(src, keyEnd, bodyEnd);
|
||||
continue;
|
||||
}
|
||||
i = keyEnd;
|
||||
} else if (Character.isJavaIdentifierStart(kc)) {
|
||||
i++;
|
||||
while (i < bodyEnd && Character.isJavaIdentifierPart(src.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
keyName = src.substring(keyStart, i);
|
||||
isIdentifierKey = true;
|
||||
} else if (kc == '[') {
|
||||
// Computed key — unsupported for inference, skip to next property
|
||||
sawUnsupported = true;
|
||||
i = scanToNextTopLevelComma(src, i, bodyEnd);
|
||||
continue;
|
||||
} else if (kc == '.' && i + 2 < bodyEnd && src.charAt(i + 1) == '.' && src.charAt(i + 2) == '.') {
|
||||
// Spread syntax — unsupported, skip
|
||||
sawUnsupported = true;
|
||||
i = scanToNextTopLevelComma(src, i, bodyEnd);
|
||||
continue;
|
||||
} else {
|
||||
sawUnsupported = true;
|
||||
i = scanToNextTopLevelComma(src, i, bodyEnd);
|
||||
continue;
|
||||
}
|
||||
|
||||
int keyEnd = i;
|
||||
while (i < bodyEnd && Character.isWhitespace(src.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i >= bodyEnd || src.charAt(i) != ':') {
|
||||
// Shorthand property or invalid — skip
|
||||
sawUnsupported = true;
|
||||
i = scanToNextTopLevelComma(src, i, bodyEnd);
|
||||
continue;
|
||||
}
|
||||
i++; // skip ':'
|
||||
|
||||
while (i < bodyEnd && Character.isWhitespace(src.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
int valueStart = i;
|
||||
int valueEnd = scanTopLevelValueEnd(src, valueStart, bodyEnd);
|
||||
if (valueEnd < valueStart) {
|
||||
sawUnsupported = true;
|
||||
break;
|
||||
}
|
||||
|
||||
TypeInfo valueType = null;
|
||||
if (inferTypes && typeResolver != null) {
|
||||
String valueExpr = src.substring(valueStart, valueEnd).trim();
|
||||
if (!valueExpr.isEmpty()) {
|
||||
char vc = valueExpr.charAt(0);
|
||||
if (vc == '{' || vc == '[') {
|
||||
// Nested object/array literals degrade to 'any'
|
||||
valueType = TypeInfo.ANY;
|
||||
} else {
|
||||
TypeInfo inferred = typeResolver.resolve(valueExpr, absBase + valueStart);
|
||||
valueType = (inferred != null) ? inferred : TypeInfo.ANY;
|
||||
}
|
||||
} else {
|
||||
valueType = TypeInfo.ANY;
|
||||
}
|
||||
}
|
||||
|
||||
int keyStartAbs = absBase + keyStart;
|
||||
int keyEndAbs = absBase + keyEnd;
|
||||
int valueStartAbs = absBase + valueStart;
|
||||
int valueEndAbs = absBase + valueEnd;
|
||||
|
||||
if (keyName != null) {
|
||||
props.add(new ObjectLiteralProperty(keyName, isIdentifierKey, keyStartAbs, keyEndAbs,
|
||||
valueStartAbs, valueEndAbs, valueType));
|
||||
}
|
||||
|
||||
i = valueEnd;
|
||||
while (i < bodyEnd && Character.isWhitespace(src.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
if (i < bodyEnd && src.charAt(i) == ',') {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (sawUnsupported) {
|
||||
supportsInference = false;
|
||||
}
|
||||
|
||||
TypeInfo inferredType = null;
|
||||
if (inferTypes && supportsInference) {
|
||||
inferredType = TypeInfo.objectLiteral(props);
|
||||
}
|
||||
|
||||
return new ObjectLiteralAnalysis(props, supportsInference, inferredType);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Synthetic object literal display name detection
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Determine whether a {@link JSTypeInfo} represents a synthetic object literal type,
|
||||
* and if so return its display name ({@code "Object"}).
|
||||
*
|
||||
* <p>Synthetic object literal types are created by {@link #parse} with
|
||||
* names of the form {@code __ObjectLiteral$<offset>} under the {@code __synthetic__} namespace.
|
||||
* This method recognizes that naming convention and maps it to a human-readable display name.</p>
|
||||
*
|
||||
* @param jsType the JS type info to inspect (may be {@code null})
|
||||
* @return {@code "Object"} if this is a synthetic object literal type, {@code null} otherwise
|
||||
*/
|
||||
public static String getSyntheticObjectLiteralDisplayName(JSTypeInfo jsType) {
|
||||
if (jsType == null) {
|
||||
return null;
|
||||
}
|
||||
String namespace = jsType.getNamespace();
|
||||
String simple = jsType.getSimpleName();
|
||||
if ("__synthetic__".equals(namespace) && simple != null && simple.startsWith("__ObjectLiteral$")) {
|
||||
return "Object";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Dynamic property access parsing
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse a left-hand-side expression to detect dynamic property access patterns.
|
||||
*
|
||||
* <p>Recognizes two forms:</p>
|
||||
* <ul>
|
||||
* <li>Dot access: {@code obj.prop} — the last dot-separated segment is treated as the property name.</li>
|
||||
* <li>Bracket access: {@code obj["prop"]} or {@code obj['prop']} — the quoted string inside brackets
|
||||
* is treated as the property name.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Only simple identifier property names are accepted. Complex expressions (e.g., {@code obj[expr]}
|
||||
* where {@code expr} is not a string literal) return {@code null}.</p>
|
||||
*
|
||||
* @param lhs the left-hand-side expression text
|
||||
* @param lhsStart the absolute document offset of the LHS start
|
||||
* @return a {@link DynamicPropertyAccess} if a valid pattern was detected, {@code null} otherwise
|
||||
*/
|
||||
public static DynamicPropertyAccess parseDynamicPropertyAccess(String lhs, int lhsStart) {
|
||||
if (lhs == null || lhs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int dotIndex = lhs.lastIndexOf('.');
|
||||
int bracketIndex = lhs.lastIndexOf('[');
|
||||
|
||||
if (dotIndex >= 0 && dotIndex > bracketIndex) {
|
||||
String receiverExpr = lhs.substring(0, dotIndex).trim();
|
||||
String propertyName = lhs.substring(dotIndex + 1).trim();
|
||||
if (!receiverExpr.isEmpty() && isSimpleIdentifier(propertyName)) {
|
||||
return new DynamicPropertyAccess(receiverExpr, propertyName, lhsStart, false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bracketIndex >= 0 && lhs.endsWith("]")) {
|
||||
String receiverExpr = lhs.substring(0, bracketIndex).trim();
|
||||
String keyExpr = lhs.substring(bracketIndex + 1, lhs.length() - 1).trim();
|
||||
String propertyName = extractQuotedPropertyName(keyExpr);
|
||||
if (!receiverExpr.isEmpty() && propertyName != null && isSimpleIdentifier(propertyName)) {
|
||||
return new DynamicPropertyAccess(receiverExpr, propertyName, lhsStart, true);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the receiver type for a dynamic property access by first trying expression type resolution
|
||||
* and then falling back to variable resolution.
|
||||
*
|
||||
* @param access the dynamic property access to resolve
|
||||
* @param typeResolver callback for expression type resolution
|
||||
* @param variableResolver callback for variable resolution
|
||||
* @return the resolved receiver {@link TypeInfo}, or {@code null} if resolution failed
|
||||
*/
|
||||
public static TypeInfo resolveDynamicPropertyReceiverType(DynamicPropertyAccess access,
|
||||
ExpressionTypeResolverFn typeResolver,
|
||||
VariableResolverFn variableResolver) {
|
||||
if (access == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TypeInfo receiverType = typeResolver.resolve(access.receiverExpr, access.receiverStart);
|
||||
if (receiverType != null && receiverType.isResolved()) {
|
||||
return receiverType;
|
||||
}
|
||||
|
||||
FieldInfo receiverField = variableResolver.resolve(access.receiverExpr, access.receiverStart);
|
||||
if (receiverField != null) {
|
||||
return receiverField.getTypeInfo();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a property name from a quoted string expression.
|
||||
*
|
||||
* <p>Handles both single and double quotes, plus basic backslash escapes.
|
||||
* Returns {@code null} if the expression is not a properly quoted string.</p>
|
||||
*
|
||||
* @param expr the expression to extract from (e.g., {@code "prop"} or {@code 'prop'})
|
||||
* @return the unquoted, unescaped property name, or {@code null} if extraction failed
|
||||
*/
|
||||
public static String extractQuotedPropertyName(String expr) {
|
||||
if (expr == null || expr.length() < 2) {
|
||||
return null;
|
||||
}
|
||||
char quote = expr.charAt(0);
|
||||
if ((quote != '"' && quote != '\'') || expr.charAt(expr.length() - 1) != quote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 1; i < expr.length() - 1; i++) {
|
||||
char c = expr.charAt(i);
|
||||
if (c == '\\') {
|
||||
if (i + 1 >= expr.length() - 1) {
|
||||
return null;
|
||||
}
|
||||
char escaped = expr.charAt(++i);
|
||||
sb.append(escaped);
|
||||
continue;
|
||||
}
|
||||
sb.append(c);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend a synthetic object literal shape with a new property (or update an existing one).
|
||||
*
|
||||
* <p>This is called when an assignment like {@code obj.newProp = value} is encountered and
|
||||
* {@code obj} was previously inferred as a synthetic object literal type. The new property
|
||||
* is added to the underlying {@link JSTypeInfo} so subsequent hover/autocomplete sees it.</p>
|
||||
*
|
||||
* <p>Only operates on types that are recognized as synthetic object literals
|
||||
* (see {@link #getSyntheticObjectLiteralDisplayName}). Returns {@code false} and does nothing
|
||||
* if the receiver type is not a synthetic object literal.</p>
|
||||
*
|
||||
* @param receiverType the type of the receiver object
|
||||
* @param propertyName the name of the property to add/update
|
||||
* @param propertyType the inferred type of the property value (defaults to {@link TypeInfo#ANY} if null/unresolved)
|
||||
* @return {@code true} if the shape was extended/updated, {@code false} if the receiver is not a synthetic object literal
|
||||
*/
|
||||
public static boolean extendSyntheticObjectShape(TypeInfo receiverType, String propertyName, TypeInfo propertyType) {
|
||||
if (receiverType == null || propertyName == null || propertyName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!receiverType.isSyntheticObjectLiteralType()) {
|
||||
return false;
|
||||
}
|
||||
TypeInfo safeType = (propertyType != null && propertyType.isResolved()) ? propertyType : TypeInfo.ANY;
|
||||
receiverType.addOrUpdateSyntheticField(propertyName, safeType);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final class DynamicFieldResult {
|
||||
public final TypeInfo receiverType;
|
||||
public final String propertyName;
|
||||
public final FieldInfo field;
|
||||
|
||||
DynamicFieldResult(TypeInfo receiverType, String propertyName, FieldInfo field) {
|
||||
this.receiverType = receiverType;
|
||||
this.propertyName = propertyName;
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
public static DynamicFieldResult resolveExistingField(DynamicPropertyAccess access,
|
||||
ExpressionTypeResolverFn typeResolver, VariableResolverFn varResolver) {
|
||||
TypeInfo receiver = resolveDynamicPropertyReceiverType(access, typeResolver, varResolver);
|
||||
FieldInfo field = (receiver != null && receiver.isResolved()) ? receiver.getFieldInfo(access.propertyName) : null;
|
||||
return new DynamicFieldResult(receiver, access.propertyName, field);
|
||||
}
|
||||
|
||||
public static DynamicFieldResult extendAndGetField(DynamicPropertyAccess access,
|
||||
TypeInfo existingReceiver, TypeInfo sourceType,
|
||||
ExpressionTypeResolverFn typeResolver, VariableResolverFn varResolver) {
|
||||
TypeInfo receiver = (existingReceiver != null && existingReceiver.isResolved())
|
||||
? existingReceiver
|
||||
: resolveDynamicPropertyReceiverType(access, typeResolver, varResolver);
|
||||
if (!extendSyntheticObjectShape(receiver, access.propertyName, sourceType)) return null;
|
||||
return new DynamicFieldResult(receiver, access.propertyName, receiver.getFieldInfo(access.propertyName));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Scanning / parsing utilities (package-private static, used by analyzer)
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Scan to the end of a string literal starting at {@code start}.
|
||||
*
|
||||
* <p>Handles backslash escapes. The returned index points one past the closing quote character.</p>
|
||||
*
|
||||
* @param src the source text
|
||||
* @param start index of the opening quote character
|
||||
* @return index one past the closing quote, or {@code -1} if the string is unterminated
|
||||
*/
|
||||
static int scanStringLiteralEnd(String src, int start) {
|
||||
if (start < 0 || start >= src.length()) {
|
||||
return -1;
|
||||
}
|
||||
char quote = src.charAt(start);
|
||||
int i = start + 1;
|
||||
while (i < src.length()) {
|
||||
char c = src.charAt(i);
|
||||
if (c == '\\') {
|
||||
i += (i + 1 < src.length()) ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (c == quote) {
|
||||
return i + 1;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a string is a valid simple identifier (Java identifier rules).
|
||||
*
|
||||
* @param s the string to check
|
||||
* @return {@code true} if {@code s} is non-empty and consists of valid Java identifier characters
|
||||
*/
|
||||
static boolean isSimpleIdentifier(String s) {
|
||||
if (s == null || s.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!Character.isJavaIdentifierStart(s.charAt(0))) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 1; i < s.length(); i++) {
|
||||
if (!Character.isJavaIdentifierPart(s.charAt(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan forward to find the end of a top-level value expression within an object literal.
|
||||
*
|
||||
* <p>Respects nested braces, brackets, parentheses, string literals, and comments.
|
||||
* Stops at a top-level comma or the {@code endLimit}.</p>
|
||||
*
|
||||
* @param src the source text
|
||||
* @param start where to start scanning
|
||||
* @param endLimit the exclusive boundary (typically the position of the closing brace)
|
||||
* @return the index where the value expression ends (at a comma or {@code endLimit})
|
||||
*/
|
||||
static int scanTopLevelValueEnd(String src, int start, int endLimit) {
|
||||
int parenDepth = 0;
|
||||
int bracketDepth = 0;
|
||||
int braceDepth = 0;
|
||||
|
||||
boolean inString = false;
|
||||
char stringChar = 0;
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
|
||||
int i = start;
|
||||
while (i < endLimit) {
|
||||
char c = src.charAt(i);
|
||||
char next = (i + 1 < endLimit) ? src.charAt(i + 1) : 0;
|
||||
|
||||
if (inLineComment) {
|
||||
if (c == '\n' || c == '\r') {
|
||||
inLineComment = false;
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') {
|
||||
inBlockComment = false;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString) {
|
||||
if (c == '\\') {
|
||||
i += (i + 1 < endLimit) ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (c == stringChar) {
|
||||
inString = false;
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && next == '/') {
|
||||
inLineComment = true;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && next == '*') {
|
||||
inBlockComment = true;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\'') {
|
||||
inString = true;
|
||||
stringChar = c;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '(') parenDepth++;
|
||||
else if (c == ')') parenDepth = Math.max(0, parenDepth - 1);
|
||||
else if (c == '[') bracketDepth++;
|
||||
else if (c == ']') bracketDepth = Math.max(0, bracketDepth - 1);
|
||||
else if (c == '{') braceDepth++;
|
||||
else if (c == '}') braceDepth = Math.max(0, braceDepth - 1);
|
||||
|
||||
if (c == ',' && parenDepth == 0 && bracketDepth == 0 && braceDepth == 0) {
|
||||
return i;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return endLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan forward to find the next top-level comma (skipping nested structures and strings).
|
||||
*
|
||||
* <p>Used to skip over unsupported property entries (computed keys, spread, etc.)
|
||||
* and advance to the next property in the object literal.</p>
|
||||
*
|
||||
* @param src the source text
|
||||
* @param start where to start scanning
|
||||
* @param endLimit the exclusive boundary
|
||||
* @return the index just past the comma, or {@code endLimit} if no comma is found
|
||||
*/
|
||||
static int scanToNextTopLevelComma(String src, int start, int endLimit) {
|
||||
int i = start;
|
||||
int depth = 0;
|
||||
boolean inString = false;
|
||||
char stringChar = 0;
|
||||
while (i < endLimit) {
|
||||
char c = src.charAt(i);
|
||||
if (inString) {
|
||||
if (c == '\\') {
|
||||
i += (i + 1 < endLimit) ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (c == stringChar) {
|
||||
inString = false;
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '"' || c == '\'') {
|
||||
inString = true;
|
||||
stringChar = c;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '{' || c == '[' || c == '(') depth++;
|
||||
else if (c == '}' || c == ']' || c == ')') depth = Math.max(0, depth - 1);
|
||||
if (c == ',' && depth == 0) {
|
||||
return i + 1;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return endLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the matching closing brace for an object literal within a standalone string.
|
||||
*
|
||||
* <p>Unlike {@code ScriptDocument.findMatchingBraceEndInDocument} which operates on the full
|
||||
* document text with excluded-range awareness, this method works on an isolated string snippet
|
||||
* and handles comments/strings inline.</p>
|
||||
*
|
||||
* @param src the source text starting with '{' (or containing it)
|
||||
* @return the index one past the matching '}', or {@code -1} if not found
|
||||
*/
|
||||
static int findMatchingObjectLiteralEnd(String src) {
|
||||
int depth = 0;
|
||||
boolean inString = false;
|
||||
char stringChar = 0;
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
|
||||
for (int i = 0; i < src.length(); i++) {
|
||||
char c = src.charAt(i);
|
||||
char next = (i + 1 < src.length()) ? src.charAt(i + 1) : 0;
|
||||
|
||||
if (inLineComment) {
|
||||
if (c == '\n' || c == '\r') {
|
||||
inLineComment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') {
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inString) {
|
||||
if (c == '\\') {
|
||||
i += (i + 1 < src.length()) ? 1 : 0;
|
||||
continue;
|
||||
}
|
||||
if (c == stringChar) {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && next == '/') {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && next == '*') {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '"' || c == '\'') {
|
||||
inString = true;
|
||||
stringChar = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '{') {
|
||||
depth++;
|
||||
} else if (c == '}') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ public class ScriptDocument {
|
||||
|
||||
private static final Pattern KEYWORD_JS_PATTERN = Pattern.compile(
|
||||
"\\b(function|var|let|const|if|else|for|while|do|switch|case|break|continue|return|" +
|
||||
"try|catch|finally|throw|new|typeof|instanceof|in|of|this|null|undefined|true|false|" +
|
||||
"try|catch|finally|throw|delete|new|typeof|instanceof|in|of|this|null|undefined|true|false|" +
|
||||
"class|extends|import|export|default|async|await|yield)\\b");
|
||||
|
||||
// Declarations - Updated to capture method parameters
|
||||
@@ -1865,6 +1865,31 @@ public class ScriptDocument {
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans forward from {@code rhsStart} to find where a JavaScript initializer expression ends.
|
||||
*
|
||||
* Unlike Java, JS has no mandatory semicolons, so this method implements a conservative
|
||||
* ASI (Automatic Semicolon Insertion) heuristic:
|
||||
* <ul>
|
||||
* <li>A {@code ;} at depth 0 always terminates the expression.</li>
|
||||
* <li>A newline while inside open parens, brackets, or braces does NOT terminate —
|
||||
* the expression continues on the next line (e.g. multi-line object literals,
|
||||
* chained calls).</li>
|
||||
* <li>A newline at depth 0 terminates only if {@link #shouldContinueJsInitializer}
|
||||
* says the line break is NOT a continuation. Continuation is inferred from
|
||||
* the current line ending with an operator/open-bracket, or the next line
|
||||
* starting with {@code .}, {@code ?.}, or {@code [}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* String literals (single/double-quoted) and both comment forms ({@code //} and
|
||||
* {@code /* ... *\/}) are skipped so their contents never affect depth tracking.
|
||||
*
|
||||
* @param source the full source text to scan
|
||||
* @param rhsStart the index to begin scanning (typically the character after {@code =})
|
||||
* @return the index of the terminating character (a {@code ;} or newline), or
|
||||
* {@code source.length()} if the source ends before a terminator is found.
|
||||
* Returns {@code rhsStart} unchanged if {@code rhsStart} is out of bounds.
|
||||
*/
|
||||
private int findJsInitializerEnd(String source, int rhsStart) {
|
||||
if (rhsStart < 0 || rhsStart >= source.length()) {
|
||||
return rhsStart;
|
||||
@@ -1872,9 +1897,84 @@ public class ScriptDocument {
|
||||
|
||||
int pos = rhsStart;
|
||||
int lineExprStart = rhsStart;
|
||||
|
||||
int parenDepth = 0;
|
||||
int bracketDepth = 0;
|
||||
int braceDepth = 0;
|
||||
|
||||
boolean inString = false;
|
||||
char stringChar = 0;
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
|
||||
while (pos < source.length()) {
|
||||
char c = source.charAt(pos);
|
||||
if (c == ';') {
|
||||
char next = (pos + 1 < source.length()) ? source.charAt(pos + 1) : 0;
|
||||
|
||||
if (inLineComment) {
|
||||
if (c == '\n' || c == '\r') {
|
||||
inLineComment = false;
|
||||
} else {
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') {
|
||||
inBlockComment = false;
|
||||
pos += 2;
|
||||
continue;
|
||||
}
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString) {
|
||||
if (c == '\\') {
|
||||
pos += (pos + 1 < source.length()) ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (c == stringChar) {
|
||||
inString = false;
|
||||
}
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && next == '/') {
|
||||
inLineComment = true;
|
||||
pos += 2;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && next == '*') {
|
||||
inBlockComment = true;
|
||||
pos += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\'') {
|
||||
inString = true;
|
||||
stringChar = c;
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '(') {
|
||||
parenDepth++;
|
||||
} else if (c == ')') {
|
||||
parenDepth = Math.max(0, parenDepth - 1);
|
||||
} else if (c == '[') {
|
||||
bracketDepth++;
|
||||
} else if (c == ']') {
|
||||
bracketDepth = Math.max(0, bracketDepth - 1);
|
||||
} else if (c == '{') {
|
||||
braceDepth++;
|
||||
} else if (c == '}') {
|
||||
braceDepth = Math.max(0, braceDepth - 1);
|
||||
}
|
||||
|
||||
if (c == ';' && parenDepth == 0 && bracketDepth == 0 && braceDepth == 0) {
|
||||
return pos;
|
||||
}
|
||||
|
||||
@@ -1885,6 +1985,12 @@ public class ScriptDocument {
|
||||
nextPos++;
|
||||
}
|
||||
|
||||
if (parenDepth != 0 || bracketDepth != 0 || braceDepth != 0) {
|
||||
pos = nextPos;
|
||||
lineExprStart = nextPos;
|
||||
continue;
|
||||
}
|
||||
|
||||
int currentLineEnd = lineBreakPos;
|
||||
while (currentLineEnd > lineExprStart && Character.isWhitespace(source.charAt(currentLineEnd - 1))) {
|
||||
currentLineEnd--;
|
||||
@@ -1893,17 +1999,17 @@ public class ScriptDocument {
|
||||
|
||||
int nextExprStart = nextPos;
|
||||
while (nextExprStart < source.length()) {
|
||||
char next = source.charAt(nextExprStart);
|
||||
if (next == ' ' || next == '\t') {
|
||||
char n = source.charAt(nextExprStart);
|
||||
if (n == ' ' || n == '\t') {
|
||||
nextExprStart++;
|
||||
continue;
|
||||
}
|
||||
if (next == ';') {
|
||||
if (n == ';') {
|
||||
return nextExprStart;
|
||||
}
|
||||
if (next == '\n' || next == '\r') {
|
||||
if (n == '\n' || n == '\r') {
|
||||
nextExprStart++;
|
||||
if (next == '\r' && nextExprStart < source.length() && source.charAt(nextExprStart) == '\n') {
|
||||
if (n == '\r' && nextExprStart < source.length() && source.charAt(nextExprStart) == '\n') {
|
||||
nextExprStart++;
|
||||
}
|
||||
continue;
|
||||
@@ -1917,8 +2023,8 @@ public class ScriptDocument {
|
||||
|
||||
int nextLineEnd = nextExprStart;
|
||||
while (nextLineEnd < source.length()) {
|
||||
char next = source.charAt(nextLineEnd);
|
||||
if (next == '\n' || next == '\r' || next == ';') {
|
||||
char n = source.charAt(nextLineEnd);
|
||||
if (n == '\n' || n == '\r' || n == ';') {
|
||||
break;
|
||||
}
|
||||
nextLineEnd++;
|
||||
@@ -3052,7 +3158,11 @@ public class ScriptDocument {
|
||||
|
||||
// Numbers - same for both languages
|
||||
addPatternMarks(marks, NUMBER_PATTERN, TokenType.LITERAL);
|
||||
|
||||
|
||||
if (isJavaScript()) {
|
||||
markObjectLiteralKeys(marks);
|
||||
}
|
||||
|
||||
// Import statements - Java only
|
||||
if (!isJavaScript()) {
|
||||
markImports(marks);
|
||||
@@ -3109,7 +3219,94 @@ public class ScriptDocument {
|
||||
|
||||
return marks;
|
||||
}
|
||||
|
||||
|
||||
private void markObjectLiteralKeys(List<ScriptLine.Mark> marks) {
|
||||
int i = 0;
|
||||
while (i < text.length()) {
|
||||
int brace = text.indexOf('{', i);
|
||||
if (brace < 0) {
|
||||
return;
|
||||
}
|
||||
i = brace;
|
||||
if (isExcluded(brace) || !isLikelyObjectLiteralStart(brace)) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int end = findMatchingBraceEndInDocument(brace);
|
||||
if (end <= brace) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
String objectLiteral = text.substring(brace, end);
|
||||
ObjectLiteralParser.ObjectLiteralAnalysis analysis = ObjectLiteralParser.parse(objectLiteral, brace, false, true, null);
|
||||
if (analysis != null) {
|
||||
for (ObjectLiteralParser.ObjectLiteralProperty p : analysis.properties) {
|
||||
if (p.isIdentifierKey) {
|
||||
marks.add(new ScriptLine.Mark(p.keyStartAbs, p.keyEndAbs, TokenType.LOCAL_FIELD));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = end;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLikelyObjectLiteralStart(int bracePos) {
|
||||
int prev = bracePos - 1;
|
||||
while (prev >= 0 && Character.isWhitespace(text.charAt(prev))) {
|
||||
prev--;
|
||||
}
|
||||
if (prev < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char pc = text.charAt(prev);
|
||||
if (pc == '=' || pc == '(' || pc == '[' || pc == ',' || pc == ':' || pc == '?' || pc == '!' ||
|
||||
pc == '+' || pc == '-' || pc == '*' || pc == '/' || pc == '%' || pc == '&' || pc == '|' ||
|
||||
pc == '^') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Character.isJavaIdentifierPart(pc)) {
|
||||
int end = prev + 1;
|
||||
int start = prev;
|
||||
while (start >= 0 && Character.isJavaIdentifierPart(text.charAt(start))) {
|
||||
start--;
|
||||
}
|
||||
start++;
|
||||
String word = text.substring(start, end);
|
||||
return "return".equals(word);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int findMatchingBraceEndInDocument(int braceStart) {
|
||||
int depth = 0;
|
||||
for (int pos = braceStart; pos < text.length(); pos++) {
|
||||
if (isExcluded(pos)) {
|
||||
continue;
|
||||
}
|
||||
char c = text.charAt(pos);
|
||||
if (c == '{') {
|
||||
depth++;
|
||||
} else if (c == '}') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
return pos + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Mark lambda arrow (->) and method reference (::) operators.
|
||||
*/
|
||||
@@ -3576,6 +3773,11 @@ public class ScriptDocument {
|
||||
"static", "final", "abstract", "synchronized", "native", "default", "enum",
|
||||
"throws", "super", "assert", "volatile", "transient"
|
||||
));
|
||||
|
||||
if (isJavaScript()) {
|
||||
knownKeywords.add("delete");
|
||||
knownKeywords.add("undefined");
|
||||
}
|
||||
|
||||
// Find all identifiers
|
||||
Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b");
|
||||
@@ -4943,10 +5145,20 @@ public class ScriptDocument {
|
||||
|
||||
if (expr.isEmpty())
|
||||
return null;
|
||||
|
||||
if (isJavaScript() && expr.charAt(0) == '{') {
|
||||
ObjectLiteralParser.ObjectLiteralAnalysis analysis = ObjectLiteralParser.parse(expr, position, true, false, this::resolveExpressionType);
|
||||
if (analysis == null) {
|
||||
return TypeInfo.ANY;
|
||||
}
|
||||
if (!analysis.supportsInference || analysis.inferredType == null) {
|
||||
return TypeInfo.ANY;
|
||||
}
|
||||
return analysis.inferredType;
|
||||
}
|
||||
|
||||
// Check if expression contains operators - if so, use the full expression resolver
|
||||
// Handle cast expressions: (Type)expr, ((Type)expr).method(), etc.
|
||||
// Handle cast expressions: (Type)expr, ((Type)expr).method(), etc
|
||||
// Also route JS function expressions and arrow lambdas through the parser
|
||||
if (containsOperators(expr) || expr.contains("::") || expr.contains("->") || expr.startsWith("(") || looksLikeFunctionOrLambda(expr)) {
|
||||
return resolveExpressionWithParserAPI(expr, position);
|
||||
@@ -5066,15 +5278,11 @@ public class ScriptDocument {
|
||||
String fieldName = segments.get(1).name;
|
||||
if (globalFields.containsKey(fieldName)) {
|
||||
currentType = globalFields.get(fieldName).getTypeInfo();
|
||||
if (currentType != null && segments.get(1).hasArrayAccess) {
|
||||
currentType = unwrapArrayElement(currentType);
|
||||
}
|
||||
currentType = applyBracketAccess(currentType, segments.get(1));
|
||||
// Continue from segment 2
|
||||
for (int i = 2; i < segments.size(); i++) {
|
||||
currentType = resolveChainSegment(currentType, segments.get(i));
|
||||
if (currentType != null && segments.get(i).hasArrayAccess) {
|
||||
currentType = unwrapArrayElement(currentType);
|
||||
}
|
||||
currentType = applyBracketAccess(currentType, segments.get(i));
|
||||
if (currentType == null) return null;
|
||||
}
|
||||
return currentType;
|
||||
@@ -5126,16 +5334,12 @@ public class ScriptDocument {
|
||||
}
|
||||
}
|
||||
|
||||
if (currentType != null && first.hasArrayAccess) {
|
||||
currentType = unwrapArrayElement(currentType);
|
||||
}
|
||||
currentType = applyBracketAccess(currentType, first);
|
||||
|
||||
// Resolve the rest of the chain
|
||||
for (int i = 1; i < segments.size(); i++) {
|
||||
currentType = resolveChainSegment(currentType, segments.get(i));
|
||||
if (currentType != null && segments.get(i).hasArrayAccess) {
|
||||
currentType = unwrapArrayElement(currentType);
|
||||
}
|
||||
currentType = applyBracketAccess(currentType, segments.get(i));
|
||||
if (currentType == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -5233,11 +5437,13 @@ public class ScriptDocument {
|
||||
}
|
||||
|
||||
boolean hasArrayAccess = false;
|
||||
String bracketKey = null;
|
||||
while (i < expr.length() && Character.isWhitespace(expr.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
if (i < expr.length() && expr.charAt(i) == '[') {
|
||||
hasArrayAccess = true;
|
||||
int bracketStart = i + 1;
|
||||
int depth = 1;
|
||||
i++;
|
||||
while (i < expr.length() && depth > 0) {
|
||||
@@ -5246,9 +5452,16 @@ public class ScriptDocument {
|
||||
else if (c == ']') depth--;
|
||||
i++;
|
||||
}
|
||||
String bracketContent = expr.substring(bracketStart, i - 1).trim();
|
||||
if (bracketContent.length() >= 2) {
|
||||
char q = bracketContent.charAt(0);
|
||||
if ((q == '"' || q == '\'') && bracketContent.charAt(bracketContent.length() - 1) == q) {
|
||||
bracketKey = bracketContent.substring(1, bracketContent.length() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segments.add(new ChainSegment(name, start, i, isMethodCall, arguments, hasArrayAccess));
|
||||
|
||||
segments.add(new ChainSegment(name, start, i, isMethodCall, arguments, hasArrayAccess, bracketKey));
|
||||
|
||||
// Skip whitespace
|
||||
while (i < expr.length() && Character.isWhitespace(expr.charAt(i))) {
|
||||
@@ -5365,21 +5578,23 @@ public class ScriptDocument {
|
||||
final int start;
|
||||
final int end;
|
||||
final boolean isMethodCall;
|
||||
final String arguments; // The text between parentheses for method calls, or null for fields
|
||||
final String arguments;
|
||||
final boolean hasArrayAccess;
|
||||
|
||||
ChainSegment(String name, int start, int end, boolean isMethodCall, String arguments, boolean hasArrayAccess) {
|
||||
final String bracketKey;
|
||||
|
||||
ChainSegment(String name, int start, int end, boolean isMethodCall, String arguments, boolean hasArrayAccess, String bracketKey) {
|
||||
this.name = name;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.isMethodCall = isMethodCall;
|
||||
this.arguments = arguments;
|
||||
this.hasArrayAccess = hasArrayAccess;
|
||||
this.bracketKey = bracketKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
private String stripLineComments(String expr) {
|
||||
if (!expr.contains("//"))
|
||||
@@ -5397,7 +5612,22 @@ public class ScriptDocument {
|
||||
}
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle ObjectLiteral bracket access (e.g., obj["field"] or obj[fieldName]) and array access (e.g., items[0]).
|
||||
* @param type
|
||||
* @param seg
|
||||
* @return
|
||||
*/
|
||||
private TypeInfo applyBracketAccess(TypeInfo type, ChainSegment seg) {
|
||||
if (type == null || !seg.hasArrayAccess) return type;
|
||||
if (seg.bracketKey != null) {
|
||||
FieldInfo f = type.getFieldInfo(seg.bracketKey);
|
||||
return f != null ? f.getTypeInfo() : TypeInfo.ANY;
|
||||
}
|
||||
if (type.isSyntheticObjectLiteralType()) return TypeInfo.ANY;
|
||||
return unwrapArrayElement(type);
|
||||
}
|
||||
/**
|
||||
* Extract the element type from an array type, using multiple fallback strategies.
|
||||
*
|
||||
@@ -5927,15 +6157,11 @@ public class ScriptDocument {
|
||||
}
|
||||
}
|
||||
|
||||
if (currentType != null && first.hasArrayAccess) {
|
||||
currentType = unwrapArrayElement(currentType);
|
||||
}
|
||||
currentType = applyBracketAccess(currentType, first);
|
||||
|
||||
for (int i = 1; i < segments.size(); i++) {
|
||||
currentType = resolveChainSegment(currentType, segments.get(i));
|
||||
if (currentType != null && segments.get(i).hasArrayAccess) {
|
||||
currentType = unwrapArrayElement(currentType);
|
||||
}
|
||||
currentType = applyBracketAccess(currentType, segments.get(i));
|
||||
if (currentType == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -6499,6 +6725,11 @@ public class ScriptDocument {
|
||||
"throws", "super", "assert", "volatile", "transient"
|
||||
));
|
||||
|
||||
if (isJavaScript()) {
|
||||
knownKeywords.add("delete");
|
||||
knownKeywords.add("undefined");
|
||||
}
|
||||
|
||||
// First pass: mark method parameters in their declaration positions
|
||||
List<MethodInfo> allMethods = getAllMethods();
|
||||
allMethods.addAll(getAllConstructors());
|
||||
@@ -6577,17 +6808,19 @@ public class ScriptDocument {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check local variables (method scope)
|
||||
FieldInfo localInfo = null;
|
||||
if (containingMethod != null) {
|
||||
Map<String, List<FieldInfo>> locals = methodLocals.get(containingMethod.getDeclarationOffset());
|
||||
if (locals != null) {
|
||||
FieldInfo localInfo = pickVisibleLocal(locals.get(name), position);
|
||||
if (localInfo != null) {
|
||||
Object metadata = callInfo != null ? new FieldInfo.ArgInfo(localInfo, callInfo) : localInfo;
|
||||
marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD, metadata));
|
||||
continue;
|
||||
}
|
||||
localInfo = pickVisibleLocal(locals.get(name), position);
|
||||
}
|
||||
} else {
|
||||
localInfo = pickVisibleTopLevelLocal(name, position);
|
||||
}
|
||||
if (localInfo != null) {
|
||||
Object metadata = callInfo != null ? new FieldInfo.ArgInfo(localInfo, callInfo) : localInfo;
|
||||
marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD, metadata));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check enclosing type fields (when inside method)
|
||||
@@ -6704,6 +6937,11 @@ public class ScriptDocument {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (List<FieldInfo> fields : topLevelLocals.values()) {
|
||||
for (FieldInfo field : fields) {
|
||||
field.clearAssignments();
|
||||
}
|
||||
}
|
||||
// Also clear assignments in script type fields
|
||||
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
|
||||
for (FieldInfo field : scriptType.getFields().values()) {
|
||||
@@ -6838,6 +7076,8 @@ public class ScriptDocument {
|
||||
private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, int equalsPos, int stmtEnd) {
|
||||
lhs = lhs.trim();
|
||||
rhs = rhs.trim();
|
||||
|
||||
ObjectLiteralParser.DynamicPropertyAccess dynamicPropertyAccess = isJavaScript() ? ObjectLiteralParser.parseDynamicPropertyAccess(lhs, stmtStart) : null;
|
||||
|
||||
// Parse the target (LHS)
|
||||
// Could be: varName or obj.field or this.field or array[index]
|
||||
@@ -6902,8 +7142,23 @@ public class ScriptDocument {
|
||||
// Extract the variable name before the bracket
|
||||
String varName = lhs.substring(0, lhs.indexOf('[')).trim();
|
||||
targetName = varName;
|
||||
targetField = resolveVariable(varName, lhsStart);
|
||||
if (targetField != null) {
|
||||
if (dynamicPropertyAccess != null) {
|
||||
ObjectLiteralParser.DynamicFieldResult res = ObjectLiteralParser.resolveExistingField(
|
||||
dynamicPropertyAccess, this::resolveExpressionType, this::resolveVariable);
|
||||
receiverType = res.receiverType;
|
||||
targetName = res.propertyName;
|
||||
targetField = res.field;
|
||||
if (targetField != null) {
|
||||
targetType = targetField.getTypeInfo();
|
||||
reflectionField = targetField.getReflectionField();
|
||||
}
|
||||
}
|
||||
|
||||
if (targetField == null) {
|
||||
targetField = resolveVariable(varName, lhsStart);
|
||||
}
|
||||
|
||||
if (targetField != null && targetType == null) {
|
||||
// The variable type is the array type (e.g., ItemStack[])
|
||||
// Unwrap to get the element type (e.g., ItemStack)
|
||||
targetType = unwrapArrayElement(targetField.getTypeInfo());
|
||||
@@ -6926,6 +7181,21 @@ public class ScriptDocument {
|
||||
} finally {
|
||||
ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null;
|
||||
}
|
||||
|
||||
if (dynamicPropertyAccess != null) {
|
||||
ObjectLiteralParser.DynamicFieldResult res = ObjectLiteralParser.extendAndGetField(
|
||||
dynamicPropertyAccess, receiverType, sourceType,
|
||||
this::resolveExpressionType, this::resolveVariable);
|
||||
if (res != null) {
|
||||
receiverType = res.receiverType;
|
||||
targetName = res.propertyName;
|
||||
targetField = res.field;
|
||||
if (targetField != null) {
|
||||
targetType = targetField.getTypeInfo();
|
||||
reflectionField = targetField.getReflectionField();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type inference: If the target field has "any" type and no inferred type yet,
|
||||
// set the inferred type from the resolved source type (first assignment wins)
|
||||
@@ -7969,7 +8239,7 @@ public class ScriptDocument {
|
||||
public Map<String, FieldInfo> getGlobalFields() {
|
||||
return Collections.unmodifiableMap(globalFields);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all variables available at a specific position.
|
||||
* Used by autocomplete to show scope-aware suggestions.
|
||||
|
||||
@@ -538,7 +538,7 @@ public final class TypeChecker {
|
||||
String[] keywords = {
|
||||
"function", "var", "let", "const", "if", "else", "for", "while", "do",
|
||||
"switch", "case", "break", "continue", "return", "try", "catch", "finally",
|
||||
"throw", "new", "typeof", "instanceof", "in", "of", "this", "null",
|
||||
"throw", "delete", "new", "typeof", "instanceof", "in", "of", "this", "null",
|
||||
"undefined", "true", "false", "async", "await", "yield", "class", "extends",
|
||||
"import", "export", "default"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ObjectLiteralParser;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo;
|
||||
@@ -18,6 +19,7 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Represents resolved type information for a class/interface/enum.
|
||||
@@ -69,6 +71,9 @@ public class TypeInfo {
|
||||
*/
|
||||
public static final TypeInfo ANY = new TypeInfo("any", "any", "", Kind.CLASS, null, true, null).setPrimitive(true);
|
||||
public static final TypeInfo NUMBER = new TypeInfo("number", "number", "", Kind.CLASS, double.class, true, null).setPrimitive(true);
|
||||
|
||||
public static final Supplier<TypeInfo> OBJECT_LITERAL = () -> new TypeInfo("ObjectLiteral", "ObjectLiteral", "",
|
||||
Kind.CLASS, null, true, null);
|
||||
|
||||
|
||||
private final String simpleName; // e.g., "List", "ColorType"
|
||||
@@ -80,6 +85,7 @@ public class TypeInfo {
|
||||
private final TypeInfo enclosingType; // For inner classes, the outer type (null if top-level)
|
||||
|
||||
private boolean isPrimitive; // Whether this is a primitive type (e.g., int, boolean)
|
||||
private boolean isObjectLiteral;
|
||||
|
||||
// JavaScript/TypeScript type info (for types from .d.ts files)
|
||||
private final JSTypeInfo jsTypeInfo; // The JS type info (null if Java type)
|
||||
@@ -325,6 +331,12 @@ public class TypeInfo {
|
||||
*/
|
||||
public static TypeInfo fromJSTypeInfo(JSTypeInfo jsType) {
|
||||
if (jsType == null) return null;
|
||||
|
||||
String syntheticDisplayName = ObjectLiteralParser.getSyntheticObjectLiteralDisplayName(jsType);
|
||||
if (syntheticDisplayName != null) {
|
||||
return new TypeInfo(syntheticDisplayName, jsType.getFullName(), "",
|
||||
Kind.INTERFACE, null, true, null, jsType);
|
||||
}
|
||||
|
||||
String simpleName = jsType.getSimpleName();
|
||||
String fullName = jsType.getFullName();
|
||||
@@ -334,7 +346,24 @@ public class TypeInfo {
|
||||
return new TypeInfo(simpleName, fullName, namespace != null ? namespace : "",
|
||||
Kind.INTERFACE, null, true, null, jsType);
|
||||
}
|
||||
|
||||
|
||||
public static TypeInfo objectLiteral(List<ObjectLiteralParser.ObjectLiteralProperty> properties) {
|
||||
TypeInfo t = OBJECT_LITERAL.get();
|
||||
t.isObjectLiteral = true;
|
||||
if (properties != null) {
|
||||
for (ObjectLiteralParser.ObjectLiteralProperty p : properties) {
|
||||
TypeInfo vt = (p.valueType != null) ? p.valueType : TypeInfo.ANY;
|
||||
t.syntheticFields.add(FieldInfo.external(p.keyName, vt, null, Modifier.PUBLIC));
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
public void addOrUpdateSyntheticField(String name, TypeInfo type) {
|
||||
syntheticFields.removeIf(f -> f.getName().equals(name));
|
||||
syntheticFields.add(FieldInfo.external(name, type, null, Modifier.PUBLIC));
|
||||
}
|
||||
|
||||
public static void injectJSMembers(TypeInfo target, JSTypeInfo jsExtensions) {
|
||||
for (JSFieldInfo jsField : jsExtensions.getFields().values())
|
||||
target.syntheticFields.add(FieldInfo.fromJSField(jsField, target));
|
||||
@@ -414,6 +443,10 @@ public class TypeInfo {
|
||||
public boolean isJSType() {
|
||||
return jsTypeInfo != null;
|
||||
}
|
||||
|
||||
public boolean isSyntheticObjectLiteralType() {
|
||||
return isObjectLiteral;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying JSTypeInfo if this is a JS type.
|
||||
|
||||
Reference in new issue
Block a user