Excellent question!
So, this is a common concern, not only with controllers but also potentially with services that a directive might need to perform its job but don't necessarily want to expose this controller / service to the "external world".
I strongly believe that global data are evil and should be avoided and this applies to directive controllers as well. If we take this assumption we can take several different approaches to define those controllers "locally". While doing so we need to keep in mind that a controller should be still "easily" accessible to unit tests so we can't simply hide it into directive's closure. IMO possibilities are:
1) Firstly, we could simply define directive's controller on a module level, ex::
angular.module('ui.bootstrap.tabs', [])
.controller('TabsController', ['$scope', '$element', function($scope, $element) {
...
}])
.directive('tabs', function() {
return {
restrict: 'EA',
transclude: true,
scope: {},
controller: 'TabsController',
templateUrl: 'template/tabs/tabs.html',
replace: true
};
})
This is a simple technique that we are using in https://github.com/angular-ui/bootstrap/blob/master/src/tabs/tabs.js which is based on Vojta's work.
While this is a very simple technique it should be noted that a controller is still exposed to the whole application which means that other module could potentially override it. In this sense it makes a controller local to AngularJS application (so not polluting a global window scope) but it also global to all AngularJS modules.
2) Use a closure scope and special files setup for testing.
If we want to completely hide a controller function we can wrap code in a closure. This is a technique that AngularJS is using. For example, looking at the NgModelController we can see that it is defined as a "global" function in its own files (and thus easily accessible for testing) but the whole file is wrapped in closure during the build time:
To sum up: the option (2) is "safer" but requires a bit of up-front setup for the build.