Skip to content

Commit

Permalink
Merge branch 'intellij-solidity-master'
Browse files Browse the repository at this point in the history
# Conflicts:
#	gradle.properties
#	src/main/kotlin/me/serce/solidity/ide/highlighter.kt
  • Loading branch information
miheevks committed Nov 10, 2022
2 parents b4b3b5e + b2cf6a4 commit 16d548c
Show file tree
Hide file tree
Showing 25 changed files with 210 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*.ipr
*.iws
gen
lib
/lib
.sandbox
.DS_Store
/out/
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version=[2.3.10-TL-0.9-SNAPSHOT
version=2.3.11-TL-0.9-SNAPSHOT
group=me.serce
kotlin_version=1.6.21
sentry_version=1.6.4
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class SolCreateFileAction : CreateFileFromTemplateAction(CAPTION, "", SolidityIc
) {
builder.setTitle(CAPTION)
.addKind("Smart contract", SolidityIcons.FILE_ICON, SMART_CONTRACT_TEMPLATE)
.addKind("Smart contract interface", SolidityIcons.FILE_ICON, "Solidity Contract Interface")
.addKind("Solidity library", SolidityIcons.FILE_ICON, "Solidity Library")
.setValidator(object : InputValidatorEx {
override fun checkInput(inputString: String): Boolean {
Expand Down
26 changes: 24 additions & 2 deletions src/main/kotlin/me/serce/solidity/ide/annotation/annotator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class SolidityAnnotator : Annotator {
when (element) {
is SolNumberType -> applyColor(holder, element, SolColor.TYPE)
is SolElementaryTypeName -> applyColor(holder, element, SolColor.TYPE)
is SolStateMutability -> if (element.text == "payable") {
applyColor(holder, element, SolColor.KEYWORD)
}
is SolEnumValue -> applyColor(holder, element, SolColor.ENUM_VALUE)
is SolMemberAccessExpression -> when(element.expression.firstChild.text) {
"super" -> applyColor(holder, element.expression.firstChild, SolColor.KEYWORD)
"msg", "block", "abi" -> applyColor(holder, element.expression.firstChild, SolColor.GLOBAL)
Expand All @@ -49,6 +53,7 @@ class SolidityAnnotator : Annotator {
is SolStructDefinition -> element.identifier?.let { applyColor(holder, it, SolColor.STRUCT_NAME) }
is SolEnumDefinition -> element.identifier?.let { applyColor(holder, it, SolColor.ENUM_NAME) }
is SolEventDefinition -> element.identifier?.let { applyColor(holder, it, SolColor.EVENT_NAME) }
is SolUserDefinedValueTypeDefinition -> element.identifier?.let { applyColor(holder, it, SolColor.USER_DEFINED_VALUE_TYPE) }
is SolConstantVariableDeclaration -> applyColor(holder, element.identifier, SolColor.CONSTANT)
is SolStateVariableDeclaration -> {
if (element.mutationModifier?.textMatches("constant") == true) {
Expand All @@ -57,7 +62,17 @@ class SolidityAnnotator : Annotator {
applyColor(holder, element.identifier, SolColor.STATE_VARIABLE)
}
}
is SolFunctionDefinition -> element.identifier?.let { applyColor(holder, it, SolColor.FUNCTION_DECLARATION) }
is SolFunctionDefinition -> {
val identifier = element.identifier
if (identifier !== null) {
applyColor(holder, identifier, SolColor.FUNCTION_DECLARATION)
} else {
val firstChildNode = element.node.firstChildNode
if (firstChildNode.text == "receive" || firstChildNode.text == "fallback") {
applyColor(holder, firstChildNode.textRange, SolColor.RECEIVE_FALLBACK_DECLARATION)
}
}
}
is SolModifierDefinition -> element.identifier?.let { applyColor(holder, it, SolColor.FUNCTION_DECLARATION) }
is SolModifierInvocation -> applyColor(holder, element.varLiteral.identifier, SolColor.FUNCTION_CALL)
is SolUserDefinedTypeName -> {
Expand All @@ -71,10 +86,17 @@ class SolidityAnnotator : Annotator {
is SolFunctionCallElement -> when(element.firstChild.text) {
"keccak256" -> applyColor(holder, element.firstChild, SolColor.GLOBAL_FUNCTION_CALL)
"require" -> applyColor(holder, element.firstChild, SolColor.KEYWORD)
"assert" -> applyColor(holder, element.firstChild, SolColor.KEYWORD)
else -> when(SolResolver.resolveTypeNameUsingImports(element).firstOrNull()) {
is SolErrorDefinition -> applyColor(holder, element.referenceNameElement, SolColor.ERROR_NAME)
is SolEventDefinition -> applyColor(holder, element.referenceNameElement, SolColor.EVENT_NAME)
else -> applyColor(holder, element.referenceNameElement, SolColor.FUNCTION_CALL)
else -> element.firstChild.let {
if (it is SolPrimaryExpression && SolResolver.resolveTypeNameUsingImports(element.firstChild).filterIsInstance<SolStructDefinition>().isNotEmpty()) {
applyColor(holder, element.referenceNameElement, SolColor.STRUCT_NAME)
} else {
applyColor(holder, element.referenceNameElement, SolColor.FUNCTION_CALL)
}
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/me/serce/solidity/ide/colors/SolColor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum class SolColor(humanName: String, default: TextAttributesKey) {
ERROR_NAME("Types//Error name", Defaults.CLASS_NAME),
EVENT_NAME("Types//Event name", Defaults.CLASS_NAME),
ENUM_NAME("Types//Enum name", Defaults.CLASS_NAME),
ENUM_VALUE("Types//Enum value", Defaults.STATIC_FIELD),
TYPE("Types//Value type", Defaults.KEYWORD),
USER_DEFINED_VALUE_TYPE("Types//User-defined value type", Defaults.CLASS_NAME),

Expand All @@ -21,6 +22,7 @@ enum class SolColor(humanName: String, default: TextAttributesKey) {
STATE_VARIABLE("Identifiers//State variable", Defaults.INSTANCE_FIELD),

FUNCTION_DECLARATION("Functions//Function declaration", Defaults.FUNCTION_DECLARATION),
RECEIVE_FALLBACK_DECLARATION("Functions//Receive/Fallback declaration", Defaults.STATIC_METHOD),
FUNCTION_CALL("Functions//Function call", Defaults.FUNCTION_CALL),
GLOBAL_FUNCTION_CALL("Functions//Global function call", Defaults.GLOBAL_VARIABLE),

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/me/serce/solidity/ide/highlighter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ object SolHighlighter : SyntaxHighlighterBase() {
IF, ELSE, FOR, WHILE, DO, BREAK, CONTINUE, THROW, USING, RETURN, RETURNS,
MAPPING, EVENT, /*ERROR,*/ ANONYMOUS, MODIFIER, ASSEMBLY,
VAR, STORAGE, MEMORY, WEI, ETHER, GWEI, SZABO, FINNEY, SECONDS, MINUTES, HOURS,
DAYS, WEEKS, YEARS, TYPE, VIRTUAL, OVERRIDE,
DAYS, WEEKS, YEARS, TYPE, VIRTUAL, OVERRIDE, IMMUTABLE, INDEXED,
NANO, NANOTON, NTON, TON, MICRO,
MICROTON, MILLI, MILLITON, KILOTON, KTON,
MEGATON, MTON, GIGATON, GTON,
Expand Down
33 changes: 18 additions & 15 deletions src/main/kotlin/me/serce/solidity/lang/resolve/engine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ object SolResolver {
val result = if (element is SolFunctionCallElement) {
resolveError(element) +
resolveEvent(element) +
resolveContractUsingImports(element, element.containingFile, true) +
resolveContract(element) +
resolveEnum(element) +
resolveUserDefinedValueType(element)
} else {
resolveContractUsingImports(element, element.containingFile, true) +
resolveContract(element) +
resolveEnum(element) +
resolveStruct(element) +
resolveUserDefinedValueType(element)
Expand All @@ -39,17 +39,18 @@ object SolResolver {
/**
* @param withAliases aliases are not recursive, so count them only at the first level of recursion
*/
private fun resolveContractUsingImports(
private fun <T: SolNamedElement>resolveUsingImports(
target: Class<T>,
element: PsiElement,
file: PsiFile,
withAliases: Boolean
): Set<SolContractDefinition> =
RecursionManager.doPreventingRecursion(ResolveContractKey(element.nameOrText, file), true) {
withAliases: Boolean,
): Set<T> =
RecursionManager.doPreventingRecursion(ResolveUsingImportsKey(element.nameOrText, file), true) {
if (element is SolUserDefinedTypeName && element.findIdentifiers().size > 1) {
emptySet()
} else {
val inFile = file.children
.filterIsInstance<SolContractDefinition>()
.filterIsInstance(target)
.filter { it.name == element.nameOrText }

val resolvedViaAlias = when (withAliases) {
Expand All @@ -64,36 +65,38 @@ object SolResolver {
}
}
}.flatMap { (alias, resolvedFile) ->
resolveContractUsingImports(alias, resolvedFile.containingFile, false)
resolveUsingImports(target, alias, resolvedFile.containingFile, false)
}
else -> emptyList()
}

val imported = file.children
.filterIsInstance<SolImportDirective>()
.mapNotNull { nullIfError { it.importPath?.reference?.resolve()?.containingFile } }
.flatMap { resolveContractUsingImports(element, it, false) }
.flatMap { resolveUsingImports(target, element, it, false) }

(inFile + resolvedViaAlias + imported).toSet()
}
} ?: emptySet()

private fun resolveContract(element: PsiElement): Set<SolContractDefinition> =
resolveUsingImports(SolContractDefinition::class.java, element, element.containingFile, true)
private fun resolveEnum(element: PsiElement): Set<SolNamedElement> =
resolveInFile<SolEnumDefinition>(element) + resolveInnerType<SolEnumDefinition>(element) { it.enumDefinitionList }
resolveInnerType<SolEnumDefinition>(element) { it.enumDefinitionList } + resolveUsingImports(SolEnumDefinition::class.java, element, element.containingFile, true)

private fun resolveStruct(element: PsiElement): Set<SolNamedElement> =
resolveInFile<SolStructDefinition>(element) + resolveInnerType<SolStructDefinition>(element) { it.structDefinitionList }
resolveInnerType<SolStructDefinition>(element) { it.structDefinitionList } + resolveUsingImports(SolStructDefinition::class.java, element, element.containingFile, true)

private fun resolveUserDefinedValueType(element: PsiElement): Set<SolNamedElement> =
resolveInFile<SolUserDefinedValueTypeDefinition>(element) + resolveInnerType<SolUserDefinedValueTypeDefinition>(
resolveInnerType<SolUserDefinedValueTypeDefinition>(
element,
{ it.userDefinedValueTypeDefinitionList })
{ it.userDefinedValueTypeDefinitionList }) + resolveUsingImports(SolUserDefinedValueTypeDefinition::class.java, element, element.containingFile, true)

private fun resolveEvent(element: PsiElement): Set<SolNamedElement> =
resolveInnerType<SolEventDefinition>(element) { it.eventDefinitionList }

private fun resolveError(element: PsiElement): Set<SolNamedElement> =
resolveInnerType<SolErrorDefinition>(element) { it.errorDefinitionList }
resolveInnerType<SolErrorDefinition>(element) { it.errorDefinitionList } + resolveUsingImports(SolErrorDefinition::class.java, element, element.containingFile, true)

private inline fun <reified T : SolNamedElement> resolveInFile(element: PsiElement) : Set<T> {
return element.parentOfType<SolidityFile>()
Expand Down Expand Up @@ -343,7 +346,7 @@ object SolResolver {
}
}

data class ResolveContractKey(val name: String?, val file: PsiFile)
data class ResolveUsingImportsKey(val name: String?, val file: PsiFile)

private fun <T> Sequence<T>.takeWhileInclusive(pred: (T) -> Boolean): Sequence<T> {
var shouldContinue = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,50 @@ class SolImportPathReference(element: SolImportPathElement) : SolReferenceBase<S
}
}

// default lib located at: forge-std/Test.sol => lib/forge-std/src/Test.sol
private fun findFoundryImportFile(file: VirtualFile, path: String): VirtualFile? {
// apply foundry remappings to import path
private fun applyRemappings(remappings: ArrayList<Pair<String,String>>, path: String):String {
var output = path;
remappings.forEach { (prefix, target) ->
if (path.contains(prefix)) {
output = path.replace(prefix, target)
return output
}
}
return output;
}

private fun foundryDefaultFallback(file: VirtualFile, path: String): VirtualFile? {
val count = Paths.get(path).nameCount;
if (count < 2) {
if (count<2) {
return null;
}
val libName = Paths.get(path).subpath(0, 1).toString();
val libFile = Paths.get(path).subpath(1, count).toString();
val libName = Paths.get(path).subpath(0,1).toString();
val libFile = Paths.get(path).subpath(1,count).toString();
val test = file.findFileByRelativePath("lib/$libName/src/$libFile");
return test;
}

// default lib located at: forge-std/Test.sol => lib/forge-std/src/Test.sol
private fun findFoundryImportFile(file: VirtualFile, path: String): VirtualFile? {
val testRemappingFile = file.findFileByRelativePath("remappings.txt");
val remappings = arrayListOf<Pair<String, String>>();
if (testRemappingFile != null) {
val mappingsContents = testRemappingFile.contentsToByteArray().toString(Charsets.UTF_8).split("[\r\n]+".toRegex());
mappingsContents.forEach { mapping ->
val splitMapping = mapping.split("=")
if (splitMapping.size == 2) {
remappings.add(Pair(splitMapping[0].trim(),splitMapping[1].trim()))
}
}
}

val remappedPath = applyRemappings(remappings, path);
val testRemappedPath = file.findFileByRelativePath(remappedPath);
val testFoundryFallback = foundryDefaultFallback(file, path);

return when {
test != null -> test
testRemappedPath != null -> testRemappedPath
testFoundryFallback != null -> testFoundryFallback
file.parent != null -> findFoundryImportFile(file.parent, path)
else -> null
}
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/me/serce/solidity/lang/resolve/ref/refs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ class SolFunctionCallReference(element: SolFunctionCallExpression) : SolReferenc
}

fun resolveFunctionCall(): Collection<SolCallable> {
if (element.parent is SolRevertStatement) {
return SolResolver.resolveTypeNameUsingImports(element).filterIsInstance<SolErrorDefinition>()
}
if (element.firstChild is SolPrimaryExpression) {
val structs = SolResolver.resolveTypeNameUsingImports(element.firstChild).filterIsInstance<SolStructDefinition>()
if (structs.isNotEmpty()) {
return structs
}
}
val resolved: Collection<SolCallable> = when (val expr = element.expression) {
is SolPrimaryExpression -> {
val regular = expr.varLiteral?.let { SolResolver.resolveVarLiteral(it) }
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/me/serce/solidity/lang/types/inference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ private fun getSolTypeFromUserDefinedTypeName(type: SolUserDefinedTypeName): Sol
is SolContractDefinition -> SolContract(it)
is SolStructDefinition -> SolStruct(it)
is SolEnumDefinition -> SolEnum(it)
is SolUserDefinedValueTypeDefinition -> getSolType(it.elementaryTypeName)
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

interface ${NAME} {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html>
<body>
Empty Solidity smart contract interface.
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract ${NAME} {
function ${NAME}(){
constructor(){

}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

library ${NAME} {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

<TYPE>uint8</TYPE> <KEYWORD>constant</KEYWORD> <CONSTANT>MASK</CONSTANT> = 0x01;

<KEYWORD>enum</KEYWORD> <ENUM_NAME>Types</ENUM_NAME> {
<ENUM_VALUE>Type1</ENUM_VALUE>,
<ENUM_VALUE>Type2</ENUM_VALUE>
}

<KEYWORD>type</KEYWORD> <USER_DEFINED_VALUE_TYPE>Arg</USER_DEFINED_VALUE_TYPE> <KEYWORD>is</KEYWORD> <TYPE>uint256</TYPE>;

/**
* @title Claimable
* @dev Extension for the Ownable contract, where the ownership needs to be claimed.
Expand All @@ -28,6 +35,9 @@
_;
}

<RECEIVE_FALLBACK_DECLARATION>receive</RECEIVE_FALLBACK_DECLARATION>() <KEYWORD>external</KEYWORD> <KEYWORD>payable</KEYWORD> {}
<RECEIVE_FALLBACK_DECLARATION>fallback</RECEIVE_FALLBACK_DECLARATION>() <KEYWORD>external</KEYWORD> {}

<KEYWORD>function</KEYWORD> <FUNCTION_DECLARATION>transferOwnership</FUNCTION_DECLARATION>(<TYPE>address</TYPE> newOwner) <FUNCTION_CALL>onlyOwner</FUNCTION_CALL> {
pendingOwner = newOwner;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,27 @@ class SolFunctionResolveTest : SolResolveTestBase() {
""")
}

fun testResolveImportedError() {
val file1 = InlineFile("""
error XyzError(uint x);
//x
""",
name = "Xyz.sol"
)

val file2 = InlineFile("""
import "./Xyz.sol";
contract B {
function doit(uint256[] storage array) {
revert XyzError(1);
//^
}
}
""")

testResolveBetweenFiles(file1, file2)
}

fun checkIsResolved(@Language("Solidity") code: String) {
val (refElement, _) = resolveInCode<SolFunctionCallExpression>(code)
assertNotNull(refElement.reference?.resolve())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package me.serce.solidity.lang.core.resolve

import me.serce.solidity.lang.psi.SolNamedElement

class SolImportResolveFoundryTest : SolResolveTestBase() {

fun testImportPathResolveFoundryRemappings() {
val testcases = arrayListOf<Pair<String, String>>(
Pair("lib/forge-std/src/Test.sol","contracts/ImportUsageFoundryStd.sol"),
Pair("lib/solmate/src/tokens/ERC721.sol","contracts/ImportUsageFoundrySolmate.sol"),
Pair("lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol","contracts/ImportUsageFoundryOpenzeppelin.sol"),
);
testcases.forEach { (targetFile, contractFile) ->
val file1 = myFixture.configureByFile(targetFile)
myFixture.configureByFile("remappings.txt")
myFixture.configureByFile(contractFile)
val (refElement) = findElementAndDataInEditor<SolNamedElement>("^")
val resolved = checkNotNull(refElement.reference?.resolve()) {
"Failed to resolve ${refElement.text}"
}
assertEquals(file1.name, resolved.containingFile.name)
}
}

override fun getTestDataPath() = "src/test/resources/fixtures/importRemappings/"
}
Loading

0 comments on commit 16d548c

Please sign in to comment.