Skip to content

Commit

Permalink
Merge pull request #74 from NixOS/utilities
Browse files Browse the repository at this point in the history
Some Tools and Fundamentals
  • Loading branch information
JojOatXGME committed Mar 21, 2024
2 parents 37d0ad6 + 9681439 commit 76d4598
Show file tree
Hide file tree
Showing 14 changed files with 899 additions and 7 deletions.
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ repositories {
dependencies {
testImplementation(platform(libs.junit5.bom))
testImplementation(libs.junit5.jupiter)
testImplementation(libs.junit5.platform.testkit)
testRuntimeOnly(libs.junit5.vintage.engine)
}

Expand Down Expand Up @@ -123,7 +124,9 @@ tasks {
}

test {
useJUnitPlatform()
useJUnitPlatform {
excludeTags("mock")
}
}

patchPluginXml {
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[libraries]
junit5-bom = { module = "org.junit:junit-bom", version = "5.9.1" }
junit5-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
junit5-platform-testkit = { module = "org.junit.platform:junit-platform-testkit" }
junit5-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine" }

[plugins]
Expand Down
95 changes: 95 additions & 0 deletions src/main/java/org/nixos/idea/psi/NixElementFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.nixos.idea.psi;

import com.intellij.lang.ASTNode;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.TokenType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.nixos.idea.file.NixFile;
import org.nixos.idea.file.NixFileType;

import java.util.Objects;

public final class NixElementFactory {

private NixElementFactory() {} // Cannot be instantiated

public static @NotNull NixString createString(@NotNull Project project, @NotNull String code) {
return createElement(project, NixString.class, "", code, "");
}

public static @NotNull NixAttr createAttr(@NotNull Project project, @NotNull String code) {
return createElement(project, NixAttr.class, "x.", code, "");
}

public static @NotNull NixAttrPath createAttrPath(@NotNull Project project, @NotNull String code) {
return createElement(project, NixAttrPath.class, "x.", code, "");
}

public static @NotNull NixBind createBind(@NotNull Project project, @NotNull String code) {
return createElement(project, NixBind.class, "{", code, "}");
}

@SuppressWarnings("unchecked")
public static <T extends NixExpr> @NotNull T createExpr(@NotNull Project project, @NotNull String code) {
return (T) createElement(project, NixExpr.class, "", code, "");
}

public static <T extends NixPsiElement> @NotNull T createElement(
@NotNull Project project, @NotNull Class<T> type,
@NotNull String prefix, @NotNull String text, @NotNull String suffix) {
return Objects.requireNonNull(
createElementOrNull(project, type, prefix, text, suffix),
"Invalid " + type.getSimpleName() + ": " + text);
}

private static <T extends NixPsiElement> @Nullable T createElementOrNull(
@NotNull Project project, @NotNull Class<T> type,
@NotNull String prefix, @NotNull String text, @NotNull String suffix) {
NixFile file = createFile(project, prefix + text + suffix);
ASTNode current = file.getNode().getFirstChildNode();
int offset = 0;
while (current != null && offset <= prefix.length()) {
int length = current.getTextLength();
// Check if we have found the right element
if (offset == prefix.length() && length == text.length()) {
PsiElement psi = current.getPsi();
if (type.isInstance(psi)) {
return containsErrors(current) ? null : type.cast(psi);
}
}
// Check if we should go into or over this element
if (offset + length <= prefix.length()) {
offset += length;
current = current.getTreeNext();
} else {
current = current.getFirstChildNode();
}
}
return null;
}

private static boolean containsErrors(ASTNode node) {
ASTNode current = node.getFirstChildNode();
while (current != null) {
if (current.getElementType() == TokenType.ERROR_ELEMENT) {
return true;
}
ASTNode next = current.getFirstChildNode();
if (next == null) {
next = current.getTreeNext();
while (next == null && (current = current.getTreeParent()) != node) {
next = current.getTreeNext();
}
}
current = next;
}
return false;
}

public static @NotNull NixFile createFile(@NotNull Project project, @NotNull String code) {
return (NixFile) PsiFileFactory.getInstance(project).createFileFromText("dummy.nix", NixFileType.INSTANCE, code);
}
}
8 changes: 8 additions & 0 deletions src/main/java/org/nixos/idea/psi/NixPsiElement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.nixos.idea.psi;

import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;

public interface NixPsiElement extends PsiElement {
<T> T accept(@NotNull NixElementVisitor<T> visitor);
}
14 changes: 14 additions & 0 deletions src/main/java/org/nixos/idea/psi/impl/AbstractNixPsiElement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.nixos.idea.psi.impl;

import com.intellij.extapi.psi.ASTWrapperPsiElement;
import com.intellij.lang.ASTNode;
import org.jetbrains.annotations.NotNull;
import org.nixos.idea.psi.NixPsiElement;

abstract class AbstractNixPsiElement extends ASTWrapperPsiElement implements NixPsiElement {

AbstractNixPsiElement(@NotNull ASTNode node) {
super(node);
}

}
131 changes: 131 additions & 0 deletions src/main/java/org/nixos/idea/util/NixStringUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.nixos.idea.util;

import com.intellij.lang.ASTNode;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.nixos.idea.psi.NixStringText;
import org.nixos.idea.psi.NixTypes;

/**
* Utilities for strings in the Nix Expression Language.
*/
public final class NixStringUtil {

private NixStringUtil() {} // Cannot be instantiated

/**
* Returns the source code for a string in the Nix Expression Language.
* When the returned string is evaluated by a Nix interpreter, the result matches the sting given to this method.
* The returned string expression is always a double-quoted string.
*
* <h4>Example</h4>
* <pre>{@code System.out.println(quote("This should be escaped: ${}"));}</pre>
* The code above prints the following:
* <pre>"This should be escaped: \${}"</pre>
*
* @param unescaped The raw string which shall be the result when the expression is evaluated.
* @return Source code for a Nix expression which evaluates to the given string.
*/
@Contract(pure = true)
public static @NotNull String quote(@NotNull CharSequence unescaped) {
StringBuilder builder = new StringBuilder();
builder.append('"');
escape(builder, unescaped);
builder.append('"');
return builder.toString();
}

/**
* Escapes the given string for use in a double-quoted string expression in the Nix Expression Language.
* Note that it is not safe to combine the results of two method calls with arbitrary input.
* For example, the following code would generate a broken result.
* <pre>{@code
* StringBuilder b1 = new StringBuilder(), b2 = new StringBuilder();
* NixStringUtil.escape(b1, "$");
* NixStringUtil.escape(b2, "{''}");
* System.out.println(b1.toString() + b2.toString());
* }</pre>
* The result would be the following broken Nix code.
* <pre>
* "${''}"
* </pre>
*
* @param builder The target string builder. The result will be appended to the given string builder.
* @param unescaped The raw string which shall be escaped.
*/
public static void escape(@NotNull StringBuilder builder, @NotNull CharSequence unescaped) {
for (int charIndex = 0; charIndex < unescaped.length(); charIndex++) {
char nextChar = unescaped.charAt(charIndex);
switch (nextChar) {
case '"':
case '\\':
builder.append('\\').append(nextChar);
break;
case '{':
if (builder.charAt(builder.length() - 1) == '$') {
builder.setCharAt(builder.length() - 1, '\\');
builder.append('$').append('{');
} else {
builder.append('{');
}
break;
case '\n':
builder.append('\\').append('n');
break;
case '\r':
builder.append('\\').append('r');
break;
case '\t':
builder.append('\\').append('t');
break;
default:
builder.append(nextChar);
break;
}
}
}

/**
* Returns the content of the given part of a string in the Nix Expression Language.
* All escape sequences are resolved.
*
* @param textNode A part of a string.
* @return The resulting string after resolving all escape sequences.
*/
public static @NotNull String parse(@NotNull NixStringText textNode) {
StringBuilder builder = new StringBuilder();
for (ASTNode child = textNode.getNode().getFirstChildNode(); child != null; child = child.getTreeNext()) {
parse(builder, child);
}
return builder.toString();
}

private static void parse(@NotNull StringBuilder builder, @NotNull ASTNode token) {
CharSequence text = token.getChars();
IElementType type = token.getElementType();
if (type == NixTypes.STR || type == NixTypes.IND_STR) {
builder.append(text);
} else if (type == NixTypes.STR_ESCAPE) {
assert text.length() == 2 && text.charAt(0) == '\\' : text;
char c = text.charAt(1);
builder.append(unescape(c));
} else if (type == NixTypes.IND_STR_ESCAPE) {
assert text.length() == 3 && ("''$".contentEquals(text) || "'''".contentEquals(text)) ||
text.length() == 4 && "''\\".contentEquals(text.subSequence(0, 3)) : text;
char c = text.charAt(text.length() - 1);
builder.append(unescape(c));
} else {
throw new IllegalStateException("Unexpected token in string: " + token);
}
}

private static char unescape(char c) {
return switch (c) {
case 'n' -> '\n';
case 'r' -> '\r';
case 't' -> '\t';
default -> c;
};
}
}
19 changes: 13 additions & 6 deletions src/main/lang/Nix.bnf
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
{
// Parser classes
parserClass="org.nixos.idea.lang.NixParser"
parserUtilClass="org.nixos.idea.psi.impl.NixParserUtil"
extends="com.intellij.extapi.psi.ASTWrapperPsiElement"

// PSI element interfaces
psiClassPrefix="Nix"
psiImplClassSuffix="Impl"
psiPackage="org.nixos.idea.psi"
implements="org.nixos.idea.psi.NixPsiElement"
// PSI element implementations
psiImplClassSuffix="Impl"
psiImplPackage="org.nixos.idea.psi.impl"

extends="org.nixos.idea.psi.impl.AbstractNixPsiElement"
// Constants (NixTypes)
elementTypeHolderClass="org.nixos.idea.psi.NixTypes"
elementTypeClass="org.nixos.idea.psi.NixElementType"
tokenTypeClass="org.nixos.idea.psi.NixTokenType"
// Visitor
psiVisitorName="NixElementVisitor"
generate=[ visitor-value="T" ]

// do not record error reporting information in recover rules and missing_semi
consumeTokenMethod(".*_recover|missing_semi")="consumeTokenFast"
Expand Down Expand Up @@ -163,7 +169,8 @@ upper legacy_app_or ::= OR_KW { extends=expr_app }
expr_simple ::=
identifier
| literal
| string
| std_string
| ind_string
| parens
| set
| list
Expand All @@ -183,9 +190,9 @@ private set_recover ::= curly_recover !bind
private list_recover ::= brac_recover !expr_select

;{ extends(".*_string")="string" }
string ::= std_string | ind_string
std_string ::= STRING_OPEN string_part* STRING_CLOSE { pin=1 }
ind_string ::= IND_STRING_OPEN string_part* IND_STRING_CLOSE { pin=1 }
fake string ::= string_part* { methods=[ string_parts="string_part" ] }
;{ extends("string_text|antiquotation")=string_part }
string_part ::= string_text | antiquotation { recoverWhile=string_part_recover }
string_text ::= string_token+
Expand Down

0 comments on commit 76d4598

Please sign in to comment.