Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
MatejKastak committed Nov 12, 2022
1 parent 01233bb commit ebf48bb
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 5 deletions.
32 changes: 32 additions & 0 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,38 @@
"type": "string",
"default": "",
"description": "Specifies an absolute path to the directory used to store samples and module data. Used only for local YARI. (example: /home/user/samples)"
},
"yls.snippets.metaEntries": {
"type": "object",
"default": {},
"description": "A set of metadata entries to insert into rules. Empty values will create meta keys with a tabstop, and built-in variables are accepted (though transforms are ignored).",
"maxProperties": 50,
"additionalProperties": true
},
"yls.snippets.sortMeta": {
"type": "boolean",
"default": true,
"description": "Sort the metadata entries in alphabetical order by keys. Otherwise, insert keys as listed."
},
"yls.snippets.condition": {
"type": "boolean",
"default": true,
"description": "Enable the condition snippet on YARA rules. Has no effect on the presence of the condition section in the rule snippet."
},
"yls.snippets.meta": {
"type": "boolean",
"default": true,
"description": "Enable the meta snippet on YARA rules. Has no effect on the presence of the meta section in the rule snippet."
},
"yls.snippets.rule": {
"type": "boolean",
"default": true,
"description": "Enable the rule skeleton snippet on YARA rules. Settings for other snippets do not affect the sections provided in this snippet."
},
"yls.snippets.strings": {
"type": "boolean",
"default": true,
"description": "Enable the strings snippet on YARA rules. Has no effect on the presence of the strings section in the rule snippet."
}
}
},
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_snippet_string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# type: ignore

from yls.snippet_string import SnippetString


def test_simple():
snip = SnippetString("test")
assert str(snip) == "test"


def test_placeholder():
snip = SnippetString()
snip.append_placeholder("one")
snip.append_placeholder("two")
assert str(snip) == "${1:one}${2:two}"


def test_complex():
snip = SnippetString()
snip.append_text("text\n")
snip.append_placeholder("one")
snip.append_tabstop()
snip.append_placeholder("two")
snip.append_variable("CLIPBOARD", "")
snip.append_variable("INVALID_VARIABLE", "default")
snip.append_choice(("c", "ch", "choice"))
assert str(snip) == "text\n${1:one}$2${3:two}${CLIPBOARD}${default}${4|c,ch,choice|}"
102 changes: 102 additions & 0 deletions tests/unit/test_snippets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# type: ignore

import pytest

from yls.snippets import SnippetGenerator


@pytest.mark.parametrize(
"config, expected",
(
(
None,
"""rule ${1:my_rule} {
\tmeta:
\t\t${2:KEY} = ${3:"VALUE"}
\tstrings:
\t\t${4:\\$name} = ${5|"string",/regex/,{ HEX }|}
\tcondition:
\t\t${6:any of them}
}
""",
),
(
{},
"""rule ${1:my_rule} {
\tmeta:
\t\t${2:KEY} = ${3:"VALUE"}
\tstrings:
\t\t${4:\\$name} = ${5|"string",/regex/,{ HEX }|}
\tcondition:
\t\t${6:any of them}
}
""",
),
(
{"metaEntries": {"author": "test user", "hash": ""}},
"""rule ${1:my_rule} {
\tmeta:
\t\tauthor = "test user"
\t\thash = "$2"
\tstrings:
\t\t${3:\\$name} = ${4|"string",/regex/,{ HEX }|}
\tcondition:
\t\t${5:any of them}
}
""",
),
(
{"metaEntries": {"filename": "${TM_FILENAME}"}},
"""rule ${1:my_rule} {
\tmeta:
\t\tfilename = "${TM_FILENAME}"
\tstrings:
\t\t${2:\\$name} = ${3|"string",/regex/,{ HEX }|}
\tcondition:
\t\t${4:any of them}
}
""",
),
(
{
"metaEntries": {
"author": "",
"date": "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}",
}
},
"""rule ${1:my_rule} {
\tmeta:
\t\tauthor = "$2"
\t\tdate = "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}"
\tstrings:
\t\t${3:\\$name} = ${4|"string",/regex/,{ HEX }|}
\tcondition:
\t\t${5:any of them}
}
""",
),
(
{
"metaEntries": {
"date": "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}",
"author": "",
},
"sortMeta": True,
},
"""rule ${1:my_rule} {
\tmeta:
\t\tauthor = "$2"
\t\tdate = "${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}"
\tstrings:
\t\t${3:\\$name} = ${4|"string",/regex/,{ HEX }|}
\tcondition:
\t\t${5:any of them}
}
""",
),
),
)
def test_basic(config, expected):
generator = SnippetGenerator(config)

assert expected == generator.generate()
21 changes: 18 additions & 3 deletions yls/completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from yls.plugin_manager_provider import PluginManagerProvider
from yls.strings import estimate_string_type
from yls.strings import string_modifiers_completion_items
from yls.snippets import SnippetGenerator

log = logging.getLogger(__name__)

Expand All @@ -26,8 +27,9 @@ def __init__(self, ls: Any):
self.ls = ls
self.completion_cache = completion.CompletionCache.from_yaramod(self.ls.ymod)

def complete(self, params: lsp_types.CompletionParams) -> lsp_types.CompletionList:
return lsp_types.CompletionList(is_incomplete=False, items=self._complete(params))
async def complete(self, params: lsp_types.CompletionParams) -> lsp_types.CompletionList:
items = await self._complete(params)
return lsp_types.CompletionList(is_incomplete=False, items=items)

def signature_help(self, params: lsp_types.CompletionParams) -> lsp_types.SignatureHelp | None:
signatures = self._signature_help(params)
Expand Down Expand Up @@ -71,7 +73,7 @@ def _signature_help(

return info

def _complete(self, params: lsp_types.CompletionParams) -> list[lsp_types.CompletionItem]:
async def _complete(self, params: lsp_types.CompletionParams) -> list[lsp_types.CompletionItem]:
document = self.ls.workspace.get_document(params.text_document.uri)

res = []
Expand Down Expand Up @@ -100,6 +102,10 @@ def _complete(self, params: lsp_types.CompletionParams) -> list[lsp_types.Comple
log.debug("[COMPLETION] Adding last valid yara file")
res += self.complete_last_valid_yara_file(document, params, word)

# Dynamic snippets
log.debug("[COMPLETION] Adding dynamic snippets")
res += await self.complete_dynamic_snippets()

# Plugin completion
log.debug("COMPLETION] Adding completion items from plugings")
res += utils.flatten_list(
Expand Down Expand Up @@ -238,3 +244,12 @@ def complete_condition_keywords(

res.append(item)
return res

async def complete_dynamic_snippets(self) -> list[lsp_types.CompletionItem]:
config = await utils.get_config_from_editor(self.ls, "yls.snippets")
log.debug(f"[COMPLETION] lsp configuration {config=}")
if config is None:
return []

generator = SnippetGenerator(config)
return generator.generate_snippets()
4 changes: 2 additions & 2 deletions yls/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,13 @@ def initiliazed(ls: YaraLanguageServer, _params: Any) -> None:


@SERVER.feature(COMPLETION, lsp_types.CompletionOptions(trigger_characters=["."]))
def completion(
async def completion(
ls: YaraLanguageServer, params: lsp_types.CompletionParams
) -> lsp_types.CompletionList:
"""Code completion."""
utils.log_command(COMPLETION)

return ls.completer.complete(params)
return await ls.completer.complete(params)


@SERVER.feature(SIGNATURE_HELP, lsp_types.SignatureHelpOptions(trigger_characters=["("]))
Expand Down
74 changes: 74 additions & 0 deletions yls/snippet_string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations
from typing import Iterable

SUPPORTED_VARIABLES = {
"TM_SELECTED_TEXT",
"TM_CURRENT_LINE",
"TM_CURRENT_WORD",
"TM_LINE_INDEX",
"TM_LINE_NUMBER",
"TM_FILENAME",
"TM_FILENAME_BASE",
"TM_DIRECTORY",
"TM_FILEPATH",
"RELATIVE_FILEPATH",
"CLIPBOARD",
"WORKSPACE_NAME",
"WORKSPACE_FOLDER",
"CURSOR_INDEX",
"CURSOR_NUMBER",
"CURRENT_YEAR",
"CURRENT_YEAR_SHORT",
"CURRENT_MONTH",
"CURRENT_MONTH_NAME",
"CURRENT_MONTH_NAME_SHORT",
"CURRENT_DATE",
"CURRENT_DAY_NAME",
"CURRENT_DAY_NAME_SHORT",
"CURRENT_HOUR",
"CURRENT_MINUTE",
"CURRENT_SECOND",
"CURRENT_SECONDS_UNIX",
"RANDOM",
"RANDOM_HEX",
"UUID",
"BLOCK_COMMENT_START",
"BLOCK_COMMENT_END",
"LINE_COMMENT",
}


class SnippetString:
cur_idx: int
value: str

def __init__(self, value: str = ""):
self.value = value
self.cur_idx = 1

def append_choice(self, values: Iterable[str]) -> None:
self.value += f"${{{self.get_and_inc()}|{','.join(values)}|}}"

def append_placeholder(self, value: str) -> None:
self.value += f"${{{self.get_and_inc()}:{value}}}"

def append_tabstop(self) -> None:
self.value += f"${self.get_and_inc()}"

def append_text(self, value: str) -> None:
"""WARNING: For now you are expected to escape the string if necessary."""
self.value += value

def append_variable(self, name: str, default_value: str) -> None:
if name in SUPPORTED_VARIABLES:
self.value += f"${{{name}}}"
else:
self.value += f"${{{default_value}}}"

def get_and_inc(self) -> int:
i = self.cur_idx
self.cur_idx += 1
return i

def __str__(self) -> str:
return self.value
Loading

0 comments on commit ebf48bb

Please sign in to comment.