答案 0 :(得分:202)
我可以为您描述我们如何在“真正的”C#IDE中有效地完成这项工作。
我们要做的第一件事就是运行一个只分析源代码中“顶级”内容的传递。我们跳过所有方法体。这使我们能够快速建立一个信息数据库,该数据库包含程序源代码中的命名空间,类型和方法(以及构造函数等)。如果你想在按键之间进行分析,那么分析每个方法体中的每一行代码都会花费太长时间。
当IDE需要计算出方法体内特定表达式的类型时 - 比如说你输入了“foo”。我们需要弄清楚foo的成员是什么 - 我们做同样的事情;我们可以合理地跳过尽可能多的工作。
我们从一个传递开始,该传递仅分析该方法中的局部变量声明。当我们运行该传递时,我们从一对“范围”和“名称”到“类型确定器”进行映射。 “类型确定器”是表示“如果需要,我可以计算出本地类型”这一概念的对象。计算出本地的类型可能很昂贵,所以如果需要,我们希望推迟这项工作。
我们现在有一个懒惰的数据库,可以告诉我们每个本地的类型。所以,回到那个“foo”。 - 我们找出相关表达式所在的语句,然后针对该语句运行语义分析器。例如,假设您有方法体:
String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.
现在我们需要弄清楚foo是char类型。我们构建了一个包含所有元数据,扩展方法,源代码类型等的数据库。我们构建了一个具有x,y和z类型确定器的数据库。我们分析包含有趣表达式的语句。我们首先将其语法转换为
var z = y.Where(foo=>foo.
为了计算foo的类型,我们必须首先知道y的类型。所以在这一点上我们问类型确定器“y的类型是什么”?然后它启动一个表达式求值程序,它解析x.ToCharArray()并询问“x的类型是什么”?我们有一个类型确定器,它说“我需要在当前上下文中查找”字符串“。当前类型中没有类型String,因此我们查看命名空间。它不存在,所以我们查看using指令并发现有一个“使用系统”,并且该系统有一个String类型。好的,这就是x的类型。
然后我们查询System.String的ToCharArray类型的元数据,并说它是一个System.Char []。超。所以我们有y的类型。
现在我们问“System.Char []有一个方法在哪里?”不,所以我们查看使用指令;我们已经预先计算了一个数据库,其中包含可能使用的扩展方法的所有元数据。
现在我们说“好了,有十八个扩展方法名为Where in scope,是否有任何第一个形式参数,其类型与System.Char []兼容?”所以我们开始进行一轮可兑换测试。但是,Where扩展方法是泛型,这意味着我们必须进行类型推断。
我编写了一个特殊类型的推理引擎,可以处理从第一个参数到扩展方法的不完整推理。我们运行类型推导器并发现有一个Where方法需要IEnumerable<T>
,并且我们可以从System.Char []到IEnumerable<System.Char>
进行推断,因此T是System.Char。 / p>
此方法的签名是Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
,我们知道T是System.Char。我们也知道扩展方法括号内的第一个参数是lambda。所以我们启动一个lambda表达式类型推导器,它说“形式参数foo被假定为System.Char”,在分析lambda的其余部分时使用这个事实。
我们现在拥有分析lambda主体所需的所有信息,即“foo”。我们查找foo的类型,我们发现根据lambda绑定器它是System.Char,我们已经完成了;我们显示System.Char的类型信息。
除了按键之间的“顶级”分析,我们会做所有事情。这真是棘手的一点。实际上写所有分析并不难;它正在使它足够快你可以在打字速度上做到这一点真的很棘手。
祝你好运!答案 1 :(得分:15)
我可以大致告诉你Delphi IDE如何与Delphi编译器一起工作以进行智能感知(代码洞察是Delphi所称的)。它不是100%适用于C#,但这是一个值得考虑的有趣方法。
Delphi中的大多数语义分析都是在解析器本身中完成的。表达式在解析时会被输入,除非这种情况不容易 - 在这种情况下,使用预读解析来计算出预期的内容,然后在解析中使用该决策。
除了使用运算符优先级解析的表达式之外,解析主要是LL(2)递归下降。 Delphi的一个独特之处在于它是单通道语言,因此构造需要在使用之前声明,因此不需要顶层传递来传递信息。
这些功能组合意味着解析器具有代码洞察所需的大致所有信息,可用于需要它的任何点。它的工作方式是:IDE通知编译器的光标位置(需要代码洞察的点),并且词法分析器将其转换为特殊标记(称为kibitz标记)。每当解析器遇到此令牌(可能在任何地方)时,它就知道这是将它返回给编辑器的所有信息发回的信号。它使用longjmp执行此操作,因为它是用C语言编写的;它所做的是它通知终极调用者所发现的kibitz点的句法结构(即语法上下文),以及该点所需的所有符号表。因此,例如,如果上下文位于表达式中,该表达式是方法的参数,我们可以检查方法重载,查看参数类型,并将有效符号过滤为只能解析为该参数类型的符号(这在下拉列表中减少了许多无关紧要的事情。如果它位于嵌套的作用域上下文中(例如在“。”之后),则解析器将回传对作用域的引用,IDE可以枚举该作用域中找到的所有符号。
其他事情也已完成;例如,如果kibitz令牌不在其范围内,则跳过方法体 - 这是乐观地完成的,并且如果它跳过令牌则回滚。相当于扩展方法 - Delphi中的类助手 - 有一种版本化缓存,因此它们的查找速度相当快。但是Delphi的泛型类型推断远比C#弱。
现在,针对具体问题:推断用var
声明的变量类型等同于Pascal推断常量类型的方式。它来自初始化表达式的类型。这些类型是自下而上构建的。如果x
的类型为Integer
,而y
的类型为Double
,则x + y
的类型为Double
,因为这些是语言规则;您可以遵循这些规则,直到右侧有完整表达式的类型,这就是您在左侧用于符号的类型。
答案 2 :(得分:7)
如果您不想编写自己的解析器来构建抽象语法树,可以考虑使用SharpDevelop或MonoDevelop中的解析器,这两个解析器都是开源的
答案 3 :(得分:4)
Intellisense系统通常使用抽象语法树来表示代码,它允许它们以与编译器相同的方式解析分配给'var'变量的函数的返回类型。如果您使用VS智能感知,您可能会注意到在您输入有效(可解析的)赋值表达式之前它不会为您提供var的类型。如果表达式仍然不明确(例如,它无法完全推断表达式的泛型参数),则var类型将无法解析。这可能是一个相当复杂的过程,因为您可能需要深入到树中才能解析类型。例如:
var items = myList.OfType<Foo>().Select(foo => foo.Bar);
返回类型为IEnumerable<Bar>
,但需要解决此问题:
IEnumerable
的类型。OfType<T>
。IEnumerable<Foo>
,并且有适用于此的扩展方法Select
。foo => foo.Bar
具有Foo类型的参数foo。这是由Select的使用推断的,它取Func<TIn,TOut>
并且由于TIn已知(Foo),因此可以推断出foo的类型。IEnumerable<TOut>
,并且可以从lambda表达式的结果推断出TOut,因此结果类型的项必须是IEnumerable<Bar>
。答案 4 :(得分:4)
由于您的目标是Emacs,因此最好从CEDET套件开始。 Eric Lippert在CEDET / Semantic for C ++中的代码分析器中已经涵盖了所有细节。还有一个C#解析器(可能需要一点TLC),因此缺少的唯一部分与调整C#的必要部分有关。
基本行为在核心算法中定义,核心算法依赖于基于每种语言定义的可重载函数。完成引擎的成功取决于已完成多少调整。以c ++为指导,获得类似于C ++的支持应该不会太糟糕。
Daniel的回答建议使用MonoDevelop进行解析和分析。这可能是一种替代机制,而不是现有的C#解析器,或者它可以用于扩充现有的解析器。
答案 5 :(得分:2)
做得好是一个难题。基本上,您需要通过大多数lexing / parsing / typechecking对语言规范/编译器进行建模,并构建源代码的内部模型,然后您可以查询该模型。 Eric为C#详细描述了它。您始终可以下载F#编译器源代码(F#CTP的一部分)并查看service.fsi
以查看F#语言服务所消耗的F#编译器所提供的接口,以提供智能感知,推断类型的工具提示如果您已将编译器作为API调用,则可以了解可能的“接口”。
另一种方法是按原样重复使用编译器,然后使用反射或查看生成的代码。从您需要“完整程序”来获取编译器的编译输出的角度来看这是有问题的,而在编辑器中编辑源代码时,您通常只有尚未解析的“部分程序”,不要已经实施了所有方法,等等。
简而言之,我认为'低预算'版本很难做得很好,'真实'版本非常非常难以做好。 (这里'硬'在这里衡量'努力'和'技术难度'。)
答案 6 :(得分:2)
NRefactory会为你做这件事。
答案 7 :(得分:0)
对于解决方案“1”,您在.NET 4中拥有一个新工具,可以快速轻松地完成此任务。 因此,如果您可以将程序转换为.NET 4,那将是您的最佳选择。