1515using System . Linq ;
1616using System . Management . Automation . Language ;
1717using Microsoft . Windows . PowerShell . ScriptAnalyzer . Generic ;
18+ #ifCORECLR
19+ using Pluralize . NET ;
20+ #else
1821using System . ComponentModel . Composition ;
22+ using System . Data . Entity . Design . PluralizationServices ;
23+ #endif
1924using System . Globalization ;
20- using System . Text . RegularExpressions ;
2125
2226namespace Microsoft . Windows . PowerShell . ScriptAnalyzer . BuiltinRules
2327{
2428/// <summary>
2529/// CmdletSingularNoun: Analyzes scripts to check that all defined cmdlets use singular nouns.
2630///
2731/// </summary>
28- [ Export ( typeof ( IScriptRule ) ) ]
29- public class CmdletSingularNoun : IScriptRule {
32+ #if! CORECLR
33+ [ Export ( typeof ( IScriptRule ) ) ]
34+ #endif
35+ public class CmdletSingularNoun : IScriptRule
36+ {
3037
3138private readonly string [ ] nounAllowList =
3239{
@@ -39,46 +46,49 @@ public class CmdletSingularNoun : IScriptRule {
3946/// <param name="ast"></param>
4047/// <param name="fileName"></param>
4148/// <returns></returns>
42- public IEnumerable < DiagnosticRecord > AnalyzeScript ( Ast ast , string fileName ) {
49+ public IEnumerable < DiagnosticRecord > AnalyzeScript ( Ast ast , string fileName )
50+ {
4351if ( ast == null ) throw new ArgumentNullException ( Strings . NullCommandInfoError ) ;
4452
4553IEnumerable < Ast > funcAsts = ast . FindAll ( item=> item is FunctionDefinitionAst , true ) ;
4654
47- char [ ] funcSeperator = { '-' } ;
48- string [ ] funcNamePieces = new string [ 2 ] ;
55+ var pluralizer = new PluralizerProxy ( ) ;
4956
5057foreach ( FunctionDefinitionAst funcAst in funcAsts )
5158{
52- if ( funcAst . Name != null && funcAst . Name . Contains ( '-' ) )
59+ if ( funcAst . Name == null || ! funcAst . Name . Contains ( '-' ) )
60+ {
61+ continue ;
62+ }
63+
64+ string noun = GetLastWordInCmdlet ( funcAst . Name ) ;
65+
66+ if ( noun is null )
5367{
54- funcNamePieces = funcAst . Name . Split ( funcSeperator ) ;
55- String nounPart = funcNamePieces [ 1 ] ;
56-
57- // Convert the noun part of the function into a series of space delimited words
58- // This helps the PluralizationService to provide an accurate determination about the plurality of the string
59- nounPart = SplitCamelCaseString ( nounPart ) ;
60- var words = nounPart . Split ( new char [ ] { ' ' } ) ;
61- var noun = words . LastOrDefault ( ) ;
62- if ( noun == null )
68+ continue ;
69+ }
70+
71+ if ( pluralizer . CanOnlyBePlural ( noun ) )
72+ {
73+ if ( nounAllowList . Contains ( noun , StringComparer . OrdinalIgnoreCase ) )
6374{
6475continue ;
6576}
66- var ps = System . Data . Entity . Design . PluralizationServices . PluralizationService . CreateService ( CultureInfo . GetCultureInfo ( "en-us" ) ) ;
6777
68- if ( ! ps . IsSingular ( noun ) && ps . IsPlural ( noun ) )
78+ IScriptExtent extent = Helper . Instance . GetScriptExtentForFunctionName ( funcAst ) ;
79+
80+ if ( extent is null )
6981{
70- IScriptExtent extent = Helper . Instance . GetScriptExtentForFunctionName ( funcAst ) ;
71- if ( nounAllowList . Contains ( noun , StringComparer . OrdinalIgnoreCase ) )
72- {
73- continue ;
74- }
75- if ( null == extent )
76- {
77- extent = funcAst . Extent ;
78- }
79- yield return new DiagnosticRecord ( string . Format ( CultureInfo . CurrentCulture , Strings . UseSingularNounsError , funcAst . Name ) ,
80- extent , GetName ( ) , DiagnosticSeverity . Warning , fileName ) ;
82+ extent = funcAst . Extent ;
8183}
84+
85+ yield return new DiagnosticRecord (
86+ string . Format ( CultureInfo . CurrentCulture , Strings . UseSingularNounsError , funcAst . Name ) ,
87+ extent ,
88+ GetName ( ) ,
89+ DiagnosticSeverity . Warning ,
90+ fileName ,
91+ suggestedCorrections : new CorrectionExtent [ ] { GetCorrection ( pluralizer , extent , funcAst . Name , noun ) } ) ;
8292}
8393}
8494
@@ -106,7 +116,8 @@ public string GetCommonName()
106116/// GetDescription: Retrieves the description of this rule.
107117/// </summary>
108118/// <returns>The description of this rule</returns>
109- public string GetDescription ( ) {
119+ public string GetDescription ( )
120+ {
110121return string . Format ( CultureInfo . CurrentCulture , Strings . UseSingularNounsDescription ) ;
111122}
112123
@@ -135,18 +146,77 @@ public string GetSourceName()
135146return string . Format ( CultureInfo . CurrentCulture , Strings . SourceName ) ;
136147}
137148
149+ private CorrectionExtent GetCorrection ( PluralizerProxy pluralizer , IScriptExtent extent , string commandName , string noun )
150+ {
151+ string singularNoun = pluralizer . Singularize ( noun ) ;
152+ string newCommandName = commandName . Substring ( 0 , commandName . Length - noun . Length ) + singularNoun ;
153+ return new CorrectionExtent ( extent , newCommandName , extent . File , $ "Singularized correction of '{ extent . Text } '") ;
154+ }
155+
138156/// <summary>
139- /// SplitCamelCaseString: Splits a Camel Case'd string into individual words with space delimited
157+ /// Gets the last word in a standard syntax, CamelCase cmdlet.
158+ /// If the cmdlet name is non-standard, returns null.
140159/// </summary>
141- private string SplitCamelCaseString ( string input )
160+ private string GetLastWordInCmdlet ( string cmdletName )
142161{
143- if ( String . IsNullOrEmpty ( input ) )
162+ if ( string . IsNullOrEmpty ( cmdletName ) )
144163{
145- return String . Empty ;
164+ return null ;
146165}
147166
148- return Regex . Replace ( input , "([A-Z])" , " $1" , RegexOptions . Compiled ) . Trim ( ) ;
167+ // Cmdlet doesn't use CamelCase, so assume it's something like an initialism that shouldn't be singularized
168+ if ( ! char . IsLower ( cmdletName [ cmdletName . Length - 1 ] ) )
169+ {
170+ return null ;
171+ }
172+
173+ for ( int i = cmdletName . Length - 1 ; i >= 0 ; i -- )
174+ {
175+ if ( cmdletName [ i ] == '-' )
176+ {
177+ // We got to the dash without seeing a CamelCase word, so nothing to singularize
178+ return null ;
179+ }
180+
181+ // We just changed from lower case to upper, so we have the end word
182+ if ( char . IsUpper ( cmdletName [ i ] ) )
183+ {
184+ return cmdletName . Substring ( i ) ;
185+ }
186+ }
187+
188+ // We shouldn't ever get here since we should always eventually hit a '-'
189+ // But if we do, assume this isn't supported cmdlet name
190+ return null ;
191+ }
192+
193+ #ifCORECLR
194+ private class PluralizerProxy
195+ {
196+ private readonly Pluralizer _pluralizer ;
197+
198+ public PluralizerProxy ( )
199+ {
200+ _pluralizer = new Pluralizer ( ) ;
201+ }
202+
203+ public bool CanOnlyBePlural ( string noun ) =>
204+ ! _pluralizer . IsSingular ( noun ) && _pluralizer . IsPlural ( noun ) ;
205+
206+ public string Singularize ( string noun ) => _pluralizer . Singularize ( noun ) ;
207+ }
208+ #else
209+ private class PluralizerProxy
210+ {
211+ private static readonly PluralizationService s_pluralizationService = PluralizationService . CreateService (
212+ CultureInfo . GetCultureInfo ( "en-us" ) ) ;
213+
214+ public bool CanOnlyBePlural ( string noun ) =>
215+ ! s_pluralizationService . IsSingular ( noun ) && s_pluralizationService . IsPlural ( noun ) ;
216+
217+ public string Singularize ( string noun ) => s_pluralizationService . Singularize ( noun ) ;
149218}
219+ #endif
150220}
151221
152222}