import * as vscode from 'vscode' ; import { Class , Type , Field , Method } from './language/structs' ; import { parseSmaliDocument , findClassName } from './language/parser' ; import { SmaliDocumentSymbolProvider } from './symbol' ; import { SmaliHoverProvider } from './hover' ; import { SmaliDefinitionProvider } from './definition' ; import { SmaliReferenceProvider } from './reference' ; import { SmaliRenameProvider } from './rename' ; import LRUCache = require ( 'lru-cache' ) ; import { loadPublicXml } from './service/resourceService' ; let loading ; let diagnostics ; const classes = new LRUCache ( { length : ( value ) => value . text . length } ) ; const identifiers = new Map ( ) ; export function activate ( context ) { const configuration = vscode . workspace . getConfiguration ( 'smalise' ) ; const configCacheMemoryLimit = configuration . get ( 'cache.memoryLimit' ) || 128 ; classes . max = configCacheMemoryLimit * 1024 * 1024 ; loading = new Promise ( ( resolve , reject ) => { vscode . workspace . findFiles ( '**/*.smali' ) . then ( files => { events . onSmaliDocumentsCreated ( files ) . then ( resolve ) . catch ( reject ) ; } ) ; vscode . workspace . findFiles ( "res/values/public.xml" ) . then ( publicXml => { if ( publicXml . length != 0 ) loadPublicXml ( publicXml [ 0 ] ) ; } ) ; } ) ; loading . catch ( ( reason ) => { vscode . window . showErrorMessage ( `Smalise: Loading smali classes failed! ${ reason }` ) ; } ) ; diagnostics = vscode . languages . createDiagnosticCollection ( 'smali' ) ; context . subscriptions . push ( diagnostics ) ; context . subscriptions . push ( ... [ vscode . languages . registerHoverProvider ( { language : 'smali' } , new SmaliHoverProvider ( ) ) , vscode . languages . registerDocumentSymbolProvider ( { language : 'smali' } , new SmaliDocumentSymbolProvider ( ) ) , vscode . languages . registerDefinitionProvider ( { language : 'smali' } , new SmaliDefinitionProvider ( ) ) , vscode . languages . registerReferenceProvider ( { language : 'smali' } , new SmaliReferenceProvider ( ) ) , vscode . languages . registerRenameProvider ( { language : 'smali' } , new SmaliRenameProvider ( ) ) , ] ) ; context . subscriptions . push ( ... [ vscode . workspace . onDidCreateFiles ( event => events . onSmaliDocumentsCreated ( event . files ) ) , vscode . workspace . onDidRenameFiles ( event => events . onSmaliDocumentsRenamed ( event . files ) ) , vscode . workspace . onDidDeleteFiles ( event => events . onSmaliDocumentsRemoved ( event . files ) ) , vscode . workspace . onDidChangeTextDocument ( events . onSmaliDocumentsChanged ) , vscode . workspace . onDidChangeConfiguration ( events . onSmaliseConfigurationChanged ) , ] ) ; } export function deactivate ( ) { loading = undefined ; classes . reset ( ) ; identifiers . clear ( ) ; } namespace fs { export async function readFile ( file ) { const jclass = classes . get ( file . toString ( ) ) ; if ( jclass ) { return jclass . text ; } for ( const document of vscode . workspace . textDocuments ) { if ( file . toString ( ) === document . uri . toString ( ) ) { return document . getText ( ) ; } } return ( await vscode . workspace . fs . readFile ( file ) ) . toLocaleString ( ) ; } export async function searchFiles ( keywords ) { const files = [ ] ; for ( const [ file , _ ] of identifiers ) { const jclass = classes . get ( file ) ; if ( jclass ) { if ( keywords . some ( text => jclass . text . includes ( text ) ) ) { files . push ( file ) ; } continue ; } const content = await readFile ( vscode . Uri . parse ( file ) ) ; if ( keywords . some ( text => content . toLocaleString ( ) . includes ( text ) ) ) { files . push ( file ) ; } } return files ; } } namespace events { export async function onSmaliDocumentsCreated ( files ) { for ( const file of files ) { const content = await fs . readFile ( file ) ; const identifier = findClassName ( content ) ; identifiers . set ( file . toString ( ) , identifier ) ; } } export function onSmaliDocumentsRenamed ( files ) { for ( const file of files ) { if ( diagnostics ) { const diagnostic = diagnostics . get ( file . oldUri ) ; diagnostics . delete ( file . oldUri ) ; diagnostics . set ( file . newUri , diagnostic ) ; } const id = identifiers . get ( file . oldUri . toString ( ) ) ; if ( id ) { identifiers . delete ( file . oldUri . toString ( ) ) ; identifiers . set ( file . newUri . toString ( ) , id ) ; } const jclass = classes . get ( file . oldUri . toString ( ) ) ; if ( jclass ) { classes . del ( file . oldUri . toString ( ) ) ; classes . set ( file . newUri . toString ( ) , jclass ) ; } } } export function onSmaliDocumentsRemoved ( files ) { for ( const file of files ) { diagnostics ?. delete ( file ) ; classes . del ( file . toString ( ) ) ; identifiers . delete ( file . toString ( ) ) ; } } export function onSmaliDocumentsChanged ( event ) { smali . loadClass ( event . document ) ; } export function onSmaliseConfigurationChanged ( event ) { if ( event . affectsConfiguration ( 'smalise.cache.memoryLimit' ) ) { const configuration = vscode . workspace . getConfiguration ( 'smalise' ) ; const configCacheMemoryLimit = configuration . get ( 'cache.memoryLimit' ) || 128 ; classes . max = configCacheMemoryLimit * 1024 * 1024 ; } } } export namespace smali { export function loadClass ( document ) { if ( document . languageId !== 'smali' ) { return undefined ; } diagnostics ?. delete ( document . uri ) ; const cache = classes . get ( document . uri . toString ( ) ) ; if ( cache && cache . version >= document . version ) { return cache ; } try { const jclass = parseSmaliDocument ( document ) ; if ( jclass ) { classes . set ( document . uri . toString ( ) , jclass ) ; identifiers . set ( document . uri . toString ( ) , jclass . name . identifier ) ; } return jclass ; } catch ( err ) { if ( err instanceof vscode . Diagnostic ) { diagnostics ?. set ( document . uri , [ err ] ) ; } else { diagnostics ?. set ( document . uri , [ new vscode . Diagnostic ( new vscode . Range ( 0 , 0 , 0 , 0 ) , `Unexpected error: ${ err }` , vscode . DiagnosticSeverity . Error ) ] ) ; } } } export async function searchClasses ( identifier ) { if ( ! identifier ) { return [ ] ; } await loading ; const results = [ ] ; for ( const [ uri , id ] of identifiers ) { if ( id === identifier ) { const cached = classes . get ( uri ) ; if ( cached ) { results . push ( [ vscode . Uri . parse ( uri ) , cached ] ) ; } const document = await vscode . workspace . openTextDocument ( vscode . Uri . parse ( uri ) ) ; const loaded = loadClass ( document ) ; if ( loaded ) { results . push ( [ vscode . Uri . parse ( uri ) , loaded ] ) ; } } } return results ; } export async function searchRootClassIdsForMethod ( identifier , method , acceptPrivateMethod = true ) { if ( ! identifier ) { return [ ] ; } await loading ; const classes = await searchClasses ( identifier ) ; const roots = [ ] ; for ( const [ _ , jclass ] of classes ) { const parents = [ jclass . super , ... jclass . implements ] . map ( type => type . identifier ) ; for ( const parent of parents ) { roots . push ( ... await searchRootClassIdsForMethod ( parent , method , false ) ) ; } if ( roots . length === 0 ) { const methods = searchMethodDefinition ( jclass , method ) ; if ( methods . length !== 0 ) { if ( acceptPrivateMethod || methods . every ( it => ! it . modifiers . includes ( 'private' ) ) ) { return [ identifier ] ; } } } } return roots ; } export async function searchSuperClassIds ( identifier ) { if ( ! identifier ) { return [ ] ; } await loading ; const results = [ ] ; const classes = await searchClasses ( identifier ) ; for ( const [ _ , jclass ] of classes ) { const parents = [ jclass . super , ... jclass . implements ] . map ( type => type . identifier ! ) ; for ( const parent of parents ) { results . push ( ... await searchSuperClassIds ( parent ) ) ; } results . push ( ... parents ) ; } return results ; } export async function searchSubClassIds ( identifier ) { if ( ! identifier ) { return [ ] ; } await loading ; const results = [ ] ; const keywords = [ `.super ${ identifier }` , `.implements ${ identifier }` ] ; const children = ( await fs . searchFiles ( keywords ) ) . map ( uri => identifiers . get ( uri ) ! ) ; for ( const child of children ) { results . push ( ... await searchSubClassIds ( child ) ) ; } results . push ( ... children ) ; return results ; } export async function searchMemberAndEnclosedClassIds ( identifier ) { if ( ! identifier ) { return [ ] ; } await loading ; return Array . from ( classes . keys ( ) ) . filter ( key => key . startsWith ( `${ identifier . slice ( 0 , - 1 ) }$` ) ) ; } export function searchFieldDefinition ( jclass , field ) { return jclass . fields . filter ( f => field . equal ( f ) ) ; } export function searchMethodDefinition ( jclass , method ) { if ( method . isConstructor ) { return jclass . constructors . filter ( m => method . equal ( m ) ) ; } else { return jclass . methods . filter ( m => method . equal ( m ) ) ; } } function searchSymbols ( document , symbol , offset = 0 ) { const index = document . getText ( ) . indexOf ( symbol , offset ) ; if ( index !== - 1 ) { const start = document . positionAt ( index ) ; const end = document . positionAt ( index + symbol . length ) ; const location = new vscode . Location ( document . uri , new vscode . Range ( start , end ) ) ; return [ location , ... searchSymbols ( document , symbol , index + 1 ) ] ; } return [ ] ; } export async function searchSymbolReference ( symbols ) { await loading ; const locations = symbols . map ( ( ) => [ ] ) ; const files = await fs . searchFiles ( symbols ) ; for ( const file of files ) { const document = await vscode . workspace . openTextDocument ( vscode . Uri . parse ( file ) ) ; for ( const i in symbols ) { locations [ i ] . push ( ... searchSymbols ( document , symbols [ i ] ) ) ; } loadClass ( document ) ; } return locations ; } }