当前位置: 首页 > 科技观察

.Groovy类型检查扩展,编写类型检查扩展

时间:2023-03-20 10:57:34 科技观察

1。介绍这篇Groovy学习笔记的第37部分。开始介绍Groovy中扩展类型检查的相关知识。了解如何定义我们的类型检查器。前面分享的类型知识更多的是通过Groovy中的静态类型检查器实现的。而这篇文章的开头就是定义我们自己的类型检查。也称为类型检查扩展,定义您自己的类型检查器。类型检查扩展是允许DSL引擎的开发人员将静态类型检查允许的相同类型的检查应用于常规groovy类的机制,从而使这些脚本更加安全。PS:一般来说,关于类型检测扩展的知识可能更适合使用Groovy进行插件开发的工程师。用于检测定义的DSL脚本是否合规等。2.编写类型检查扩展下面介绍如何编写我们的类型检查。2.1智能类型检查器Groovy可以在编译时与静态类型检查器一起使用,使用@TypeChecked注释启用。在这种模式下,编译器变得更加冗长,并抛出拼写错误、不存在的方法等错误。然而,这也带来了一些限制,其中大部分来自于Groovy本质上仍然是一种动态语言。例如,您不能对使用标记构建器的代码进行类型检查:defbuilder=newMarkupBuilder(out)builder.html{head{//...}body{p'Hello,world!'}}在上面的例子中,html、head、body或p方法都不存在。但是,如果代码被执行,它会起作用,因为Groovy使用动态分派并在运行时转换这些方法调用。在这个构建器中,我们可以使用的标签和属性的数量没有限制,这意味着类型检查器没有机会在编译时知道所有可能的方法(标签),除非我们创建一个专用于HTML的构建器。Groovy是实现内部DSL的首选平台。灵活的语法,结合运行时和编译时元编程功能,使Groovy成为一个有趣的选择,因为它允许程序员专注于DSL,而不是工具或实现。由于GroovyDSL是Groovy代码,因此无需编写专门的插件即可轻松获得IDE工具的支持。在许多情况下,DSL引擎是用Groovy(或Java)编写的,然后用户代码作为脚本执行,这意味着在用户逻辑之上有某种包装器。例如,包装器可能被包装在GroovyShell或GroovyScriptEngine中,它们在运行脚本之前透明地执行一些任务(添加导入、应用AST转换、扩展基本脚本等)。通常,用户编写的脚本无需测试即可投入生产,因为DSL逻辑达到任何用户都可以使用DSL语法编写代码的程度。最后,用户可能会忘记他们写的实际上是代码。这给DSL实现者增加了一些挑战,例如确保用户代码的执行,或者在这种情况下,及早报告错误。例如,想象一个DSL:它的目标是在火星上远程驾驶漫游车。向探测器发送信息大约需要15分钟。如果流动站因错误(例如打字错误)而未能执行脚本,您将遇到两个问题:首先,反馈仅在30分钟后(探测器获取脚本所需的时间和返回脚本所需的时间)收到错误),其次,脚本的某些部分已经实现,您可能必须对固定脚本进行重大更改(这意味着您需要知道流动站的当前状态......)类型检查扩展是一个一种允许DSL引擎开发人员对常规groovy类应用静态类型检查所允许的相同类型检查的机制,从而使这些脚本更加安全。这里的原则是尽早失败,即尽早编译脚本失败,尽可能向用户提供反馈(包括漂亮的错误信息)。简而言之,类型检查扩展背后的想法是让编译器了解DSL使用的所有运行时元编程技巧,以便脚本可以获得与冗长的静态编译代码相同级别的编译时检查。正如我们将看到的,您可以执行普通类型检查器无法执行的检查,从而为用户提供强大的编译时检查。2.2extensions属性@TypeChecked注解支持一个名为extensions的属性。此参数接受与类型检查扩展脚本列表相对应的字符串数组。这些脚本在编译时在类路径中找到。例如:@TypeChecked(extensions='/path/to/myextension.groovy')voidfoo(){...}在这种情况下,方法foo将使用普通类型检查器的规则进行类型检查,这groovy脚本由myextension中的规则完成。PS:请注意,虽然类型检查器在内部支持多种机制来实现类型检查扩展(包括普通的旧java代码),但推荐的方法是使用这些类型检查扩展脚本。2.3用于类型检查的DSL类型检查扩展背后的想法是使用DSL来扩展类型检查器的功能。这个DSL允许我们使用“事件驱动”API挂钩到编译过程,更具体地说是类型检查阶段。例如,当类型检查器进入一个方法体时,它会抛出一个beforeVisitMethod事件,扩展可以对此事件做出反应:beforeVisitMethod{methodNode->println"Entering${methodNode.name}"}假设我们手边有这个RoverDSL。用户可以这样写:robot.move100如果你有一个这样定义的类:classRobot{Robotmove(intqt){this}}脚本可以在执行前进行类型检查,使用:defconfig=newCompilerConfiguration()config.addCompilationCustomizers(newASTTransformationCustomizer(TypeChecked)//编译器配置为所有类添加@TypeChecked注解)defshell=newGroovyShell(config)//在GroovyShell中使用配置defrobot=newRobot()shell.setVariable('robot',robot)shell.evaluate(script)//这样,使用shell编译的脚本将使用@typecheck进行编译,而无需用户显式添加它使用上面的编译器配置,我们可以在脚本中透明地应用@typecheck。在这种情况下,它将在编译时失败并显示以下错误日志:[静态类型检查]-变量[robot]未声明。现在,我们将稍微更新配置以包含扩展参数:config.addCompilationCustomizers(newASTTransformationCustomizer(TypeChecked,extensions:['robotextension.groovy']))然后将以下内容添加到类路径中:首先:创建一个robotextension.groovy文件,然后在文件中添加以下代码:unresolvedVariable{var->if('robot'==var.name){storeType(var,classNodeFor(Robot))handled=true}}这里我们告诉编译器,如果我们找到一个名为robot的未解析变量,那么我们可以确保该变量是robot类型。2.4类型检查扩展的相关APIAST:类型检查API是处理抽象语法树的低级API。要开发扩展,您必须对AST有很好的理解,尽管DSL比处理纯Java或Groovy的AST代码要容易得多。事件:类型检查器发送以下事件,扩展脚本可以对其作出反应。具体事件示例如下表所示:事件名称(Eventname)调用时(CalledWhen)参数(Arguments)用法(Usage)备注setup类型检查器完成初始化后调用without(none)setup{//被调用在任何其他操作之前}可用于执行我们的扩展的设置finish在类型检查器完成类型检查而无需(无)finish之后调用{//这是在完成时完成的在所有类型检查之后}可用于在之后执行其他检查类型检查器已经完成了它的工作。unresolvedVariable当类型检查器发现未解析的变量时调用VariableExpressionvexpunresolvedVariable{VariableExpressionvexp->if(vexp.name=='people'){storeType(vexp,LIST_TYPE)handled=true}}允许开发人员帮助类型检查器使用用户注入的变量。unresolvedProperty当类型检查器在接收器上找不到属性时调用PropertyExpressionpexpunresolvedProperty{PropertyExpressionpexp->if(pexp.propertyAsString=='longueur'&&getType(pexp.objectExpression)==STRING_TYPE){storeType(pexp,int_TYPE)handled=true}}允许开发者处理“动态”属性->if(getType(aexp.objectExpression)==STRING_TYPE){storeType(aexp,STRING_TYPE)handled=true}}允许开发者处理缺失的属性beforeMethodCall在类型检查器MethodCall调用的开始beforeMethodCall{call->if(isMethodCallExpression(call)&&call.methodAsString=='toUpperCase'){addStaticTypeError('Notallowed',call)handled=true}}允许您在类型检查器执行自己的检查之前拦截方法调用.如果您想在有限范围内用自定义类型检查替换默认类型检查,这将很有用。在这种情况下,processed标志必须设置为true,以便类型检查器跳过它自己的检查。afterMethodCallCallMethodCall在类型检查器完成方法调用的类型检查后调用afterMethodCall{call->if(getTargetMethod(call).name=='toUpperCase'){addStaticTypeError('Notallowed',call)handled=true}}允许您在类型检查器完成自己的检查后执行额外的检查。如果您希望执行标准类型检查测试,但也希望确保额外的类型安全,例如检查参数之间的差异,这将特别有用。请注意,即使您在beforemethodcall之前将handled标志设置为true,afterMethodCall也会被调用。onMethodSelection当类型检查器找到适合方法调用的方法时,由它调用Expressionexpr,MethodNodenodeonMethodSelection{expr,node->if(node.declaringClass.name=='java.lang.String'){if(++count>2){addStaticTypeError("Youcanuseonly2callsonStringinyoursourcecode",expr)}}}类型检查器推断方法调用的参数类型,然后选择要处理的目标方法。如果找到对应的,则触发此事件。这很有趣,例如,如果您想对特定方法调用做出反应,例如进入将闭包作为参数的方法的范围(如在构建器中)。请注意,可能会为各种类型的表达式抛出此事件,而不仅仅是方法调用(例如二进制表达式)。methodNotFound当类型检查器找不到适合方法调用的方法时调用ClassNodereceiver,Stringname,ArgumentListExpressionargList,ClassNode[]argTypes,MethodCallcallmethodNotFound{receiver,name,argList,argTypes,call->if(receiver==classNodeFor(String)&&name=='longueur'&&argList.size()==0){handled=truereturnnewMethod('longueur',classNodeFor(String))}}与onMethodSelection不同的是,当类型检查器找不到方法调用的目标方法(实例或静态)时,将发送此事件。它使您有机会在将错误发送给用户之前拦截错误,而且还允许您设置目标方法。为此,您需要返回一个MethodNodes列表。大多数情况下,你会返回:一个空列表,表示你没有找到对应的方法,一个只有一个元素的列表,说明目标方法是无可置疑的,如果你返回多个MethodNode,那么编译器会抛出一个向用户指出方法调用不明确的错误,列出了可能的方法。为了方便,如果只想返回一个方法,可以直接返回,而不是包装成一个列表。beforeVisitMethod在类型检查方法bodyMethodNodenodebeforeVisitMethod{methodNode->handled=methodNode.name.startsWith('skip')}类型检查器会启动这个方法在对方法体进行类型检查之前调用。例如,如果您希望自己执行类型检查,而不是让类型检查器来做,您必须将processed标志设置为true。此事件还可用于帮助定义扩展的范围(例如,仅在方法foo中应用它)。afterVisitMethod在类型检查方法主体MethodNodenodeafterVisitMethod{methodNode->scopeExit{if(methods>2){addStaticTypeError("Method${methodNode.name}包含超过2个方法调用",methodNode)}}}让您有机会在类型检查器访问方法体后执行额外的检查。这很有用,例如,如果您收集信息并希望在收集完所有信息后执行额外检查。beforeVisitClass在类型检查类ClassNode节点beforeVisitClass{ClassNodeclassNode->defname=classNode.nameWithoutPackageif(!(name[0]in'A'..'Z')){addStaticTypeError("Class'${name}'doesn'tstartwithanuppercaseletter",classNode)}}如果对类进行了类型检查,将在访问该类之前发送此事件。对于用@typecheck注释的类中定义的内部类也是如此。它可以帮助您定义扩展的范围,或者您甚至可以用自定义类型检查实现完全替换类型检查器的访问。为此,您必须将已处理标志设置为true。afterVisitClass由类型检查器在完成访问类型检查类ClassNode节点afterVisitClass{ClassNodeclassNode->defname=classNode.nameWithoutPackageif(!(name[0]in'A'..'Z')){addStaticTypeError("Class'${name}'doesn'tstartwithanuppercaseletter",classNode)}}在类型检查器为每个类型完成检查类型后调用.这包括用@typecheck注释的类,并且不会跳过在同一类中定义的任何内部/匿名类。incompatibleAssignment当类型检查器认为赋值不正确时调用,即赋值的右侧与左侧不兼容ClassNodelhsType,ClassNoderhsType,ExpressionassignmentincompatibleAssignment{lhsType,rhsType,expr->if(isBinaryExpression(expr)&&isAssignment(expr.operation.type)){if(lhsType==classNodeFor(int)&&rhsType==classNodeFor(Closure)){handled=true}}}使开发者能够处理不正确的分配。这很有用,例如,如果一个类覆盖了setProperty,因为在这种情况下,将一种类型的变量分配给另一种类型的属性可能由运行时机制处理。在这种情况下,您可以通过告诉类型检查器赋值有效(使用设置为true的句柄)来帮助类型检查器。incompatibleReturnType当类型检查器认为返回值与封闭闭包或方法的返回类型不兼容时调用ReturnStatement语句,ClassNodevalueTypeincompatibleReturnType{stmt,type->if(type==STRING_TYPE){handled=true}}使开发者能够处理不正确的返回值。这很有用,例如,当返回值将进行隐式转换时,或者当封闭闭包的目标类型难以正确推断时。在这种情况下,您可以通过告诉类型检查器赋值有效(通过设置Handler属性)来帮助类型检查器。ambiguousMethods当类型检查器无法在多个候选方法中进行选择时调用Listmethods,ExpressionoriginambiguousMethods{methods,origin->methods.find{it.parameters.any{it.type==classNodeFor(Integer)}}}使开发人员能够处理不正确的分配。这很有用,例如,如果一个类覆盖了setProperty,因为在这种情况下,将一种类型的变量分配给另一种类型的属性可能由运行时机制处理。在这种情况下,您可以通过告诉类型检查器赋值有效(使用设置为true的句柄)来帮助类型检查器。(PS:如果上表不清楚,可以访问我的博客网站:zinyan.com/?p=486)当然,扩展脚本可能由几个block组成,可以用多个block来响应同一个事件。这使得DSL看起来更好,更容易编写。然而,仅仅对事件做出反应是不够的。如果我们知道我们可以对事件作出反应,我们还需要处理错误,这意味着有几个辅助方法可以使事情变得更容易。