Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2894952
ArcanistLintEngine.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
14 KB
Referenced Files
None
Subscribers
None
ArcanistLintEngine.php
View Options
<?php
abstract
class
ArcanistLintEngine
extends
ArcanistOperationEngine
{
protected
$workingCopy
;
protected
$paths
=
array
(
)
;
protected
$fileData
=
array
(
)
;
private
$cachedResults
;
private
$cacheVersion
;
private
$repositoryVersion
;
private
$results
=
array
(
)
;
private
$stopped
=
array
(
)
;
private
$minimumSeverity
=
ArcanistLintSeverity
::
SEVERITY_DISABLED
;
private
$changedLines
=
array
(
)
;
private
$configurationManager
;
private
$linterResources
=
array
(
)
;
final
public
function
getUnitEngineType
(
)
{
return
$this
->
getPhobjectClassConstant
(
'ENGINETYPE'
)
;
}
public
static
function
getAllLintEngines
(
)
{
return
id
(
new
PhutilClassMapQuery
(
)
)
->
setAncestorClass
(
__CLASS__
)
->
setUniqueMethod
(
'getLintEngineType'
)
->
execute
(
)
;
}
final
public
function
setPathChangedLines
(
$path
,
$changed
)
{
if
(
$changed
===
null
)
{
$this
->
changedLines
[
$path
]
=
null
;
}
else
{
$this
->
changedLines
[
$path
]
=
array_fill_keys
(
$changed
,
true
)
;
}
return
$this
;
}
final
public
function
getPathChangedLines
(
$path
)
{
return
idx
(
$this
->
changedLines
,
$path
)
;
}
final
public
function
setFileData
(
$data
)
{
$this
->
fileData
=
$data
+
$this
->
fileData
;
return
$this
;
}
final
public
function
loadData
(
$path
)
{
if
(
!
isset
(
$this
->
fileData
[
$path
]
)
)
{
$disk_path
=
$this
->
getFilePathOnDisk
(
$path
)
;
$this
->
fileData
[
$path
]
=
Filesystem
::
readFile
(
$disk_path
)
;
}
return
$this
->
fileData
[
$path
]
;
}
public
function
pathExists
(
$path
)
{
$disk_path
=
$this
->
getFilePathOnDisk
(
$path
)
;
return
Filesystem
::
pathExists
(
$disk_path
)
;
}
final
public
function
isDirectory
(
$path
)
{
$disk_path
=
$this
->
getFilePathOnDisk
(
$path
)
;
return
is_dir
(
$disk_path
)
;
}
final
public
function
isBinaryFile
(
$path
)
{
try
{
$data
=
$this
->
loadData
(
$path
)
;
}
catch
(
Exception
$ex
)
{
return
false
;
}
return
ArcanistDiffUtils
::
isHeuristicBinaryFile
(
$data
)
;
}
final
public
function
isSymbolicLink
(
$path
)
{
return
is_link
(
$this
->
getFilePathOnDisk
(
$path
)
)
;
}
final
public
function
getFilePathOnDisk
(
$path
)
{
return
Filesystem
::
resolvePath
(
$path
,
$this
->
getWorkingCopy
(
)
->
getProjectRoot
(
)
)
;
}
final
public
function
setMinimumSeverity
(
$severity
)
{
$this
->
minimumSeverity
=
$severity
;
return
$this
;
}
final
public
function
run
(
)
{
$linters
=
$this
->
buildLinters
(
)
;
if
(
!
$linters
)
{
throw
new
ArcanistNoEffectException
(
pht
(
'No linters to run.'
)
)
;
}
foreach
(
$linters
as
$key
=>
$linter
)
{
$linter
->
setLinterID
(
$key
)
;
}
$linters
=
msort
(
$linters
,
'getLinterPriority'
)
;
foreach
(
$linters
as
$linter
)
{
$linter
->
setEngine
(
$this
)
;
}
$have_paths
=
false
;
foreach
(
$linters
as
$linter
)
{
if
(
$linter
->
getPaths
(
)
)
{
$have_paths
=
true
;
break
;
}
}
if
(
!
$have_paths
)
{
throw
new
ArcanistNoEffectException
(
pht
(
'No paths are lintable.'
)
)
;
}
$versions
=
array
(
$this
->
getCacheVersion
(
)
)
;
foreach
(
$linters
as
$linter
)
{
$version
=
get_class
(
$linter
)
.
':'
.
$linter
->
getCacheVersion
(
)
;
$symbols
=
id
(
new
PhutilSymbolLoader
(
)
)
->
setType
(
'class'
)
->
setName
(
get_class
(
$linter
)
)
->
selectSymbolsWithoutLoading
(
)
;
$symbol
=
idx
(
$symbols
,
'class$'
.
get_class
(
$linter
)
)
;
if
(
$symbol
)
{
$version
.=
':'
.
md5_file
(
phutil_get_library_root
(
$symbol
[
'library'
]
)
.
'/'
.
$symbol
[
'where'
]
)
;
}
$versions
[
]
=
$version
;
}
$this
->
cacheVersion
=
crc32
(
implode
(
"\n"
,
$versions
)
)
;
$runnable
=
$this
->
getRunnableLinters
(
$linters
)
;
$this
->
stopped
=
array
(
)
;
$exceptions
=
$this
->
executeLinters
(
$runnable
)
;
foreach
(
$runnable
as
$linter
)
{
foreach
(
$linter
->
getLintMessages
(
)
as
$message
)
{
$this
->
validateLintMessage
(
$linter
,
$message
)
;
if
(
!
$this
->
isSeverityEnabled
(
$message
->
getSeverity
(
)
)
)
{
continue
;
}
if
(
!
$this
->
isRelevantMessage
(
$message
)
)
{
continue
;
}
$message
->
setGranularity
(
$linter
->
getCacheGranularity
(
)
)
;
$result
=
$this
->
getResultForPath
(
$message
->
getPath
(
)
)
;
$result
->
addMessage
(
$message
)
;
}
}
if
(
$this
->
cachedResults
)
{
foreach
(
$this
->
cachedResults
as
$path
=>
$messages
)
{
$messages
=
idx
(
$messages
,
$this
->
cacheVersion
,
array
(
)
)
;
$repository_version
=
idx
(
$messages
,
'repository_version'
)
;
unset
(
$messages
[
'stopped'
]
)
;
unset
(
$messages
[
'repository_version'
]
)
;
foreach
(
$messages
as
$message
)
{
$use_cache
=
$this
->
shouldUseCache
(
idx
(
$message
,
'granularity'
)
,
$repository_version
)
;
if
(
$use_cache
)
{
$this
->
getResultForPath
(
$path
)
->
addMessage
(
ArcanistLintMessage
::
newFromDictionary
(
$message
)
)
;
}
}
}
}
foreach
(
$this
->
results
as
$path
=>
$result
)
{
$disk_path
=
$this
->
getFilePathOnDisk
(
$path
)
;
$result
->
setFilePathOnDisk
(
$disk_path
)
;
if
(
isset
(
$this
->
fileData
[
$path
]
)
)
{
$result
->
setData
(
$this
->
fileData
[
$path
]
)
;
}
else
if
(
$disk_path
&&
Filesystem
::
pathExists
(
$disk_path
)
)
{
// TODO: this may cause us to, e.g., load a large binary when we only
// raised an error about its filename. We could refine this by looking
// through the lint messages and doing this load only if any of them
// have original/replacement text or something like that.
try
{
$this
->
fileData
[
$path
]
=
Filesystem
::
readFile
(
$disk_path
)
;
$result
->
setData
(
$this
->
fileData
[
$path
]
)
;
}
catch
(
FilesystemException
$ex
)
{
// Ignore this, it's noncritical that we access this data and it
// might be unreadable or a directory or whatever else for plenty
// of legitimate reasons.
}
}
}
if
(
$exceptions
)
{
throw
new
PhutilAggregateException
(
pht
(
'Some linters failed:'
)
,
$exceptions
)
;
}
return
$this
->
results
;
}
final
public
function
isSeverityEnabled
(
$severity
)
{
$minimum
=
$this
->
minimumSeverity
;
return
ArcanistLintSeverity
::
isAtLeastAsSevere
(
$severity
,
$minimum
)
;
}
final
private
function
shouldUseCache
(
$cache_granularity
,
$repository_version
)
{
switch
(
$cache_granularity
)
{
case
ArcanistLinter
::
GRANULARITY_FILE
:
return
true
;
case
ArcanistLinter
::
GRANULARITY_DIRECTORY
:
case
ArcanistLinter
::
GRANULARITY_REPOSITORY
:
return
(
$this
->
repositoryVersion
==
$repository_version
)
;
default
:
return
false
;
}
}
/**
* @param dict<string path, dict<string version, list<dict message>>>
* @return this
*/
final
public
function
setCachedResults
(
array
$results
)
{
$this
->
cachedResults
=
$results
;
return
$this
;
}
final
public
function
getResults
(
)
{
return
$this
->
results
;
}
final
public
function
getStoppedPaths
(
)
{
return
$this
->
stopped
;
}
abstract
public
function
buildLinters
(
)
;
final
public
function
setRepositoryVersion
(
$version
)
{
$this
->
repositoryVersion
=
$version
;
return
$this
;
}
final
private
function
isRelevantMessage
(
ArcanistLintMessage
$message
)
{
// When a user runs "arc lint", we default to raising only warnings on
// lines they have changed (errors are still raised anywhere in the
// file). The list of $changed lines may be null, to indicate that the
// path is a directory or a binary file so we should not exclude
// warnings.
if
(
!
$this
->
changedLines
||
$message
->
isError
(
)
||
$message
->
shouldBypassChangedLineFiltering
(
)
)
{
return
true
;
}
$locations
=
$message
->
getOtherLocations
(
)
;
$locations
[
]
=
$message
->
toDictionary
(
)
;
foreach
(
$locations
as
$location
)
{
$path
=
idx
(
$location
,
'path'
,
$message
->
getPath
(
)
)
;
if
(
!
array_key_exists
(
$path
,
$this
->
changedLines
)
)
{
if
(
phutil_is_windows
(
)
)
{
// We try checking the UNIX path form as well, on Windows. Linters
// store noramlized paths, which use the Windows-style "\" as a
// delimiter; as such, they don't match the UNIX-style paths stored
// in changedLines, which come from the VCS.
$path
=
str_replace
(
'\\'
,
'/'
,
$path
)
;
if
(
!
array_key_exists
(
$path
,
$this
->
changedLines
)
)
{
continue
;
}
}
else
{
continue
;
}
}
$changed
=
$this
->
getPathChangedLines
(
$path
)
;
if
(
$changed
===
null
||
!
$location
[
'line'
]
)
{
return
true
;
}
$last_line
=
$location
[
'line'
]
;
if
(
isset
(
$location
[
'original'
]
)
)
{
$last_line
+=
substr_count
(
$location
[
'original'
]
,
"\n"
)
;
}
for
(
$l
=
$location
[
'line'
]
;
$l
<=
$last_line
;
$l
++
)
{
if
(
!
empty
(
$changed
[
$l
]
)
)
{
return
true
;
}
}
}
return
false
;
}
final
protected
function
getResultForPath
(
$path
)
{
if
(
empty
(
$this
->
results
[
$path
]
)
)
{
$result
=
new
ArcanistLintResult
(
)
;
$result
->
setPath
(
$path
)
;
$result
->
setCacheVersion
(
$this
->
cacheVersion
)
;
$this
->
results
[
$path
]
=
$result
;
}
return
$this
->
results
[
$path
]
;
}
protected
function
getCacheVersion
(
)
{
return
1
;
}
/**
* Get a named linter resource shared by another linter.
*
* This mechanism allows linters to share arbitrary resources, like the
* results of computation. If several linters need to perform the same
* expensive computation step, they can use a named resource to synchronize
* construction of the result so it doesn't need to be built multiple
* times.
*
* @param string Resource identifier.
* @param wild Optionally, default value to return if resource does not
* exist.
* @return wild Resource, or default value if not present.
*/
public
function
getLinterResource
(
$key
,
$default
=
null
)
{
return
idx
(
$this
->
linterResources
,
$key
,
$default
)
;
}
/**
* Set a linter resource that other linters can access.
*
* See @{method:getLinterResource} for a description of this mechanism.
*
* @param string Resource identifier.
* @param wild Resource.
* @return this
*/
public
function
setLinterResource
(
$key
,
$value
)
{
$this
->
linterResources
[
$key
]
=
$value
;
return
$this
;
}
private
function
getRunnableLinters
(
array
$linters
)
{
assert_instances_of
(
$linters
,
'ArcanistLinter'
)
;
// TODO: The canRun() mechanism is only used by one linter, and just
// silently disables the linter. Almost every other linter handles this
// by throwing `ArcanistMissingLinterException`. Both mechanisms are not
// ideal; linters which can not run should emit a message, get marked as
// "skipped", and allow execution to continue. See T7045.
$runnable
=
array
(
)
;
foreach
(
$linters
as
$key
=>
$linter
)
{
if
(
$linter
->
canRun
(
)
)
{
$runnable
[
$key
]
=
$linter
;
}
}
return
$runnable
;
}
private
function
executeLinters
(
array
$runnable
)
{
assert_instances_of
(
$runnable
,
'ArcanistLinter'
)
;
$all_paths
=
$this
->
getPaths
(
)
;
$path_chunks
=
array_chunk
(
$all_paths
,
32
,
$preserve_keys
=
true
)
;
$exception_lists
=
array
(
)
;
foreach
(
$path_chunks
as
$chunk
)
{
$exception_lists
[
]
=
$this
->
executeLintersOnChunk
(
$runnable
,
$chunk
)
;
}
return
array_mergev
(
$exception_lists
)
;
}
private
function
executeLintersOnChunk
(
array
$runnable
,
array
$path_list
)
{
assert_instances_of
(
$runnable
,
'ArcanistLinter'
)
;
$path_map
=
array_fuse
(
$path_list
)
;
$exceptions
=
array
(
)
;
$did_lint
=
array
(
)
;
foreach
(
$runnable
as
$linter
)
{
$linter_id
=
$linter
->
getLinterID
(
)
;
$paths
=
$linter
->
getPaths
(
)
;
foreach
(
$paths
as
$key
=>
$path
)
{
// If we aren't running this path in the current chunk of paths,
// skip it completely.
if
(
empty
(
$path_map
[
$path
]
)
)
{
unset
(
$paths
[
$key
]
)
;
continue
;
}
// Make sure each path has a result generated, even if it is empty
// (i.e., the file has no lint messages).
$result
=
$this
->
getResultForPath
(
$path
)
;
// If a linter has stopped all other linters for this path, don't
// actually run the linter.
if
(
isset
(
$this
->
stopped
[
$path
]
)
)
{
unset
(
$paths
[
$key
]
)
;
continue
;
}
// If we have a cached result for this path, don't actually run the
// linter.
if
(
isset
(
$this
->
cachedResults
[
$path
]
[
$this
->
cacheVersion
]
)
)
{
$cached_result
=
$this
->
cachedResults
[
$path
]
[
$this
->
cacheVersion
]
;
$use_cache
=
$this
->
shouldUseCache
(
$linter
->
getCacheGranularity
(
)
,
idx
(
$cached_result
,
'repository_version'
)
)
;
if
(
$use_cache
)
{
unset
(
$paths
[
$key
]
)
;
if
(
idx
(
$cached_result
,
'stopped'
)
==
$linter_id
)
{
$this
->
stopped
[
$path
]
=
$linter_id
;
}
}
}
}
$paths
=
array_values
(
$paths
)
;
if
(
!
$paths
)
{
continue
;
}
try
{
$this
->
executeLinterOnPaths
(
$linter
,
$paths
)
;
$did_lint
[
]
=
array
(
$linter
,
$paths
)
;
}
catch
(
Exception
$ex
)
{
$exceptions
[
]
=
$ex
;
}
}
foreach
(
$did_lint
as
$info
)
{
list
(
$linter
,
$paths
)
=
$info
;
try
{
$this
->
executeDidLintOnPaths
(
$linter
,
$paths
)
;
}
catch
(
Exception
$ex
)
{
$exceptions
[
]
=
$ex
;
}
}
return
$exceptions
;
}
private
function
beginLintServiceCall
(
ArcanistLinter
$linter
,
array
$paths
)
{
$profiler
=
PhutilServiceProfiler
::
getInstance
(
)
;
return
$profiler
->
beginServiceCall
(
array
(
'type'
=>
'lint'
,
'linter'
=>
$linter
->
getInfoName
(
)
,
'paths'
=>
$paths
,
)
)
;
}
private
function
endLintServiceCall
(
$call_id
)
{
$profiler
=
PhutilServiceProfiler
::
getInstance
(
)
;
$profiler
->
endServiceCall
(
$call_id
,
array
(
)
)
;
}
private
function
executeLinterOnPaths
(
ArcanistLinter
$linter
,
array
$paths
)
{
$call_id
=
$this
->
beginLintServiceCall
(
$linter
,
$paths
)
;
try
{
$linter
->
willLintPaths
(
$paths
)
;
foreach
(
$paths
as
$path
)
{
$linter
->
setActivePath
(
$path
)
;
$linter
->
lintPath
(
$path
)
;
if
(
$linter
->
didStopAllLinters
(
)
)
{
$this
->
stopped
[
$path
]
=
$linter
->
getLinterID
(
)
;
}
}
}
catch
(
Exception
$ex
)
{
$this
->
endLintServiceCall
(
$call_id
)
;
throw
$ex
;
}
$this
->
endLintServiceCall
(
$call_id
)
;
}
private
function
executeDidLintOnPaths
(
ArcanistLinter
$linter
,
array
$paths
)
{
$call_id
=
$this
->
beginLintServiceCall
(
$linter
,
$paths
)
;
try
{
$linter
->
didLintPaths
(
$paths
)
;
}
catch
(
Exception
$ex
)
{
$this
->
endLintServiceCall
(
$call_id
)
;
throw
$ex
;
}
$this
->
endLintServiceCall
(
$call_id
)
;
}
private
function
validateLintMessage
(
ArcanistLinter
$linter
,
ArcanistLintMessage
$message
)
{
$name
=
$message
->
getName
(
)
;
if
(
!
strlen
(
$name
)
)
{
throw
new
Exception
(
pht
(
'Linter "%s" generated a lint message that is invalid because it '
.
'does not have a name. Lint messages must have a name.'
,
get_class
(
$linter
)
)
)
;
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Jan 19 2025, 20:45 (6 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1128565
Default Alt Text
ArcanistLintEngine.php (14 KB)
Attached To
Mode
rARC Arcanist
Attached
Detach File
Event Timeline
Log In to Comment