Merge commit '45ce22c16ec924e34517cf785e23c07952e45893'

pull/315/head
Wilfred Hughes 2022-07-10 23:03:39 +07:00
commit a6eb1fb41d
20 changed files with 6131 additions and 5309 deletions

@ -2,13 +2,9 @@ node_modules
queries
package-lock.json
build
target
Cargo.lock
npm-debug.log
log.html
tree-sitter-hcl.wasm
.env
.DS_Store
fuzz/fuzzer
fuzz/*.o
fuzz/*.a
fuzz/fuzz-*.log

@ -1,12 +1,15 @@
# Changelog
## 0.7.0 - not yet released
## 0.7.0 - 2022-06-02
housekeeping:
* bump tree-sitter dependency to 0.20.6
* regenerate parser
* fix clang+windows CI job
fix:
* allow empty string literals
## 0.6.0 - 2021-09-20
feature:

@ -0,0 +1,26 @@
[package]
name = "tree-sitter-hcl"
description = "hcl grammar for the tree-sitter parsing library"
version = "0.0.1"
keywords = ["incremental", "parsing", "hcl"]
categories = ["parsing", "text-editors"]
repository = "https://github.com/MichaHoffmann/tree-sitter-hcl"
edition = "2018"
license = "Apache"
build = "bindings/rust/build.rs"
include = [
"bindings/rust/*",
"grammar.js",
"queries/*",
"src/*",
]
[lib]
path = "bindings/rust/lib.rs"
[dependencies]
tree-sitter = "~0.20"
[build-dependencies]
cc = "1.0"

@ -2,6 +2,10 @@
tree-sitter grammar for the [HCL](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md) language
## Try It Out
Try the parser in the [playground](https://michahoffmann.github.io/tree-sitter-hcl/)
## Example
Highlighting `example/example.hcl`:
@ -30,3 +34,6 @@ Total parses: 1892; successful parses: 1892; failed parses: 0; success percentag
See the [fuzzing repo for this parser](https://github.com/MichaHoffmann/tree-sitter-hcl-fuzz)
## Attributions
Pages were copied from https://github.com/m-novikov/tree-sitter-sql

@ -0,0 +1,19 @@
{
"targets": [
{
"target_name": "tree_sitter_hcl_binding",
"include_dirs": [
"<!(node -e \"require('nan')\")",
"src"
],
"sources": [
"bindings/node/binding.cc",
"src/parser.c",
# If your language uses an external scanner, add it here.
],
"cflags_c": [
"-std=c99",
]
}
]
}

@ -0,0 +1,28 @@
#include "tree_sitter/parser.h"
#include <node.h>
#include "nan.h"
using namespace v8;
extern "C" TSLanguage * tree_sitter_hcl();
namespace {
NAN_METHOD(New) {}
void Init(Local<Object> exports, Local<Object> module) {
Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
tpl->SetClassName(Nan::New("Language").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
Local<Object> instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
Nan::SetInternalFieldPointer(instance, 0, tree_sitter_hcl());
Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("hcl").ToLocalChecked());
Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
}
NODE_MODULE(tree_sitter_hcl_binding, Init)
} // namespace

@ -0,0 +1,19 @@
try {
module.exports = require("../../build/Release/tree_sitter_hcl_binding");
} catch (error1) {
if (error1.code !== 'MODULE_NOT_FOUND') {
throw error1;
}
try {
module.exports = require("../../build/Debug/tree_sitter_hcl_binding");
} catch (error2) {
if (error2.code !== 'MODULE_NOT_FOUND') {
throw error2;
}
throw error1
}
}
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

@ -0,0 +1,35 @@
fn main() {
let src_dir = std::path::Path::new("src");
let mut c_config = cc::Build::new();
c_config.include(&src_dir);
c_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable")
.flag_if_supported("-Wno-trigraphs");
let parser_path = src_dir.join("parser.c");
c_config.file(&parser_path);
// If your language uses an external scanner written in C,
// then include this block of code:
/*
let scanner_path = src_dir.join("scanner.c");
c_config.file(&scanner_path);
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
*/
c_config.compile("parser");
println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
let mut cpp_config = cc::Build::new();
cpp_config.cpp(true);
cpp_config.include(&src_dir);
cpp_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable");
let scanner_path = src_dir.join("scanner.cc");
cpp_config.file(&scanner_path);
cpp_config.compile("scanner");
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
}

@ -0,0 +1,52 @@
//! This crate provides hcl language support for the [tree-sitter][] parsing library.
//!
//! Typically, you will use the [language][language func] function to add this language to a
//! tree-sitter [Parser][], and then use the parser to parse some code:
//!
//! ```
//! let code = "";
//! let mut parser = tree_sitter::Parser::new();
//! parser.set_language(tree_sitter_hcl::language()).expect("Error loading hcl grammar");
//! let tree = parser.parse(code, None).unwrap();
//! ```
//!
//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
//! [language func]: fn.language.html
//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
//! [tree-sitter]: https://tree-sitter.github.io/
use tree_sitter::Language;
extern "C" {
fn tree_sitter_hcl() -> Language;
}
/// Get the tree-sitter [Language][] for this grammar.
///
/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
pub fn language() -> Language {
unsafe { tree_sitter_hcl() }
}
/// The content of the [`node-types.json`][] file for this grammar.
///
/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json");
// Uncomment these to include any queries that this grammar contains
// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm");
// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm");
// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm");
// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm");
#[cfg(test)]
mod tests {
#[test]
fn test_can_load_grammar() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(super::language())
.expect("Error loading hcl language");
}
}

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Tree Sitter HCL Playground</title>
<style>
#playground-container {
max-width: 640px;
margin-left: auto;
margin-right: auto;
}
#playground-container .CodeMirror {
border: 1px solid;
}
#create-issue-btn {
padding: 0.2em;
float: right;
font-size: 1.5em;
}
#checkboxes {
padding-bottom: 1em;
}
#output-container {
border: 1px solid;
}
.highlight {
background-color: #f8f8f8;
}
</style>
</head>
<body>
<!--
This file is licensed under MIT license
Copyright (c) 2018 Max Brunsfeld
Taken from https://github.com/tree-sitter/tree-sitter/docs/section-7-playground.html
-->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.css"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.css"
/>
<div id="playground-container">
<h1>Tree Sitter HCL Playground</h1>
<h4>Code</h4>
<div id="checkboxes">
<input id="logging-checkbox" type="checkbox" />
<label for="logging-checkbox">Log</label>
<input id="query-checkbox" type="checkbox" />
<label for="query-checkbox">Query</label>
</div>
<textarea id="code-input">
example "test" {
foo = "bar"
}
</textarea>
<div id="query-container" style="visibility: hidden; position: absolute">
<h4>Query</h4>
<textarea id="query-input"></textarea>
</div>
<h4>Tree</h4>
<span id="update-time"></span>
<div id="output-container-scroll">
<pre id="output-container" class="highlight"></pre>
</div>
<button id="create-issue-btn" type="button">Create Issue</button>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.js"></script>
<script src="./vendor/tree-sitter.js"></script>
<script id="playground-script" src="./playground.js?v=3"></script>
</body>
</html>

@ -0,0 +1,498 @@
// This file is licensed under MIT license
// Copyright (c) 2018 Max Brunsfeld
// Taken from https://github.com/tree-sitter/tree-sitter/docs/assets/playground.js
let tree;
(async () => {
const CAPTURE_REGEX = /@\s*([\w\._-]+)/g;
const COLORS_BY_INDEX = [
"blue",
"chocolate",
"darkblue",
"darkcyan",
"darkgreen",
"darkred",
"darkslategray",
"dimgray",
"green",
"indigo",
"navy",
"red",
"sienna",
];
const scriptURL = document.getElementById("playground-script").src;
const codeInput = document.getElementById("code-input");
const loggingCheckbox = document.getElementById("logging-checkbox");
const outputContainer = document.getElementById("output-container");
const outputContainerScroll = document.getElementById(
"output-container-scroll",
);
const playgroundContainer = document.getElementById("playground-container");
const queryCheckbox = document.getElementById("query-checkbox");
const createIssueBtn = document.getElementById("create-issue-btn");
const queryContainer = document.getElementById("query-container");
const queryInput = document.getElementById("query-input");
const updateTimeSpan = document.getElementById("update-time");
loadState();
await TreeSitter.init();
const parser = new TreeSitter();
const codeEditor = CodeMirror.fromTextArea(codeInput, {
lineNumbers: true,
showCursorWhenSelecting: true,
});
const queryEditor = CodeMirror.fromTextArea(queryInput, {
lineNumbers: true,
showCursorWhenSelecting: true,
});
const cluster = new Clusterize({
rows: [],
noDataText: null,
contentElem: outputContainer,
scrollElem: outputContainerScroll,
});
const renderTreeOnCodeChange = debounce(renderTree, 50);
const saveStateOnChange = debounce(saveState, 2000);
const runTreeQueryOnChange = debounce(runTreeQuery, 50);
let languageName = "hcl";
let treeRows = null;
let treeRowHighlightedIndex = -1;
let parseCount = 0;
let isRendering = 0;
let query;
codeEditor.on("changes", handleCodeChange);
codeEditor.on("viewportChange", runTreeQueryOnChange);
codeEditor.on("cursorActivity", debounce(handleCursorMovement, 150));
queryEditor.on("changes", debounce(handleQueryChange, 150));
loggingCheckbox.addEventListener("change", handleLoggingChange);
queryCheckbox.addEventListener("change", handleQueryEnableChange);
outputContainer.addEventListener("click", handleTreeClick);
createIssueBtn.addEventListener("click", handleCreateIssue);
handleQueryEnableChange();
await loadLanguage();
playgroundContainer.style.visibility = "visible";
async function loadLanguage() {
const query = new URL(scriptURL).search;
const url = `tree-sitter-hcl.wasm${query}`;
const language = await TreeSitter.Language.load(url);
tree = null;
parser.setLanguage(language);
handleCodeChange();
handleQueryChange();
}
async function handleCodeChange(editor, changes) {
const newText = codeEditor.getValue() + "\n";
const edits = tree && changes && changes.map(treeEditForEditorChange);
const start = performance.now();
if (edits) {
for (const edit of edits) {
tree.edit(edit);
}
}
const newTree = parser.parse(newText, tree);
const duration = (performance.now() - start).toFixed(1);
updateTimeSpan.innerText = `${duration} ms`;
if (tree) tree.delete();
tree = newTree;
parseCount++;
renderTreeOnCodeChange();
runTreeQueryOnChange();
saveStateOnChange();
}
async function renderTree() {
isRendering++;
const cursor = tree.walk();
let currentRenderCount = parseCount;
let row = "";
let rows = [];
let finishedRow = false;
let visitedChildren = false;
let indentLevel = 0;
for (let i = 0; ; i++) {
if (i > 0 && i % 10000 === 0) {
await new Promise(r => setTimeout(r, 0));
if (parseCount !== currentRenderCount) {
cursor.delete();
isRendering--;
return;
}
}
let displayName;
if (cursor.nodeIsMissing) {
displayName = `MISSING ${cursor.nodeType}`;
} else if (cursor.nodeIsNamed) {
displayName = cursor.nodeType;
}
if (visitedChildren) {
if (displayName) {
finishedRow = true;
}
if (cursor.gotoNextSibling()) {
visitedChildren = false;
} else if (cursor.gotoParent()) {
visitedChildren = true;
indentLevel--;
} else {
break;
}
} else {
if (displayName) {
if (finishedRow) {
row += "</div>";
rows.push(row);
finishedRow = false;
}
const start = cursor.startPosition;
const end = cursor.endPosition;
const id = cursor.nodeId;
let fieldName = cursor.currentFieldName();
if (fieldName) {
fieldName += ": ";
} else {
fieldName = "";
}
row = `<div>${" ".repeat(
indentLevel,
)}${fieldName}<a class='plain' href="#" data-id=${id} data-range="${
start.row
},${start.column},${end.row},${end.column}">${displayName}</a> [${
start.row
}, ${start.column}] - [${end.row}, ${end.column}])`;
finishedRow = true;
}
if (cursor.gotoFirstChild()) {
visitedChildren = false;
indentLevel++;
} else {
visitedChildren = true;
}
}
}
if (finishedRow) {
row += "</div>";
rows.push(row);
}
cursor.delete();
cluster.update(rows);
treeRows = rows;
isRendering--;
handleCursorMovement();
}
function runTreeQuery(_, startRow, endRow) {
if (endRow == null) {
const viewport = codeEditor.getViewport();
startRow = viewport.from;
endRow = viewport.to;
}
codeEditor.operation(() => {
const marks = codeEditor.getAllMarks();
marks.forEach(m => m.clear());
if (tree && query) {
const captures = query.captures(
tree.rootNode,
{ row: startRow, column: 0 },
{ row: endRow, column: 0 },
);
let lastNodeId;
for (const { name, node } of captures) {
if (node.id === lastNodeId) continue;
lastNodeId = node.id;
const { startPosition, endPosition } = node;
codeEditor.markText(
{ line: startPosition.row, ch: startPosition.column },
{ line: endPosition.row, ch: endPosition.column },
{
inclusiveLeft: true,
inclusiveRight: true,
css: `color: ${colorForCaptureName(name)}`,
},
);
}
}
});
}
function handleQueryChange() {
if (query) {
query.delete();
query.deleted = true;
query = null;
}
queryEditor.operation(() => {
queryEditor.getAllMarks().forEach(m => m.clear());
if (!queryCheckbox.checked) return;
const queryText = queryEditor.getValue();
try {
query = parser.getLanguage().query(queryText);
let match;
let row = 0;
queryEditor.eachLine(line => {
while ((match = CAPTURE_REGEX.exec(line.text))) {
queryEditor.markText(
{ line: row, ch: match.index },
{ line: row, ch: match.index + match[0].length },
{
inclusiveLeft: true,
inclusiveRight: true,
css: `color: ${colorForCaptureName(match[1])}`,
},
);
}
row++;
});
} catch (error) {
const startPosition = queryEditor.posFromIndex(error.index);
const endPosition = {
line: startPosition.line,
ch: startPosition.ch + (error.length || Infinity),
};
if (error.index === queryText.length) {
if (startPosition.ch > 0) {
startPosition.ch--;
} else if (startPosition.row > 0) {
startPosition.row--;
startPosition.column = Infinity;
}
}
queryEditor.markText(startPosition, endPosition, {
className: "query-error",
inclusiveLeft: true,
inclusiveRight: true,
attributes: { title: error.message },
});
}
});
runTreeQuery();
saveQueryState();
}
function handleCursorMovement() {
if (isRendering) return;
const selection = codeEditor.getDoc().listSelections()[0];
let start = { row: selection.anchor.line, column: selection.anchor.ch };
let end = { row: selection.head.line, column: selection.head.ch };
if (
start.row > end.row ||
(start.row === end.row && start.column > end.column)
) {
let swap = end;
end = start;
start = swap;
}
const node = tree.rootNode.namedDescendantForPosition(start, end);
if (treeRows) {
if (treeRowHighlightedIndex !== -1) {
const row = treeRows[treeRowHighlightedIndex];
if (row)
treeRows[treeRowHighlightedIndex] = row.replace(
"highlighted",
"plain",
);
}
treeRowHighlightedIndex = treeRows.findIndex(row =>
row.includes(`data-id=${node.id}`),
);
if (treeRowHighlightedIndex !== -1) {
const row = treeRows[treeRowHighlightedIndex];
if (row)
treeRows[treeRowHighlightedIndex] = row.replace(
"plain",
"highlighted",
);
}
cluster.update(treeRows);
const lineHeight = cluster.options.item_height;
const scrollTop = outputContainerScroll.scrollTop;
const containerHeight = outputContainerScroll.clientHeight;
const offset = treeRowHighlightedIndex * lineHeight;
if (scrollTop > offset - 20) {
$(outputContainerScroll).animate({ scrollTop: offset - 20 }, 150);
} else if (scrollTop < offset + lineHeight + 40 - containerHeight) {
$(outputContainerScroll).animate(
{ scrollTop: offset - containerHeight + 40 },
150,
);
}
}
}
function handleCreateIssue() {
const queryText = codeEditor.getValue();
const outputText = outputContainer.innerText;
const title = `Error parsing SQL`;
const body = `Error when parsing the following SQL:
\`\`\`
${queryText}
\`\`\`
Error:
\`\`\`
${outputText}
\`\`\``;
const queryParams = `title=${encodeURIComponent(
title,
)}&body=${encodeURIComponent(body)}`;
const url = `https://github.com/MichaHoffmann/tree-sitter-hcl/issues/new?${queryParams}`;
window.open(url);
}
function handleTreeClick(event) {
if (event.target.tagName === "A") {
event.preventDefault();
const [startRow, startColumn, endRow, endColumn] =
event.target.dataset.range.split(",").map(n => parseInt(n));
codeEditor.focus();
codeEditor.setSelection(
{ line: startRow, ch: startColumn },
{ line: endRow, ch: endColumn },
);
}
}
function handleLoggingChange() {
if (loggingCheckbox.checked) {
parser.setLogger((message, lexing) => {
if (lexing) {
console.log(" ", message);
} else {
console.log(message);
}
});
} else {
parser.setLogger(null);
}
}
function handleQueryEnableChange() {
if (queryCheckbox.checked) {
queryContainer.style.visibility = "";
queryContainer.style.position = "";
} else {
queryContainer.style.visibility = "hidden";
queryContainer.style.position = "absolute";
}
handleQueryChange();
}
function treeEditForEditorChange(change) {
const oldLineCount = change.removed.length;
const newLineCount = change.text.length;
const lastLineLength = change.text[newLineCount - 1].length;
const startPosition = { row: change.from.line, column: change.from.ch };
const oldEndPosition = { row: change.to.line, column: change.to.ch };
const newEndPosition = {
row: startPosition.row + newLineCount - 1,
column:
newLineCount === 1
? startPosition.column + lastLineLength
: lastLineLength,
};
const startIndex = codeEditor.indexFromPos(change.from);
let newEndIndex = startIndex + newLineCount - 1;
let oldEndIndex = startIndex + oldLineCount - 1;
for (let i = 0; i < newLineCount; i++) newEndIndex += change.text[i].length;
for (let i = 0; i < oldLineCount; i++)
oldEndIndex += change.removed[i].length;
return {
startIndex,
oldEndIndex,
newEndIndex,
startPosition,
oldEndPosition,
newEndPosition,
};
}
function colorForCaptureName(capture) {
const id = query.captureNames.indexOf(capture);
return COLORS_BY_INDEX[id % COLORS_BY_INDEX.length];
}
function storageGetItem(lookupKey) {
try {
return localStorage.getItem(lookupKey);
} catch {
return null;
}
}
function storageSetItem(lookupKey, value) {
try {
return localStorage.setIem(lookupKey, value);
} catch {}
}
function loadState() {
const language = storageGetItem("language");
const sourceCode = storageGetItem("sourceCode");
const query = storageGetItem("query");
const queryEnabled = storageGetItem("queryEnabled");
if (language != null && sourceCode != null && query != null) {
queryInput.value = query;
codeInput.value = sourceCode;
queryCheckbox.checked = queryEnabled === "true";
}
}
function saveState() {
storageSetItem("sourceCode", codeEditor.getValue());
saveQueryState();
}
function saveQueryState() {
storageSetItem("queryEnabled", queryCheckbox.checked);
storageSetItem("query", queryEditor.getValue());
}
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this,
args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
})();

File diff suppressed because one or more lines are too long

@ -108,7 +108,7 @@ module.exports = grammar({
string_lit: $ => prec(PREC.string_lit, seq(
$.quoted_template_start,
$.template_literal,
optional($.template_literal),
$.quoted_template_end,
)),

@ -313,8 +313,16 @@
"name": "quoted_template_start"
},
{
"type": "SYMBOL",
"name": "template_literal"
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "template_literal"
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",

File diff suppressed because it is too large Load Diff

@ -29,6 +29,25 @@ block_1 {}
(block_start)
(block_end))))
================================================================================
basic block with empty type
================================================================================
block_1 "" {
}
--------------------------------------------------------------------------------
(config_file
(body
(block
(identifier)
(string_lit
(quoted_template_start)
(quoted_template_end))
(block_start)
(block_end))))
================================================================================
block with attribute
================================================================================
@ -166,8 +185,8 @@ locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") }
(template_literal)
(quoted_template_end))))
(expression
(template_expr
(quoted_template
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end)))))))))
(block_end))))

@ -126,8 +126,8 @@ resource "azurerm_storage_blob" "proxy_cert" {
(identifier))
(get_attr
(identifier))
(template_expr
(quoted_template
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end))))))
(expression
@ -158,8 +158,8 @@ resource "azurerm_storage_blob" "proxy_cert" {
(identifier))
(get_attr
(identifier))
(template_expr
(quoted_template
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end))))))
(expression
@ -203,8 +203,8 @@ resource "azurerm_storage_blob" "proxy_cert" {
(identifier))
(get_attr
(identifier))
(template_expr
(quoted_template
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end))))))
(expression
@ -259,8 +259,8 @@ resource "azurerm_storage_blob" "proxy_cert" {
(identifier))
(get_attr
(identifier))
(template_expr
(quoted_template
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end))))))
(expression
@ -315,8 +315,8 @@ resource "azurerm_storage_blob" "proxy_cert" {
(identifier))
(get_attr
(identifier))
(template_expr
(quoted_template
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end))))))
(expression
@ -396,16 +396,16 @@ resource "azurerm_storage_blob" "proxy_cert" {
(identifier))
(get_attr
(identifier))
(template_expr
(quoted_template
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end)))))
(variable_expr
(identifier))))
(get_attr
(identifier))
(template_expr
(quoted_template
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end))))))
(expression

@ -1,3 +1,21 @@
================================================================================
empty string
================================================================================
foo = ""
--------------------------------------------------------------------------------
(config_file
(body
(attribute
(identifier)
(expression
(literal_value
(string_lit
(quoted_template_start)
(quoted_template_end)))))))
================================================================================
unescaped tab
================================================================================