1 弁言
编译器除了天生语法树以外,还要在输入涌现毛病时给出适当的提醒。
比方当用户输入 select (name
,这是个未完成的 SQL 语句,我们的目的是提醒出这个语句未完成,并给出后续的发起: )
-
+
%
/
*
.
(
。
2 精读
剖析一个 SQL 语句,现将 query 字符串转成 Token 数组,再组织文法树剖析,那末能够涌现毛病的状况有两种:
- 语句毛病。
- 文法未完成。
给出毛病提醒的第一步是推断毛病发作。
经由历程这张 Token 婚配历程图能够发明,当深度优先遍历文法节点时,婚配胜利后才会返回父元素继承往下走。而当走到父元素没有根节点了才算婚配胜利;当尝试 Chance 时没有机会了,就是毛病发作的机遇。
所以我们只需找到末了一个婚配胜利的节点,再依据末了胜利与否,以及搜刮出下一个能够节点,就可以晓得毛病范例以及给出发起了。
function onMatchNode(matchNode, store) {
const matchResult = matchNode.run(store.scanner);
if (!matchResult.match) {
tryChances(matchNode, store);
} else {
const restTokenCount = store.scanner.getRestTokenCount();
if (matchNode.matching.type !== "loose") {
if (!lastMatch) {
lastMatch = {
matchNode,
token: matchResult.token,
restTokenCount
};
}
}
callParentNode(matchNode, store, matchResult.token);
}
}
所以在运转语法剖析器时,在碰到婚配节点(MatchNode
)时,假如婚配胜利,就记录下这个节点,如许我们最终会找到末了一个婚配胜利的节点:lastMatch
。
以后经由历程 findNextMatchNodes
函数找到下一个能够的引荐节点列表,作为毛病恢复的发起。
findNextMatchNodes
函数会依据某个节点,找出下一节点一切能够 Tokens 列表,这个函数背面文章再特地引见,或许你也能够先浏览
源码.
语句毛病
也就是任何一个 Token 婚配失利。比方:
select * from table_name as table1 error_string;
这里 error_string 就是冗余的语句。
经由历程语法剖析器剖析,能够获得实行失利的效果,然后经由历程 findNextMatchNodes
函数,我们能够获得下面剖析效果:
能够看到,顺序推断出了 error_string 这个 Token 属于毛病范例,同时给出发起,能够将 error_string 替换成这 14 个发起字符串中恣意一个,都能使语句准确。
之所以失利范例推断为毛病范例,是由于查找了这个准确 Token table1
背面另有一个没有被运用的 error_string
,所以毛病归类是 wrong
。
注重,这里给出的是下一个 Token 发起,而不是悉数 Token 发起,因而引荐了 where 示意 “或许背面跟一个完全的 where 语句”。
文法未完成
和语句毛病差别,这类毛病一切输入的单词都是准确的,但却没有写完。比方:
select *
经由历程语法剖析器剖析,能够获得实行失利的效果,然后经由历程 findNextMatchNodes
函数,我们能够获得下面剖析效果:
能够看到,顺序推断出了 * 这个 Token 属于未完成的毛病范例,发起在背面补全这 14 个发起字符串中恣意一个。比较轻易联想到的是 where
,但也能够是恣意子文法的未完成状况,比方背面补充 ,
继承填写字段,或许直接跟一个单词示意别号,或许先输入 as
再跟别号。
之所以失利范例推断为未完成,是由于末了一个准确 Token *
以后没有 Token 了,但语句剖析失利,那只有一个缘由,就是语句为写完,因而毛病归类是 inComplete
。
找到最易读的毛病范例
在一最先有提到,我们只需找到末了一个婚配胜利的节点,就可以够顺藤摸瓜找到毛病缘由以及提醒,但末了一个胜利的节点能够和我们人类直觉相违犯。举下面这个例子:
select a from b where a = '1' ~ -- 这里手滑了
一般状况,我们都认为毛病点在 ~
,而末了一个准确输入是 '1'
。但词法剖析器可不这么想,在我第一版代码里,推断出毛病是如许的:
提醒是 where
错了,而且提醒是 .
,有点摸不着头脑。
读者能够已想到了,这个题目与文法构造有关,我们看 fromClause
的文法形貌:
const fromClause = () =>
chain(
"from",
tableSources,
optional(whereStatement),
optional(groupByStatement),
optional(havingStatement)
)();
虽然现实传入的 where
语句多了一个 ~
标记,但由于文法认为全部 whereStatement
是可选的,因而失足后会跳出,跳到 b
的位置继承婚配,而 明显 groupByStatement
与 havingStatement
都不能婚配到 where
,因而编译器认为 “不会从 b where a = '1' ~
” 最先就有题目吧?因而继承往回追溯,从 tableName
最先婚配:
const tableName = () =>
chain([matchWord, chain(matchWord, ".", matchWord)()])();
此时第一次走的 b where a = '1' ~
线路对应 matchWord
,因而尝试第二条线路,所以认为 where
应当换成 .
。
要处理这个题目,首先要 认可这个推断是对的,由于这是一种 毛病提早的状况,只是人类明白时每每只能看到末了几步,所以我们默许用户想要的毛病信息,是 准确婚配链路最长的那条,并对 onMatchNode
作出下面优化:
将 lastMatch
对象改成 lastMatchUnderShortestRestToken
:
if (
!lastMatchUnderShortestRestToken ||
(lastMatchUnderShortestRestToken &&
lastMatchUnderShortestRestToken.restTokenCount > restTokenCount)
) {
lastMatchUnderShortestRestToken = {
matchNode,
token: matchResult.token,
restTokenCount
};
}
也就是每次婚配到准确字符,都猎取盈余 Token 数目,只保存末了一婚配准确 且盈余 Token 起码的谁人。
3 总结
做语法剖析器毛病提醒功用时,再次革新了笔者三观,本来我们认为的必定,在编译器里对应着那末多 “能够”。
当我们碰到一个毛病 SQL 时,毛病缘由每每不止一个,你能够随意截取一段,说是从这一步最先就错了。语法剖析器为了让报错相符人们的第一直觉,对毛病信息做了 过滤,只保存盈余 Token 数最短的那条毛病信息。
4 更多议论
假如你想介入议论,请点击这里,每周都有新的主题,周末或周一宣布。