Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ public default Set<String> getReservedWords() {
return Collections.emptySet();
}

/**
* @param script The script contents.
* @return The names of the members declared at the top level of the script,
* or null if they can not be determined.
*/
@Nullable
public default Set<String> getTopLevelMembers(String script) {
return null;
}

/**
* Check a script for syntax errors, without executing it.
* @param path The script path.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -758,6 +759,15 @@ private void onSelectionModified() {
startPos = value.length() - matcherToStart.end() + 1;
}
selected = value.substring(startPos, endPos);

// Only bind to declarations, so moving through function bodies doesn't rebind the card
Path path = getMenu().getActiveScriptPath();
ILanguageHandler languageHandler = path != null ? LanguageHandlers.REGISTRY.getProvider(path) : null;
Set<String> members = languageHandler != null ? languageHandler.getTopLevelMembers(value) : null;
if (members != null && !members.contains(selected)) {
String previous = getMenu().getSelectedMember();
selected = members.contains(previous) ? previous : "";
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package org.cyclops.integratedscripting.core.language;

import com.google.common.collect.Sets;

import javax.annotation.Nullable;
import java.util.Set;

/**
* Finds the names declared at the top level of a JavaScript script, without executing or fully parsing it.
* @author rubensworks
*/
public class JavaScriptDeclarations {

// Characters after which a slash starts a regex instead of a division
private static final String REGEX_PRECEDERS = "(,=:[!&|?{};+-*%<>~^";
// Characters after which a function or class is an expression instead of a declaration
private static final String EXPRESSION_PRECEDERS = "(,=:[!&|?+-*%<>~^";

/**
* @param script A script.
* @return The names of the top-level functions, classes and variables,
* or null if the script contains declarations that are not supported, such as destructuring.
*/
@Nullable
public static Set<String> find(String script) {
Set<String> names = Sets.newHashSet();
int depth = 0;
int length = script.length();
// The last significant character, to detect member accesses and regexes
char previous = ';';
String previousWord = "";
// If a name is expected next, after a declaration keyword or a comma in a variable declaration
boolean expectName = false;
boolean inVariableDeclaration = false;
int i = 0;
while (i < length) {
char c = script.charAt(i);
if (Character.isWhitespace(c)) {
i++;
} else if (c == '/' && i + 1 < length && script.charAt(i + 1) == '/') {
int end = script.indexOf('\n', i);
i = end < 0 ? length : end;
} else if (c == '/' && i + 1 < length && script.charAt(i + 1) == '*') {
int end = script.indexOf("*/", i + 2);
i = end < 0 ? length : end + 2;
} else if (c == '"' || c == '\'' || c == '`'
|| (c == '/' && (REGEX_PRECEDERS.indexOf(previous) >= 0 || previousWord.equals("return")))) {
i = skipQuoted(script, i, c);
previous = 'a';
previousWord = "";
expectName = false;
} else if (Character.isJavaIdentifierStart(c)) {
int start = i;
while (i < length && Character.isJavaIdentifierPart(script.charAt(i))) {
i++;
}
String word = script.substring(start, i);
if (depth == 0 && previous != '.') {
if (expectName) {
names.add(word);
expectName = false;
} else if ((word.equals("function") || word.equals("class"))
&& EXPRESSION_PRECEDERS.indexOf(previous) < 0 && !previousWord.equals("return")) {
expectName = true;
inVariableDeclaration = false;
} else if (word.equals("var") || word.equals("let") || word.equals("const")) {
expectName = true;
inVariableDeclaration = true;
}
}
previous = 'a';
previousWord = word;
} else {
if (depth == 0) {
if (expectName && (c == '{' || c == '[') && inVariableDeclaration) {
// Destructuring
return null;
}
if (c == ';') {
inVariableDeclaration = false;
} else if (c == ',' && inVariableDeclaration) {
expectName = true;
} else if (c != '*') {
// Allow generator functions
expectName = false;
}
}
if (c == '{' || c == '(' || c == '[') {
depth++;
} else if ((c == '}' || c == ')' || c == ']') && depth > 0) {
depth--;
}
previous = c;
previousWord = "";
i++;
}
}
return names;
}

private static int skipQuoted(String script, int start, char quote) {
int i = start + 1;
while (i < script.length()) {
char c = script.charAt(i);
if (c == '\\') {
i += 2;
} else if (c == quote) {
return i + 1;
} else if (c == '\n' && quote != '`') {
// Unterminated
return i;
} else {
i++;
}
}
return i;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ public Set<String> getReservedWords() {
return RESERVED_WORDS;
}

@Nullable
@Override
public Set<String> getTopLevelMembers(String script) {
return JavaScriptDeclarations.find(script);
}

@Override
public IScriptFactory getScriptFactory() throws EvaluationException {
return this.scriptFactory;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.cyclops.integratedscripting.core.language;

import com.google.common.collect.Sets;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;

public class JavaScriptDeclarationsTest {

@Test
public void testDeclarations() {
assertThat(JavaScriptDeclarations.find("function a(x, y) { return x; }\nconst b = 1;\nlet c = 2\nvar d\nclass E {}\nasync function f() {}\nfunction* g() {}\nexport const h = 3;"),
equalTo(Sets.newHashSet("a", "b", "c", "d", "E", "f", "g", "h")));
}

@Test
public void testMultipleDeclarators() {
assertThat(JavaScriptDeclarations.find("const a = f(1, 2), b = [3, 4], c = { x: 1, y: 2 };\nfoo(d, e);"),
equalTo(Sets.newHashSet("a", "b", "c")));
}

@Test
public void testIgnoresNested() {
assertThat(JavaScriptDeclarations.find("function a(item) {\n const b = item.size();\n function c() {}\n return b;\n}\nconst d = (x) => { let e = x; return e; };"),
equalTo(Sets.newHashSet("a", "d")));
}

@Test
public void testIgnoresExpressions() {
assertThat(JavaScriptDeclarations.find("const a = function b() {};\nconst c = class D {};\nfoo(function e() {});"),
equalTo(Sets.newHashSet("a", "c")));
}

@Test
public void testIgnoresStringsCommentsAndRegexes() {
assertThat(JavaScriptDeclarations.find("// const a = 1;\n/* function b() {} */\nconst c = \"const d = '{';\";\nconst e = /[{(]/.test(c);\nconst f = `${c} {`;\nconst g = 4 / 2;"),
equalTo(Sets.newHashSet("c", "e", "f", "g")));
}

@Test
public void testIgnoresMemberAccesses() {
assertThat(JavaScriptDeclarations.find("const a = idContext.ops.function;"), equalTo(Sets.newHashSet("a")));
}

@Test
public void testDestructuringUnsupported() {
assertThat(JavaScriptDeclarations.find("const { a, b } = c;"), nullValue());
assertThat(JavaScriptDeclarations.find("const [a, b] = c;"), nullValue());
}

}
Loading