Types with source files stored in the same directory, should be declared in the same namespace

Rule ND2104 Source Files Organization

The namespaces hierarchy should be synchronized with the source files directory hierarchy — a widely accepted convention; misalignment makes code less maintainable and harder to navigate. Hence types with source files in the same directory should share the same namespace.

For each directory where most types are in one namespace (the main namespace) and a few are not, this rule matches the main namespace, typesOutOfMainNamespace (declared in the directory but outside it), and typesInMainNamespace (informational).

Source files out of the project directory are ignored: such linked files are shared across projects and already matched by the rule Avoid referencing source file out of the project directory.

How to fix

Violations are the types in typesOutOfMainNamespace. Typically such type…

- … is in the wrong namespace, right directory: move it to the main namespace.

- … is in the right namespace, wrong directory: move its source file to the proper namespace directory.

- … is declared in source files in different directories: prefer a single directory.

Estimated Debt: 2 minutes plus 5 minutes per type in typesOutOfMainNamespace.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>Types with source files stored in the same directory, should be declared in the same namespace</Name>
// <Id>ND2104:TypesWithSourceFilesStoredInTheSameDirectoryShouldBeDeclaredInTheSameNamespace</Id>

warnif count > 0 

// Group JustMyCode types in a lookup 
// where groups are keyed with directories that contain the types' source file(s).
// Note that a type can be contained in several groups 
// if it is declared in several source files stored in different directories.
let lookup = JustMyCode.Types.Where(t => t.SourceFileDeclAvailable)
            .ToMultiKeyLookup(
               t => t.SourceDecls
                          // Source files out of the project directory are linked files shared across
                          // projects: they follow no single project's folder layout (ND2100 matches them)
                          .Where(decl => t.ParentAssembly.VisualStudioProjectFilePath == null ||
                                         decl.SourceFile.FilePath.IsChildOf(t.ParentAssembly.VisualStudioProjectFilePath.ParentDirectoryPath))
                          .Select(decl => decl.SourceFile.FilePath.ParentDirectoryPath).Distinct()
            )

from groupOfTypes in lookup
let parentNamespaces = groupOfTypes.ParentNamespaces()

// Select group of types (with source files stored in the same directory) …
// … but contained in several namespaces
where parentNamespaces.Count() > 1

// mainNamespaces is the namespace that contains many types 
// declared in the directory groupOfTypes .key
let mainNamespace  = groupOfTypes
                     .ToLookup(t => t.ParentNamespace)
                     .OrderByDescending(g => g.Count()).First().Key

// Select types with source files stored in the same directory,
// but contained in namespaces different than mainNamespace.
let typesOutOfMainNamespace = groupOfTypes
                              .Where(t => t.ParentNamespace != mainNamespace &&
                                          t.ParentAssembly == mainNamespace.ParentAssembly)

                              // Filter types declared on several source files that contain generated methods 
                              // because typically such type contains one or several partial definitions generated.
                              // These partially generated types would be false positive for the present rule.
                              .Where(t => t.SourceDecls.Count() == 1 ||
                                          t.Methods.Count(m => JustMyCode.Contains(m)) == 0)
where typesOutOfMainNamespace.Count() > 0

let typesInMainNamespace = groupOfTypes.Where(t => t.ParentNamespace == mainNamespace)

select new { 
   mainNamespace, 
   typesOutOfMainNamespace,
   typesInMainNamespace,
   Debt = (2+5*typesOutOfMainNamespace.Count()).ToMinutes().ToDebt(),
   Severity = Severity.High
}

//<Expl>
// The source file{2?:s} that contain the declaration{2?:s} of the *{0}* 
// {2?is:are} located in a directory that also contains the *{1}* declared 
// in {1?a:} different namespace{1?:s}.
// {1?Concerning this type:For each of these types}, either change
// its namespace or its source file's directory.
//</Expl>

//<Desc>
// The namespaces hierarchy should be synchronized with the source files
// directory hierarchy — a widely accepted convention; misalignment makes
// code less maintainable and harder to navigate. Hence types with source
// files in the same directory should share the same namespace.
//
// For each directory where most types are in one namespace (the **main
// namespace**) and a few are not, this rule matches the *main namespace*,
// **typesOutOfMainNamespace** (declared in the directory but outside it),
// and *typesInMainNamespace* (informational).
//
// Source files out of the project directory are ignored: such linked files
// are shared across projects and already matched by the rule *Avoid
// referencing source file out of the project directory*.
//</Desc>

//<HowToFix>
// Violations are the types in *typesOutOfMainNamespace*. Typically such
// type…
//
// -  … is in the wrong namespace, right directory: move it to the *main
//    namespace*.
//
// -  … is in the right namespace, wrong directory: move its source file to
//    the proper namespace directory.
//
// -  … is declared in source files in different directories: prefer a
//    single directory.
//
// Estimated Debt: 2 minutes plus 5 minutes per type in
// *typesOutOfMainNamespace*.
//</HowToFix>

Source Files Organization: all 6 items