-1

I have Dictionary<string, List<string>> object. The Key represents the name of a file and the Value is a List<string> which represents the names of certain methods in the file.

I loop over the dictionary and use the Key to read in data from the file. I'm trying to then find lines in this file which contain an element from the Values object:

static void FindInvalidAttributes(Dictionary<string, List<string>> dictionary)
{
    //Get the files from my controller dir
    List<string> controllers = Directory.GetFiles(controllerPath, "*.cs", SearchOption.AllDirectories).ToList<string>();

    //Iterate over my dictionary
    foreach (KeyValuePair<string, List<string>> entry in dictionary)
    {
        //Build the correct file name using the dictionary key
        string controller = Path.Combine(ControllerPath, entry.Key + "Controller.cs");

        if (File.Exists(controller))
        {
            //Read the file content and loop over it
            string[] lines = File.ReadAllLines(controller);
            for (int i = 0; i < lines.Count(); i++)
            {
                //loop over every element in my dictionary's value (List<string>)
                foreach (string method in entry.Value)
                {
                    //If the line in the file contains a dictionary value element
                    if (lines[i].IndexOf(method) > -1 && lines[i].IndexOf("public") > -1)
                    {
                        //Get the previous line containing the attribute
                        string verb = lines[i - 1];
                    }
                }
            }
        }
    }
}

There's got to be a cleaner way to implement the code inside the if (File.Exists(controller)) statement. I don't want to nest a foreach inside of a for, inside the parent-most foreach.

Question: How can I determine if a string contains any element in a List<string> using LINQ?

Note that the two values are not identical; part of the string should contain the entire list element. I was able to find plenty of examples looking for the string inside the list elements, but that is not what I'm trying to do.

Example:

lines[0] = "public void SomeMethod()";
lines[1] = "public void SomeOtherMethod()";

List<string> myList = new List<string>();
myList.Add("SomeMethod");
myList.Add("AnotherMethod");

Using the above data, lines[0] should result in my FindInvalidAttributes method looking at the previous line because this string contains the first element in myList. lines[1] should not result in the method inspecting the previous line because SomeOtherMethod does not appear in myList.

Edit I'm very curious why this was downvoted and marked to be closed for being "too broad". I'm asking a tremendously specific question, provided my code, sample data and expected output for the sample data.

sab669
  • 3,984
  • 8
  • 38
  • 75
  • Can't you use reflection for this task? – Alisson Reinaldo Silva May 31 '17 at 17:49
  • Method definitions can be split onto different lines which would break your logic if the programmer decided to do so. Also overloaded methods may mess things up. An example of getting attributes for methods (versus class attributes) is [here](https://stackoverflow.com/questions/3467765/find-methods-that-have-custom-attribute-using-reflection); use reflection to get a list of methods as `MethodInfo` objects, then iterate those objects to get each method's attributes. Parsing .cs files I'm pretty sure is a hornets nest. – Quantic May 31 '17 at 18:20
  • @Quantic For my purposes I can guarantee that method declarations are on one line and that overloads aren't a concern. Every `public` method defined in all of my controllers is marked with 1 of 2 possible attributes. Some JS files curiously invoke these methods in an unconventional way and need to make sure these methods have the correct attribute assigned. I will look into reflection, but I'd prefer to solve the general problem of "How to determine if a string contains an element from a collection" rather than the specific problem of "which attributes does this method have". – sab669 May 31 '17 at 18:38

2 Answers2

1

If you have lines with all lines from the file and entry.Value as the list of words to look for, then the following will return all lines containing at least one word from entry.Value:

var result = lines.Where(l => l.Split(' ').Intersect(entry.Value).Any());

or more into your specific example:

var result = 
    lines.Where(l => 
        l.Trim().StartsWith("public") && entry.Value.Any(l.Contains));
Ofir Winegarten
  • 9,215
  • 2
  • 21
  • 27
  • https://dotnetfiddle.net/qC8hP0 - `FindInvalidAttributes` is my implementation. `FindWithLINQ` is yours. The first one correctly identifies something in `lines` matches an element in `myList`, the second one does not. I tried with both snippets of code you provided but neither generates any output – sab669 May 31 '17 at 18:25
  • You are right, it because of the parenthesis. I've edited my answer – Ofir Winegarten May 31 '17 at 18:38
  • Just to make sure I'm reading this correctly, `entry.Value.Any(l.Contains)` can essentially be read as "If the line (`l`) contains any value found in `entry.Value`", is that correct? – sab669 May 31 '17 at 18:49
  • 1
    Yes. That's exactly what it means – Ofir Winegarten May 31 '17 at 18:55
  • @sab669 it is this [`string.Contains()` method](https://msdn.microsoft.com/en-us/library/dy85x1sa(v=vs.110).aspx) instead of e.g., `IndexOf` and has the caveats explained on that page. – Quantic May 31 '17 at 18:59
  • @OfirWinegarten, OP's code attempts to extract line immediately before the match, does it not? I don't see that part in your solution, or am I missing something? – LB2 May 31 '17 at 19:04
  • @LB2 The question is "How to determine if a string contains an element within a collection" which this solves – sab669 May 31 '17 at 19:33
1

You can build up a regular expression that looks for all items in the list for the file in one shot.

Also, note that you're not using controllers variable at all.

static void FindInvalidAttributes(Dictionary<string, List<string>> dictionary)
{
    //Get the files from my controller dir
    List<string> controllers = Directory.GetFiles(controllerPath, "*.cs", SearchOption.AllDirectories).ToList<string>();

    //Iterate over my dictionary
    foreach (var entry in dictionary)
    {
        //Build the correct file name using the dictionary key
        string controller = Path.Combine(ControllerPath, entry.Key + "Controller.cs");

        if (File.Exists(controller))
        {
            var regexText = "(?<attribLine>.*)\n" + string.Join("|", entry.Value.Select(t => "[^\n]*public\s[^\n]*" + Regex.Escape(t)))
            var regex = new Regex(regexText)
            //Read the file content and loop over it
            var fileContent = File.ReadAllText(controller);
            foreach (Match match in regex.Matches(fileContent))
            {
                 // Here, match.Groups["attribLine"] should contain here what you're looking for.
            }
        }
    }
}

I don't have input, so cannot easily test the code or the regex, but it should give the general idea for the approach and simplification.

LB2
  • 4,802
  • 19
  • 35
  • Woops, I forgot to remove the declaration for `controllers` while working on this. Could you explain what the `var regexText = ...` line is doing? I don't understand `(?.*)\n` and putting that into regexr.com says the `?` is an target for a quantifier. – sab669 May 31 '17 at 18:31
  • @sab669 `regexText` is a regular expression that tries to match a line that is followed by the next line that contains at least one of the strings in the list. The first line is captured in a named group - `attribLine` - that upon a match can be retrieved. That line is what contains attribute information for your method. Does it make sense? – LB2 May 31 '17 at 19:02
  • Ah I'm not familiar with named groups. So basically that's just a way to create elements in this `match.Groups` collection which can be referenced by a string. But there is some error in the regex text itself: I get `Unrecognized escape sequence` on the **s** in `"[^\n]*public\s[^\n]\*"` which you can see [here](https://dotnetfiddle.net/2BAGch) (although it wasn't until I actually put it into VS that it told me it was the `s` specifically) – sab669 May 31 '17 at 19:55
  • OK fixed that issue by prefacing the string with `@` (and adding the missing semi-colons). [Updated dotnetfiddle](https://dotnetfiddle.net/VUg7Y0) seems to be good? Although `match.Groups["attribLine"]` does not seem to compile – sab669 May 31 '17 at 20:09
  • `\s` should be a white space, i.e. a space, a tab... you can probably replace it with \b - word boundary and not change the semantics of it. – LB2 May 31 '17 at 20:25
  • 1
    @sab669, I see the compilation issue. Change `foreach (var match ` to `foreach (Match match `. I just tried it in your fiddle, and it worked. – LB2 May 31 '17 at 20:27