11

The C# 6 preview for Visual Studio 2013 supported a primary constructors feature that the team has decided will not make it into the final release. Unfortunately, my team implemented over 200 classes using primary constructors.

We're now looking for the most straightforward path to migrate our source. Since this is a one time thing, a magical regex replacement string or hacky parser would work.

Before I spend a lot of time writing such a beast, is there anyone out there that's already done this or knows of a better way?

David Pfeffer
  • 38,869
  • 30
  • 127
  • 202
  • 6
    Can you show us an example so we get a better idea. – John Alexiou Dec 12 '14 at 18:32
  • 21
    Slightly crazy idea: use the version of Roslyn within that preview to perform the code rewriting. Basically, add a reference to the Roslyn DLLs that were installed with the preview, and ask it to build your solution. Then for each compilation, search the syntax tree for primary constructor nodes, and modify the syntax tree accordingly. If you're using the parameters in initializers, it could get a bit hairy, mind you. I would seriously consider just doing it by hand though - tedious, but it may well be quicker in the long run. – Jon Skeet Dec 12 '14 at 18:33
  • 1
    (Or maybe get as far as you can with whatever you can knock up in half an hour, and do the rest by hand.) – Jon Skeet Dec 12 '14 at 18:34
  • 12
    200 doesn't sound like a big number. How many are there in your team? If they started to convert the code when you posted this question, by now they would have completed it. – Sriram Sakthivel Dec 12 '14 at 19:23
  • @JonSkeet I might give that a shot. Thanks! – David Pfeffer Dec 12 '14 at 22:29
  • 19
    Sorry to state the obvious, but you do know that you shouldn't be using prerelease software to write production code, right? – Gigi Dec 13 '14 at 09:33
  • @Gigi The IL generated by the Roslyn compiler for is virtually identical to the C# 5 compiler. If it saves us lots of effort (money) to use the new language features, I don't see the issue. – David Pfeffer Dec 13 '14 at 14:57
  • 7
    @Gigi Such a blanket prescriptive statement doesn't really have a place on StackOverflow in my opinion. What if we had been prerelease? Not to mention, we started using the compiler with eyes wide open and knowing the potential risks. Making a comment about the potential risk would be one thing but you aren't in my shoes or on my team and you can't possibly know the particulars of what caused us to arrive at the decision to use the compiler before release. – David Pfeffer Dec 13 '14 at 14:58
  • 2
    I just tried doing this with a regular expressions and it looks monstrous even not covering most use-cases. Just to share my observations - your problem will require at least three regular expressions: 1 - creating constructor with property initializers code 'to fixup'. 2 - Fixup property initialization in newly created constructor. 3 - Remove broken property initializers. However, I don't think it's a problem where RegExp suits best and agree with Jon - doing it manually or dealing with syntax tree programmatically would be much simpler in this case. – t3z Dec 13 '14 at 23:27
  • Where can I see most easily which features the C# 6 team have decided to ship in the final release, and which they intend to take out? Maybe that changes on a weekly basis? – Jeppe Stig Nielsen Jan 03 '15 at 22:53
  • @JeppeStigNielsen You can find the feature list here: http://roslyn.codeplex.com/wikipage?title=Language%20Feature%20Status&referringTitle=Documentation – David Pfeffer Jan 06 '15 at 15:51
  • 1
    I would add reflection to the mix of possibilities, a bit of code to reflect the classes (using the C#6 preview) that spits out triplettes of of string such as: {class/file name}, {replace match}, {replace target}, for example: "Point.cs","public class Point(int x, int y) \n {","public class Point{ \n public Point(int x, int y) { this.X = x; this.Y = t}" THEN I would make a quick recursive file crawler to open up the File and make the replacements... I was not aware of primary constructors so taking this as an example: http://wesnerm.blogs.com/net_undocumented/2013/12/mads-on-c-60.html – Chris Amelinckx Jan 06 '15 at 17:34
  • @JonSkeet : I'm always weirded out by comments that answer the question. Why didn't you post an answer ? – thomasb Jan 23 '15 at 16:24
  • @cosmo0: When I posted the suggestion, I wasn't convinced it was even slightly sane... will do so now. – Jon Skeet Jan 23 '15 at 16:33
  • For those that make a lot of data types and want primary constructors back, consider [LeMP](http://loyc.net/lemp). – Qwertie Mar 10 '16 at 07:43

2 Answers2

6

As I suggested in comments, you could use the version of Roslyn which does know about primary constructors to parse the code into a syntax tree, then modify that syntax tree to use a "normal" constructor instead. You'd need to put all the initializers that use primary constructor parameters into the new constructor too, mind you.

I suspect that writing that code would take me at least two or three hours, quite possibly more - whereas I could do the job manually for really quite a lot of classes in the same amount of time. Automation's great, but sometimes the quickest solution really is to do things by hand... even 200 classes may well be faster to do manually, and you could definitely parallelize the work across multiple people.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
3
(\{\s*)(\w*\s*?=\s*?\w*\s*?;\s*?)*?(public\s*\w*\s*)(\w*)(\s*?{\s*?get;\s*?\})(\s*?=\s*?\w*;\s*)
\1\2\4\5

A few answers: the first with a simple Regex find and replace which you need to repeat a few times:

  1. Regex: A few lines of explanation then the actual regex string and replacement string:

    a. In regex, first you match the full string of what your looking for (in your case a primary constructor). Not hard to do: search for curly bracket, the word public, then two words and an equals sign etc. Each text found according to this is called a Match.

    b. Sometimes there are possible repeated sequences in the text that you are looking for. (In your case: The parameters are defined in a line for each). For that, you simply mark the expected sequence as a Group by surrounding it with parenthesis.

    c. You then want to mark different parts of what you found, so you can use them or replace them in your corrected text. These parts are also called "Groups" actually "Capture Groups". Again simply surround the parts with parenthesis. In your case you'll be retaining the first captured group (the curly bracket) and the name of the property with its assignment to the parameter.

    d. Here's the regex:

    (\{\s*)(\w*\s*?=\s*?\w*\s*?;\s*?)*?(public\s*\w*\s*)(\w*)(\s*?{\s*?get;\s*?})(\s*?=\s*?\w*;\s*)
    
    1. ( 
          // ---- Capture1 -----
          {         
              // code: \{\s*? 
              // explained: curley bracket followed by possible whitespace
       ) 
    
    2. ( - Capture2  - previously corrected text 
          // - possible multiple lines of 'corrected' non-primary-constructors 
          // created during the find-replace process previously, 
    
          Propname = paramname;  // word,  equals-sign, word, semicolon 
          // code:  \w*\s*?=\s*?\w*\s*?;\s*?
          // explained:   \w - any alphanumeric, \s - any whitespace
          //              * - one or more times, *? - 0 or more times 
       )*?  
          // code: )*?
          // explained:  this group can be repeated zero or more times 
          // in other words it may not be found at all. 
          // These text lines are created during the recursive replacement process...
    
    3. ( 
          //  ----Capture 3-----
          // The first line of a primary constructor: 
          public type
          // code: public\s*\w*\s*  
          // explained: the word 'public' and then another word (and [whitespace]) 
        )
    
    4. (
        // ----- capture 4 -----
        Propname
         // code: \w@  
         // explained:  any amount of alphanumeric letters
       )
    
    5. (
         // ---- capture 5 ----
         { get; }
         // code: \s*?{\s*?get;\s*?\}
       )
    
    6. (
        // ---- capture 6 ----
         = propname; 
        code: \s*?=\s*?\w*;\s*
        explained: by now you should get it. 
    

The replacement string is

\1\2\4\6

This leaves:

{ 
    [old corrected code] 
    [new corrected line]
    possible remaining lines to be corrected. 
  1. Notepad++ 10 minutes trial-and-error. I guarantee it won't take you more than that.

  2. Visual Studio 2014 refactor. but a. You have to install it on a separate VM or PC. MS warns you not to install it side by side with your existing code. b. I'm not sure the refactor works the other way. [Here's an article about it][1]

  3. Visual Studio macros. I know I know, they're long gone, but there are at least two plugins that replace them and perhaps more. I read about them on this SO (StackOverflow) discussion. (They give a few other options) Here:

  4. Visual Commander - Free open source Visual Studio macro runner add-on
  5. VSScript - A Visual Studio add-on: costs $50 !!

  6. Try Automatic Regexp by example:You give it several examples of code in which you highlight what IS the expected result, and then the same (or other) code in which you highlight what IS NOT the expected result. You then wait for it to run through the examples and give you some regex code.

    // for the following code (from http://odetocode.com/blogs/scott/archive/2014/08/14/c-6-0-features-part-ii-primary-constructors.aspx )

    public struct Money(string currency, decimal amount) { public string Currency { get; } = currency; public decimal Amount { get; } = amount; } // I get something like: { ++\w\w[^r-u][^_]++|[^{]++(?={ \w++ =)

  7. Play with the regexp on this great site: https://www.regex101.com/

    // I first tried: \{\s*((public\s*\w*\s*)\w*(\s*?{\s*?get;\s*?})\s*?=\s*?\w*;\s*)*\}
    

The repeated sequence of the primary-constructor lines (the "repeated capture group") only captures the last one.

  1. Use c# code with regex.captures as explained here in another StackOverflow (see accepted answer)
Community
  • 1
  • 1
pashute
  • 3,965
  • 3
  • 38
  • 65