Syntax Highlighting Sucks


but static analysis rocks

不得不承认,即使是在最现代的 IDE 工具集(JetBrains 系列)中,语法高亮的配置如此之多,依旧无法解决代码编写与阅读中的核心问题——语义理解。

当然,JetBrains IDEs 在其他方面依旧做的足够出色。

当我们在谈论语法高亮时,我们在谈论什么

实际上,无论是语法高亮,还是 linter、静态分析等更高级的程序分析工具,最终的目标都是辅助程序员阅读、编写代码。

然而,(我的)实践表明,即使是精心配置过的语法高亮,在辅助代码阅读上的功能,甚至完全不如更加简单的类型标注来得有效(当然我还是不能理解为什么 JetBrains 的 IDE 明明会自动插入类型 Label,却不能点击这个 Label 来跳转到类型定义)。

在初学编程时,我也特别喜欢配置繁复的语法高亮规则,让代码看起来五颜六色,似乎这能帮助我阅读代码。

但以我现在的眼光来看,这是完完全全错误的做法。五颜六色的代码只会加重大脑的负担。

总体上来看,语法高亮真正有效的作用有:

  • 辅助人们忽略一切不关键的部分。也许只有这部分,可以称作是 syntax highlight
  • 辅助显示静态分析的结果。这部分更多的并不是 syntax,而是 static analysis
    • 突出显示一些特定的语法元素。例如,使用一种特别的颜色或字体来显示全局变量。

忽略不关键的部分

什么是不关键的部分?实际上,几乎所有的“关键字”都是不关键的。绝大部分时候,没有人在乎一个循环到底是 for 还是 while,没有人在乎这个 switch 到底有几个 case,没有人在乎一个函数究竟有几个修饰符。

我们关心的是 for/while 循环的是谁,是 switch/case 的条件,是函数的实现。在这一点上,语法高亮确实很好地完成了它的工作——用统一的颜色标注出关键字,好告诉程序员们这部分完全可以忽略。

无意义的高亮

但语法高亮有时候加重了代码阅读的负担。

到底是谁发明的“函数”需要有颜色?是的,我确实有时候需要区分一个函数到底是 accessor, extension method, static method, overloaded method, operator,等等等等。天杀的,一个“函数调用”怎么会有那么多种类型!

——但这一切仅仅发生在阅读代码的人不熟悉 codebase,且需要更改它的时候。

一个对代码库了如指掌的人,或者是一个根本不想修改代码库,只是想看看代码如何工作的人,根本不会关注一个函数到底是什么类型。

应该关心的是什么?是语义!

在一长串的函数调用中,我们真正关心的是这个函数做了什么,而非是什么。实际上,我根本无法从一堆函数调用的颜色上获取任何信息。一个函数是什么种类,和它的语义有任何关系吗?没有。

一个最最简单的例子,我实在迫切需要一个高亮元素,帮助我知道一个函数是 mutable 还是 immutable 的(哪怕这个语言本身并没有类似的支持),如果是 mutable 的,他 mutate 了的是哪个参数。

更进一步,我还想一眼看过去就能发觉,哪些函数是在准备数据,处理异常,哪些函数才是真正的核心逻辑。我根本不关心各种各样的输入输出是怎么被处理成内部数据结构的。

我还关心不同分支中哪一部分的代码具有相同的特征、完成类似的工作。

以我最近阅读的一段 calico-node 的代码为例(完整代码段):

func autoDetectCIDR(method string, version int) *cnet.IPNet {
	if method == "" || method == AUTODETECTION_METHOD_FIRST {
		return autoDetectCIDRFirstFound(version)
	} else if strings.HasPrefix(method, AUTODETECTION_METHOD_INTERFACE) {
		// ...
		return autoDetectCIDRByInterface(ifRegexes, version)
	} else if strings.HasPrefix(method, AUTODETECTION_METHOD_CIDR) {
		// ...
		return autoDetectCIDRByCIDR(matches, version)
	} else if strings.HasPrefix(method, AUTODETECTION_METHOD_CAN_REACH) {
		// ...
		return autoDetectCIDRByReach(destStr, version)
	} else if strings.HasPrefix(method, AUTODETECTION_METHOD_SKIP_INTERFACE) {
		// ...
		return autoDetectCIDRBySkipInterface(ifRegexes, version)
	}

	// The autodetection method is not recognised and is required.  Exit.
	log.Errorf("Invalid IP autodetection method: %s", method)
	terminate()
	return nil
}

这一段代码算的上是非常经典的分支结构。实际上原本的代码也并不是特别复杂,但我相信大家一定都见过更复杂的类似版本,它们甚至可能在 if 里套 if。实际上大多数时候形如此的代码阅读体验都甚是糟糕。IDE 没有提供任何有关核心代码与数据流向的提示,但幸而我展示的这段代码逻辑并不复杂,每个分支的核心逻辑仅仅只有一个 return 而已。更多时候我们不得不非常仔细地阅读每一个 if,找到我们关心的那一个,然后在脑内仔细模拟代码运行,最终找到我们想要的部分。

此外,我也非常关心函数的返回值在哪里被生成,在哪里被修改了。

还是以 calico-node 的代码为例。

// generateIPv6ULAPrefix return a random generated ULA IPv6 prefix as per RFC 4193.  The pool
// is generated from bytes pulled from a secure random source.
func GenerateIPv6ULAPrefix() (string, error) {
	ulaAddr := []byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
	_, err := cryptorand.Read(ulaAddr[1:6])
	if err != nil {
		return "", err
	}
	ipNet := net.IPNet{
		IP:   net.IP(ulaAddr),
		Mask: net.CIDRMask(48, 128),
	}
	return ipNet.String(), nil
}

这一段代码的功能可以算得上是非常简单了,然而它的长度达到了“令人发指”的 10 行。当然,10 行长的函数不算什么。但是,在不阅读函数注释、不知道什么是 IPv6 ULA 的情况下,想理解这个函数,真正需要关心的代码只有 3 行:一行表明 IP 从何而来(首位 0xfd,六个随机 byte,以及一堆 0),一行表明 Mask 是什么(/48),以及函数的返回值处理(String())。

目前,语法高亮不能在类似的代码阅读中提供任何帮助。它仅仅只是分析语法结构并以此着色。而语法在程序编写中是最不重要的一环,正如语法在文学创作中一样。

语义分析的困难性,与注解

好吧,这才是我想说的。

分析器也许并不可能自动做到我所说的功能,但我们完全可以通过其他扩展语法元素通知分析器这段代码是做什么的——我多希望有一种通用的、面向分析器的语法/协议。

虽然我对在注释,或其类似物中填充“逻辑”深恶痛绝(没错,我说的就是 // @ts-ignore),但不得不承认这是一种最普适性的做法。

我称类似的元素为注解(Annotation),注意此处并不特指 Java 里的那个注解,虽然其语义应当是类似的。

当今似乎并没有语法高亮器和静态分析工具提供普适性的类似功能,最符合我所说的还是 JetBrains 的 IntelliJ IDEA。IDEA 也是我发现的最早实现类似功能的 IDE。

IDEA 利用 Java 的 annotation 实现了类似的功能,其最为有用的部分是 Contract,他〇的,全世界的 IDE 开发者都应该好好学学什么是真正面向用户需求的产品!

按上面链接官方文档所说,最简单的两个例子:

ContractEffect
@Contract("_, null -> null")The method returns null if its second argument is null.
@Contract("_, null -> null; _, !null -> !null")The method returns null if its second argument is null, and not-null otherwise.

当然这个功能是侵入式的,我个人觉得普适性不佳。在代码中平添了依赖。

类似的,JetBrains Rider 也有这样的功能(NuGet JetBrains.Annotations)。不开玩笑的说,IDEA 和 Rider/ReSharper 领先所有其他 IDE 一个世代,JetBrains 简直就是世界的主宰!

当然这些功能都只是添头,并不能达到我所希望的美好愿景。因为这些已有的注解归根究底是辅助静态分析器的,貌似对语法高亮元素没有任何控制,不得不说这十分遗憾。

假如这些注解能够控制着色元素,例如,将某个函数的颜色标注为 dimmed,来将其的颜色变暗,其实就很能协助代码编写者突出查看核心代码了。但这也还不够,因为代码的大部分并不是函数调用,而是可恶的基本语句,以及大量的 if。对于基本语句的处理,我暂时还没有想到非常好的方法来控制。此外,对于外部库的控制,效果也会很差。

结论

总的来说,当今的“语法高亮”,确实依旧停留在“语法”上。白白浪费了字体、颜色等宝贵的信息量。大部分时候,这些高亮元素并不能给予程序员足够的提示。如果有一种方法,能够让程序编写者控制着色器,甚至自动折叠(auto collapse code block)的工作,那么对提升代码阅读速度将有着质的提升,将开发者从海量的错误处理等代码中解放出来。