diff --git a/python/ycm/diagnostic_filter.py b/python/ycm/diagnostic_filter.py index a69a3e7d..9a68824b 100644 --- a/python/ycm/diagnostic_filter.py +++ b/python/ycm/diagnostic_filter.py @@ -24,20 +24,19 @@ standard_library.install_aliases() from builtins import * # noqa from future.utils import iterkeys, iteritems +from ycm import vimsupport import re class DiagnosticFilter( object ): - def __init__( self, config ): - self._filters = [] + def __init__( self, config_or_filters ): + if isinstance( config_or_filters, list ): + self._filters = config_or_filters + print( 'NewFilter', config_or_filters) - for filter_type in iterkeys( config ): - compiler = FILTER_COMPILERS.get( filter_type ) - - if compiler is not None: - for filter_config in _ListOf( config[ filter_type ] ): - compiledFilter = compiler( filter_config ) - self._filters.append( compiledFilter ) + else: + self._filters = _CompileFilters( config_or_filters ) + print( 'CompileFilters', config_or_filters) def IsAllowed( self, diagnostic ): @@ -49,19 +48,59 @@ class DiagnosticFilter( object ): return True - @staticmethod - def from_filetype( user_options, filetypes ): - spec = {} - all_filters = dict( user_options.get( 'filter_diagnostics', {} ) ) - for typeSpec, filterValue in iteritems( dict( all_filters ) ): - if typeSpec.find(',') != -1: - for filetype in typeSpec.split(','): - all_filters[ filetype ] = filterValue + def SubsetForTypes( self, filetypes ): + """Return a sub-filter limited to the given filetypes""" + # NOTE: actually, this class is already filtered + return self + + @staticmethod + def CreateFromOptions( user_options ): + all_filters = dict( user_options.get( 'filter_diagnostics', {} ) ) + compiled_by_type = {} + for type_spec, filter_value in iteritems( dict( all_filters ) ): + filetypes = [ type_spec ] + if type_spec.find( ',' ) != -1: + filetypes = type_spec.split( ',' ) + for filetype in filetypes: + compiled_by_type[ filetype ] = _CompileFilters( filter_value ) + + return _MasterDiagnosticFilter( compiled_by_type ) + + +class _MasterDiagnosticFilter( object ): + + def __init__( self, all_filters ): + self._all_filters = all_filters + self._cache = {} + + + def IsAllowed( self, diagnostic ): + # NOTE: in this class's implementation, we ask vimsupport for + # the current filetypes and delegate automatically; it is probably, + # more efficient, however, to call SubsetForTypes() and reuse + # the returned DiagnosticFilter if it will be checked repeatedly. + filetypes = vimsupport.CurrentFiletypes() + return self.SubsetForTypes( filetypes ).IsAllowed( diagnostic ) + + + def SubsetForTypes( self, filetypes ): + # check cache + cache_key = ','.join( filetypes ) + cached = self._cache.get( cache_key ) + if cached is not None: + return cached + + # build a new DiagnosticFilter mergin all filters + # for the provided filetypes + spec = [] for filetype in filetypes: - type_specific = all_filters.get( filetype, {} ) - spec = _Merge( spec, type_specific ) - return DiagnosticFilter( spec ) + type_specific = self._all_filters.get( filetype, [] ) + spec.extend( type_specific ) + + new_filter = DiagnosticFilter( spec ) + self._cache[ cache_key ] = new_filter + return new_filter def _ListOf( config_entry ): @@ -74,13 +113,6 @@ def _ListOf( config_entry ): return [ config_entry ] -def _Merge( into, other ): - for key in iterkeys( other ): - into[ key ] = _ListOf( into.get( key ) ) + _ListOf( other[ key ] ) - - return into - - def _CompileRegex( raw_regex ): pattern = re.compile( raw_regex, re.IGNORECASE ) @@ -104,3 +136,18 @@ def _CompileLevel( level ): FILTER_COMPILERS = { 'regex' : _CompileRegex, 'level' : _CompileLevel } + + +def _CompileFilters( config ): + """Given a filter config dictionary, return a list of compiled filters""" + filters = [] + + for filter_type in iterkeys( config ): + compiler = FILTER_COMPILERS.get( filter_type ) + + if compiler is not None: + for filter_config in _ListOf( config[ filter_type ] ): + compiledFilter = compiler( filter_config ) + filters.append( compiledFilter ) + + return filters diff --git a/python/ycm/diagnostic_interface.py b/python/ycm/diagnostic_interface.py index 7866ba23..90385a33 100644 --- a/python/ycm/diagnostic_interface.py +++ b/python/ycm/diagnostic_interface.py @@ -33,6 +33,7 @@ import vim class DiagnosticInterface( object ): def __init__( self, user_options ): self._user_options = user_options + self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options ) # Line and column numbers are 1-based self._buffer_number_to_line_to_diags = defaultdict( lambda: defaultdict( list ) ) @@ -62,15 +63,13 @@ class DiagnosticInterface( object ): def PopulateLocationList( self, diags ): vimsupport.SetLocationList( - vimsupport.ConvertDiagnosticsToQfList( diags ) ) + vimsupport.ConvertDiagnosticsToQfList( + self._ApplyDiagnosticFilter( diags ) ) ) def UpdateWithNewDiagnostics( self, diags ): - diag_filter = DiagnosticFilter.from_filetype( - self._user_options, - vimsupport.CurrentFiletypes() ) - normalized_diags = [ _NormalizeDiagnostic( x ) for x in diags - if diag_filter.IsAllowed( x ) ] + normalized_diags = [ _NormalizeDiagnostic( x ) for x in + self._ApplyDiagnosticFilter( diags ) ] self._buffer_number_to_line_to_diags = _ConvertDiagListToDict( normalized_diags ) @@ -86,6 +85,13 @@ class DiagnosticInterface( object ): if self._user_options[ 'always_populate_location_list' ]: self.PopulateLocationList( normalized_diags ) + + def _ApplyDiagnosticFilter( self, diags ): + filetypes = vimsupport.CurrentFiletypes() + diag_filter = self._diag_filter.SubsetForTypes( filetypes ) + return filter( diag_filter.IsAllowed, diags ) + + def _EchoDiagnosticForLine( self, line_num ): buffer_num = vim.current.buffer.number diags = self._buffer_number_to_line_to_diags[ buffer_num ][ line_num ] diff --git a/python/ycm/tests/diagnostic_filter_test.py b/python/ycm/tests/diagnostic_filter_test.py index 9741e0c9..c73e6127 100644 --- a/python/ycm/tests/diagnostic_filter_test.py +++ b/python/ycm/tests/diagnostic_filter_test.py @@ -24,6 +24,10 @@ standard_library.install_aliases() from builtins import * # noqa from hamcrest import assert_that, equal_to + +from ycm.test_utils import MockVimModule +MockVimModule() + from ycm.diagnostic_filter import DiagnosticFilter @@ -46,9 +50,13 @@ def _JavaFilter( config ): return { 'filter_diagnostics' : { 'java': config } } +def _CreateFilterForTypes( opts, types ): + return DiagnosticFilter.CreateFromOptions( opts ).SubsetForTypes( types ) + + def RegexFilter_test(): opts = _JavaFilter( { 'regex' : 'taco' } ) - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) + f = _CreateFilterForTypes( opts, [ 'java' ] ) _assert_rejects( f, 'This is a Taco' ) _assert_accepts( f, 'This is a Burrito' ) @@ -56,7 +64,7 @@ def RegexFilter_test(): def RegexSingleList_test(): opts = _JavaFilter( { 'regex' : [ 'taco' ] } ) - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) + f = _CreateFilterForTypes( opts, [ 'java' ] ) _assert_rejects( f, 'This is a Taco' ) _assert_accepts( f, 'This is a Burrito' ) @@ -64,15 +72,23 @@ def RegexSingleList_test(): def RegexMultiList_test(): opts = _JavaFilter( { 'regex' : [ 'taco', 'burrito' ] } ) - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) + f = _CreateFilterForTypes( opts, [ 'java' ] ) _assert_rejects( f, 'This is a Taco' ) _assert_rejects( f, 'This is a Burrito' ) +def RegexNotFiltered_test(): + opts = _JavaFilter( { 'regex' : 'taco' } ) + f = _CreateFilterForTypes( opts, [ 'cs' ] ) + + _assert_accepts( f, 'This is a Taco' ) + _assert_accepts( f, 'This is a Burrito' ) + + def LevelWarnings_test(): opts = _JavaFilter( { 'level' : 'warning' } ) - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) + f = _CreateFilterForTypes( opts, [ 'java' ] ) _assert_rejects( f, { 'text' : 'This is an unimportant taco', 'kind' : 'WARNING' } ) @@ -82,7 +98,7 @@ def LevelWarnings_test(): def LevelErrors_test(): opts = _JavaFilter( { 'level' : 'error' } ) - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) + f = _CreateFilterForTypes( opts, [ 'java' ] ) _assert_accepts( f, { 'text' : 'This is an IMPORTANT taco', 'kind' : 'WARNING' } ) @@ -94,7 +110,7 @@ def MultipleFilterTypesTypeTest_test(): opts = _JavaFilter( { 'regex' : '.*taco.*', 'level' : 'warning' } ) - f = DiagnosticFilter.from_filetype( opts, [ 'java' ] ) + f = _CreateFilterForTypes( opts, [ 'java' ] ) _assert_rejects( f, { 'text' : 'This is an unimportant taco', 'kind' : 'WARNING' } ) @@ -110,7 +126,7 @@ def MergeMultipleFiletypes_test(): 'java' : { 'regex' : '.*taco.*' }, 'xml' : { 'regex' : '.*burrito.*' } } } - f = DiagnosticFilter.from_filetype( opts, [ 'java', 'xml' ] ) + f = _CreateFilterForTypes( opts, [ 'java', 'xml' ] ) _assert_rejects( f, 'This is a Taco' ) _assert_rejects( f, 'This is a Burrito' ) @@ -122,7 +138,7 @@ def CommaSeparatedFiletypes_test(): opts = { 'filter_diagnostics' : { 'java,c,cs' : { 'regex' : '.*taco.*' } } } - f = DiagnosticFilter.from_filetype( opts, [ 'cs' ] ) + f = _CreateFilterForTypes( opts, [ 'cs' ] ) _assert_rejects( f, 'This is a Taco' ) _assert_accepts( f, 'This is a Burrito' )