用纯功能语言安全清除死代码

2021-01-29 02:25:54

感谢elm-review,我能够在225个不同的文件中删除大约7300行代码,如果没有它的帮助,我是无法完成的。谢谢耶隆!

刚刚清理了很多Elm代码。我想删除对没有价值的模块的使用。手动删除主要使用位置,并遵循编译器错误。大约200行.elm-review处理了当时死代码的其他2700行。

从规模上看,这是一个大约170k行代码的项目。好吧,在删除代码之前。如果我们按我的同事的所作所为,他基本上可以删除〜5%的代码库,同时触摸所有文件的〜30%。

我想介绍一下Elm静态分析工具elm-review如何以我们非常自信的方式帮助我们轻松地删除所有这些代码。剧透:这是因为该语言没有副作用。

我们将比较无副作用的静态语言(Elm)的静态分析工具与动态有效的语言(JavaScript)可以删除的代码。

我将以一些JavaScript代码为例,因为那是我在静态分析方面投入大量精力的另一种语言。

在此示例中您是否注意到userInfo已声明但从未使用过?静态分析工具(如果需要的话,可以使用linter)可以很容易地发现它,这是它们提供的非常普遍的功能。

现在,我们应该如何处理这个未使用的变量?如果我们想清理上面的摘录,我们唯一可以安全地做的就是删除formatUserInfo(user)对userInfo的分配,如下所示。

我们无法删除对formatUserInfo(user)的调用,因为我们不知道它的作用。也许它会改变用户参数或全局变量,发出HTTP请求等。在这个示例中,如果formatUserInfo通过添加其他用户字段中的信息来改变user.name,我不会感到惊讶。

这些被称为副作用:函数在返回值之外或不返回值时所做的事情。当一个功能有副作用时,我们说它是“不纯的”,如果没有它,我们就说它是“纯的”。

带有副作用的棘手事情是,周围的代码可能依赖于所应用的那些副作用,但是代码中没有任何东西(不会变得过时)表明了这种依赖性。

如果函数不纯,则删除对其的调用可能会更改代码的行为。这意味着在不知道它是纯净还是不纯净的情况下,我们无法安全地将其删除。对于本文,如果不会改变代码的行为或引入编译器错误,我将考虑删除“安全”的代码。

一个智能的静态分析工具可以尝试检查formatUserInfo来查看是否有副作用,但这可能会非常麻烦。在某些情况下,由于静态分析工具对动态语言的固有局限性,这甚至是完全不可能的。

语言获得的动态性越强,其产生的副作用越多,静态分析工具进行准确分析并减少误报和误报的难度就越大。

以下是不完整的JavaScript功能列表,这些功能使您很难预测代码将执行的操作:

通过getter和setter,即使是简单的表达式如a.b也会产生副作用。

通过更改特定或核心类型的原型,甚至被认为是纯粹的标准化功能也可能变得不纯。这实际上是一个安全问题,并且是您遇到许多Dependabot问题以破坏依赖关系的原因之一。

通过宏(例如,使用Webpack或Babel),可以以未知方式转换代码。在某些情况下,该工具将无法解析未转换的代码。

通过动态导入(要求或动态HTML脚本标签),可以导入和执行该工具甚至无法访问的任意代码,从而有可能应用上述功能之一。

在Elm中,您只有纯功能。这意味着在调用函数与不使用结果以及根本不调用结果之间没有明显的区别。

formatUserName user =让userInfo = user中的formatUserInfo用户。名称 。第一++" " ++ Formatting.formatLastName用户。名称 。持续

在这里,我们可以安全地在不更改程序行为的情况下,报告可以删除userInfo的整个声明,包括对formatUserInfo的调用,并建议自动修复它。

为什么未使用该值?它可能在某个时候失去了目的,但尚未清除,或者可能是一个臭虫,因为它需要在某个地方使用,但是编写/更改了代码的开发人员却忘记了。如果不直接与开发人员交谈,我们将无法知道我们处于哪种情况以及应该使用还是删除该值。

因此,当elm-review分析代码并且已使用--fix运行该代码时,它将在应用建议的删除变量的修补程序之前要求用户进行确认。每个elm-review修复建议都必须经过用户的批准,然后才能提交到文件系统。

不过,有一种方法可以批量处理它们,以避免过程过于繁琐,我发现人们在工具获得信任之后就开始使用它。

在本文的其余部分,我将引用我们之前在步骤1中所做的更改。

在JavaScript领域,我们不得不保留对formatUserInfo的调用,但是在Elm领域,我们能够将其删除。这使我们可以做另一件事:检查formatUserInfo是否在其他任何地方使用过。

模块SomeModule公开(formatUserName)导入格式化formatUserName user = user。名称 。第一++" " ++ Formatting.formatLastName用户。名称 。最后一个formatUserInfo用户= {角色= Formatting.formatRole用户。角色,描述= String.trim用户。说明}

当我们查看此模块时,似乎从来没有以任何方式使用formatUserInfo:它没有公开给其他模块,也没有用在任何其他函数中。因此我们也可以安全地删除它!

模块格式暴露(formatLastName,formatRole)导入变音符号导入ReCase类型大写= AllCaps | SnakeCase formatLastName lastName = applyFormatting AllCaps lastName formatRole角色= applyFormatting SnakeCase角色applyFormatting格式化字符串= AllCaps的大小写格式。 String.toUpper字符串SnakeCase-> ReCase.fromSnake(ReCase.toSnake(Diacritics.removeAccents字符串))

elm-review规则可以在报告错误之前查看项目的所有模块。这使它比仅一次查看单个模块的静态分析工具强大得多(就像elm-review最初所做的那样,我可以告诉您,作为规则编写者,这非常令人沮丧)。这种方法使我们能够基于在其他模块中的使用情况来报告有关该模块的信息。

在这种情况下,另一个名为NoUnused.Exports的规则(以前我们使用NoUnused.Variables规则)将报告formatRole作为模块API的一部分公开,但从未在其他模块中使用,SomeModule是整个代码库中的唯一模块被引用的地方。由于此模块之外的项目在任何地方都没有使用它,因此我们可以安全地停止从模块中公开它。

请注意,如果这是您真正想要保留的某种实用程序模块,则可以为该文件禁用此特定规则。此外,此规则也不会报告作为Elm包的公共API公开的功能,对此无后顾之忧。

现在看来,formatRole也未在Formatting模块内部内部使用,因此我们可以像在步骤2中对formatUserInfo所做的那样完全删除它。

formatRole使用的是大写的SnakeCase变体,这是该变体创建的唯一位置。如果从未创建该变体,则无需处理它。

对于不熟悉自定义类型(又称“联合类型”,又称“代数数据类型”)但不熟悉JavaScript的用户,它们使您可以列出某些内容可以具有的所有离散值。当用于case表达式(大致相当于JavaScript中的开关)时,编译器可以确定您是否已处理所有可能的值,并在未提示时提醒您,从而无需添加默认大小写。

函数applyFormatting(formatting,string){switch(formatting){case" AllCaps" :// ... case" SnakeCase" :// ...默认值:// Elm不需要,因为所有情况都可以处理}

这是第一种不提供自动修复的情况,因为我们将需要同时删除自定义类型定义和不同模式中的变体,可能会删除多个文件。在这种情况下,让用户自己删除定义并让Elm编译器帮助他们修复所有导致编译器错误的方法比较安全。

一个工具可能可以做到,但是至少到目前为止,elm-review还不能。用户可能会这样删除未使用的变体。

...类型大写= AllCaps-| SnakeCase ... applyFormatting格式化字符串= AllCaps的大小写格式-> String.toUpper字符串-SnakeCase-> ReCase.fromSnake(ReCase.toSnake(Diacritics.removeAccents字符串))

在最后一步中,我们从Diacritics模块中删除了对removeAccents函数的调用,这是该导入在模块中的最后用法。

在JavaScript中,导入模块可能会导致副作用。这意味着为了不改变程序的行为,我们只能从导入声明中删除赋值部分。

在Elm中,导入模块没有副作用。这意味着我们可以安全地删除整个导入。

同样,ReCase的导入变得多余,因此我们可以用相同的方式删除它(为简便起见,请计入第6.5步)。

NoUnused.Modules告诉我们,我们在项目中创建的Diacritics模块实际上从未导入任何地方。好吧,如果我们从不导入模块,则可以删除整个文件!

删除该文件后,我们可以查看在导入的Diacritics模块中定义的所有功能和类型。

类似地,但略有不同,NoUnused.Dependencies报告我们在elm.json中定义的s6o / elm-recase依赖关系尚未使用。

由于我们删除了该依赖关系中包含的ReCase模块的唯一导入,并且在代码库中没有从该包中导入其他模块,因此我们可以安全地从项目中删除该依赖关系。

模块SomeModule公开(formatUserName)导入FormattingformatUserName user =-let-userInfo = formatUserInfo用户-在user.name中。first ++" " ++ Formatting.formatLastName user.name.last-formatUserInfo用户=-{角色= Formatting.formatRole user.role-,描述= String.trim user.description-}

-模块格式公开(formatLastName,formatRole)+模块格式公开(formatLastName)-导入变音符号-导入ReCasetype大写= AllCaps-| SnakeCaseformatLastName lastName = applyFormatting AllCaps lastName-formatRole角色=-applyFormatting SnakeCase roleapplyFormatting格式化字符串= AllCaps的大小写格式-> String.toUpper字符串-SnakeCase-> ReCase.fromSnake(ReCase.toSnake(Diacritics.removeAccents字符串))

这些模块变得更短了。此外,我们删除了整个Diacritics模块和一个依赖项。

搜索未使用的代码类似于在有向图中搜索,一个图中每个函数(或值/常数)是一个节点,另一个图中每个模块是节点,等等。

副作用是一种代码隐式依赖于另一代码的方式,在我们的图形中添加了模糊且不可预测的箭头,这使分析变得不可靠。换句话说,纯函数式语言(没有副作用)仅使代码之间的依赖关系变得明确。

在我们执行的每个步骤中,我们都删除了代码段之间的依赖关系。在删除了足够多的子图之后,我们最终得到了一个未连接到主程序的子图,或者说主程序不依赖于该子图,因此可以安全地删除它。

对于有效的语言,即使静态分析工具报告或删除了我们显示的一些示例代码,我们也必须仔细检查其工作,以确保它没有进行任何不安全的更改。由于其固有的动态特性,这些自动修复程序通常是启发式的,而不是保证安全的更改。

静态,无副作用的语言范例为我们提供了可以安全执行的更大可能性。

最近,我一直在做很多工作,以在Elm中找到越来越多的无效代码,例如,通过发现无法到达的代码路径中未使用的函数。

对于您精于删除可能有用的代码以及让我追逐死代码如此重要,这是否对您有价值?我想说不是,但是正如我们在此示例中看到的那样,这仅仅是因为我们如此腐,以至于我们甚至能够开始这一过程。

如果我们在示例中保留了let变量,我们将无法继续continue在图表上,最后最终删除了整个代码部分。

即使可以安全删除代码,您也可以保留一些代码,以防万一以后有用。如前所述,您可以在代码库的某些部分上忽略这些规则,但是除了(可能)隔离的实用程序功能之外,我建议您不要这样做。

我已经开始拥抱YAGNI(您将不需要它)并删除我可以安全删除的所有内容,因为我知道我有可以依靠的Git历史记录,并且知道我可以保留的代码甚至可能不适合我正在努力保持这种情况。

直到实际使用该代码的时间到来为止,保持该代码的可读性为负值,因为无效代码可能会误导和分散您的注意力。它在维护方面也将具有负值,因为即使它毫无用处,也需要维护。

相反,报告未使用的代码具有积极的作用,可以警告您明显的错误,即您没有使用最近定义的和应该使用的错误。告诉我elm-review在他们的代码中发现这种错误的同事已经使我想起了几次。

我不会花很多时间尝试手动查找未使用的代码,因为这项任务非常耗时。 但是由于我们有工具可以在非常合理的时间内自动执行此操作,因此我发现成本非常低,价值也很高。 玛丽·近藤(Marie Kondo)说,如果某些事情不能带给您欢乐,那么就应该把它扔掉。 就我而言,在重构一些代码之后,我运行elm-review并希望它将开始寻找可以扔掉的东西。 正如Martin Janiczek所说: 令人高兴的是,elm-review让您现在可以删除一些无效的代码!