fileoverview author kazuya kawaguchi (a.k.a. kazupon)
  import type { AST as VAST } from 'vue-eslint-parser' ; import glob from 'glob' ; import { resolve , dirname , extname } from 'path' ; import { FileLocaleMessage , BlockLocaleMessage , LocaleMessages } from './locale-messages' ; import { CacheLoader } from './cache-loader' ; import { defineCacheFunction } from './cache-function' ; import type { RuleContext , TemplateListener , RuleListener , LocaleKeyType , SettingsVueI18nLocaleDir , SettingsVueI18nLocaleDirObject , SettingsVueI18nLocaleDirGlob , CustomBlockVisitorFactory } from '../types' ; import * as jsoncESLintParser from 'jsonc-eslint-parser' ; import * as yamlESLintParser from 'yaml-eslint-parser' ; import { getCwd } from './get-cwd' ; interface LocaleFiles { files : string [ ] ; localeKey : LocaleKeyType ; localePattern ? : string | RegExp ; } const UNEXPECTED_ERROR_LOCATION = { line : 1 , column : 0 } ; see https export function defineTemplateBodyVisitor ( context , templateBodyVisitor , scriptVisitor ? ) { if ( context . parserServices . defineTemplateBodyVisitor == null ) { const filename = context . getFilename ( ) ; if ( extname ( filename ) === '.vue' ) { context . report ( { loc : UNEXPECTED_ERROR_LOCATION , message : 'Use the latest vue-eslint-parser. See also https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error' } ) ; } return { } ; } return context . parserServices . defineTemplateBodyVisitor ( templateBodyVisitor , scriptVisitor ) ; } param { VElement } node param { string } name param { string } value returns { VAttribute | null } export function getAttribute ( node , name ) { return ( node . startTag . attributes . map ( node => ( ! node . directive ? node : null ) ) . find ( node => { return node && node . key . name === name ; } ) || null ) ; } param { VElement } node param { string } name param { string } argument returns { VDirective | null } export function getDirective ( node , name , argument ) { return ( node . startTag . attributes . map ( node => ( node . directive ? node : null ) ) . find ( node => { return ( node && node . key . name . name === name && ( argument === undefined || ( node . key . argument && node . key . argument . type === 'VIdentifier' && node . key . argument . name ) === argument ) ) ; } ) || null ) ; } function loadLocaleMessages ( localeFilesList , cwd ) { const results = [ ] ; const checkDupeMap = { } ; for ( const { files , localeKey , localePattern } of localeFilesList ) { for ( const file of files ) { const localeKeys = checkDupeMap [ file ] || ( checkDupeMap [ file ] = [ ] ) ; if ( localeKeys . includes ( localeKey ) ) { continue ; } localeKeys . push ( localeKey ) ; const fullpath = resolve ( cwd , file ) ; results . push ( new FileLocaleMessage ( { fullpath , localeKey , localePattern } ) ) ; } } return results ; } type { Set < RuleContext > } const puttedSettingsError = new WeakSet < RuleContext > ( ) ; param { RuleContext } context returns { LocaleMessages } export function getLocaleMessages ( context , options ? ) { const { settings } = context ; type { SettingsVueI18nLocaleDir | null } const localeDir = ( settings [ 'vue-i18n' ] && settings [ 'vue-i18n' ] . localeDir ) || null ; const documentFragment = context . parserServices . getDocumentFragment && context . parserServices . getDocumentFragment ( ) ; type { VElement [ ] } const i18nBlocks = ( documentFragment && documentFragment . children . filter ( ( node ) : node is VAST . VElement => node . type === 'VElement' && node . name === 'i18n' ) ) || [ ] ; if ( ! localeDir && ! i18nBlocks . length ) { if ( ! puttedSettingsError . has ( context ) && ! options ?. ignoreMissingSettingsError ) { context . report ( { loc : UNEXPECTED_ERROR_LOCATION , message : `You need to set 'localeDir' at 'settings', or '<i18n>' blocks. See the 'eslint-plugin-vue-i18n' documentation` } ) ; puttedSettingsError . add ( context ) ; } return new LocaleMessages ( [ ] ) ; } return new LocaleMessages ( [ ... ( getLocaleMessagesFromI18nBlocks ( context , i18nBlocks ) || [ ] ) , ... ( ( localeDir && localeDirLocaleMessagesCache . getLocaleMessagesFromLocaleDir ( context , localeDir ) ) || [ ] ) ] ) ; } class LocaleDirLocaleMessagesCache { private _targetFilesLoader : CacheLoader < [ string , string ] , string [ ] > ; private _loadLocaleMessages : ( localeFilesList : LocaleFiles [ ] , cwd : string ) => FileLocaleMessage [ ] ; constructor ( ) { this . _targetFilesLoader = new CacheLoader ( ( pattern , cwd ) => glob . sync ( pattern , { cwd } ) ) ; this . _loadLocaleMessages = defineCacheFunction ( ( localeFilesList , cwd ) => { return loadLocaleMessages ( localeFilesList , cwd ) ; } ) ; } param { SettingsVueI18nLocaleDir } localeDir returns { LocaleMessage [ ] } getLocaleMessagesFromLocaleDir ( context , localeDir ) { const cwd = getCwd ( context ) ; let localeFilesList ; if ( Array . isArray ( localeDir ) ) { localeFilesList = localeDir . map ( dir => this . _toLocaleFiles ( dir , cwd ) ) ; } else { localeFilesList = [ this . _toLocaleFiles ( localeDir , cwd ) ] ; } return this . _loadLocaleMessages ( localeFilesList , cwd ) ; } private _toLocaleFiles ( localeDir , cwd ) { const targetFilesLoader = this . _targetFilesLoader ; if ( typeof localeDir === 'string' ) { return { files : targetFilesLoader . get ( localeDir , cwd ) , localeKey : 'file' } ; } else { return { files : targetFilesLoader . get ( localeDir . pattern , cwd ) , localeKey : String ( localeDir . localeKey ?? 'file' ) as LocaleKeyType , localePattern : localeDir . localePattern } ; } } } const localeDirLocaleMessagesCache = new LocaleDirLocaleMessagesCache ( ) ; type { Map < Program , LocaleMessage [ ] > } const i18nBlockLocaleMessages = new WeakMap ( ) ; param { RuleContext } context param { VElement [ ] } i18nBlocks returns { LocaleMessage [ ] } function getLocaleMessagesFromI18nBlocks ( context , i18nBlocks ) { const sourceCode = context . getSourceCode ( ) ; let localeMessages = i18nBlockLocaleMessages . get ( sourceCode . ast ) ; if ( localeMessages ) { return localeMessages ; } const filename = context . getFilename ( ) ; localeMessages = i18nBlocks . map ( block => { const attrs = getStaticAttributes ( block ) ; let localeMessage = null ; if ( attrs . src ) { const fullpath = resolve ( dirname ( filename ) , attrs . src ) ; if ( attrs . locale ) { localeMessage = new FileLocaleMessage ( { fullpath , locales : [ attrs . locale ] , localeKey : 'file' } ) ; } else { localeMessage = new FileLocaleMessage ( { fullpath , localeKey : 'key' } ) ; } } else if ( block . endTag ) { if ( attrs . locale ) { localeMessage = new BlockLocaleMessage ( { block , fullpath : filename , locales : [ attrs . locale ] , localeKey : 'file' , context , lang : attrs . lang } ) ; } else { localeMessage = new BlockLocaleMessage ( { block , fullpath : filename , localeKey : 'key' , context , lang : attrs . lang } ) ; } } if ( localeMessage ) { return localeMessage ; } return null ; } ) . filter ( e => e ) ; i18nBlockLocaleMessages . set ( sourceCode . ast , localeMessages ) ; return localeMessages ; } export function defineCustomBlocksVisitor ( context , jsonRule , yamlRule ) { if ( ! context . parserServices . defineCustomBlocksVisitor ) { return { } ; } const jsonVisitor = context . parserServices . defineCustomBlocksVisitor ( context , jsoncESLintParser , { target ( lang , block ) { if ( block . name !== 'i18n' ) { return false ; } return ! lang || lang === 'json' || lang === 'json5' ; } , create : jsonRule } ) ; const yamlVisitor = context . parserServices . defineCustomBlocksVisitor ( context , yamlESLintParser , { target ( lang , block ) { if ( block . name !== 'i18n' ) { return false ; } return lang === 'yaml' || lang === 'yml' ; } , create : yamlRule } ) ; return compositingVisitors ( jsonVisitor , yamlVisitor ) ; } export type VueObjectType = 'mark' | 'export' | 'definition' | 'instance' | 'variable' | 'components-option' ; param context param node returns export function getVueObjectType ( context , node ) { if ( node . type !== 'ObjectExpression' || ! node . parent ) { return null ; } const parent = node . parent ; if ( parent . type === 'ExportDefaultDeclaration' ) { const ext = extname ( context . getFilename ( ) ) . toLowerCase ( ) ; if ( ( ext === '.vue' || ext === '.jsx' || ! ext ) && skipTSAsExpression ( parent . declaration ) === node ) { const scriptSetup = getScriptSetupElement ( context ) ; if ( scriptSetup && scriptSetup . range [ 0 ] <= parent . range [ 0 ] && parent . range [ 1 ] <= scriptSetup . range [ 1 ] ) { return null ; } return 'export' ; } } else if ( parent . type === 'CallExpression' ) { if ( getVueComponentDefinitionType ( node ) != null && skipTSAsExpression ( parent . arguments . slice ( - 1 ) [ 0 ] ) === node ) { return 'definition' ; } } else if ( parent . type === 'NewExpression' ) { if ( isVueInstance ( parent ) && skipTSAsExpression ( parent . arguments [ 0 ] ) === node ) { return 'instance' ; } } else if ( parent . type === 'VariableDeclarator' ) { if ( parent . init === node && parent . id . type === 'Identifier' && /^[A-Z][a-zA-Z\d]+/u . test ( parent . id . name ) && parent . id . name . toUpperCase ( ) !== parent . id . name ) { return 'variable' ; } } else if ( parent . type === 'Property' ) { const componentsCandidate = parent . parent as VAST . ESLintObjectExpression ; const pp = componentsCandidate . parent ; if ( pp && pp . type === 'Property' && pp . value === componentsCandidate && ! pp . computed && ( pp . key . type === 'Identifier' ? pp . key . name : pp . key . type === 'Literal' ? pp . key . value + '' : '' ) === 'components' ) { return 'components-option' ; } } if ( getComponentComments ( context ) . some ( el => el . loc . end . line === node . loc . start . line - 1 ) ) { return 'mark' ; } return null ; } param context returns export function getScriptSetupElement ( context ) { const df = context . parserServices . getDocumentFragment && context . parserServices . getDocumentFragment ( ) ; if ( ! df ) { return null ; } const scripts = df . children . filter ( isVElement ) . filter ( e => e . name === 'script' ) ; if ( scripts . length === 2 ) { return scripts . find ( e => getAttribute ( e , 'setup' ) ) || null ; } else { const script = scripts [ 0 ] ; if ( script && getAttribute ( script , 'setup' ) ) { return script ; } } return null ; } param node export function isVElement ( node ) { return node . type === 'VElement' ; } param node export function isI18nBlock ( node ) { return isVElement ( node ) && node . name === 'i18n' ; } param element export function getStaticAttributes ( element ) { const attrs = { } ; for ( const attr of element . startTag . attributes ) { if ( ! attr . directive && attr . value ) { attrs [ attr . key . name ] = attr . value . value ; } } return attrs ; } template T param node returns export function skipTSAsExpression < T extends VAST . Node > ( node ) { if ( ! node ) { return node ; } if ( node . type === 'TSAsExpression' ) { return skipTSAsExpression ( node . expression ) ; } return node ; } function compositingVisitors ( visitor , ... visitors ) { for ( const v of visitors ) { for ( const key in v ) { if ( visitor [ key ] ) { const o = visitor [ key ] as any ; visitor [ key ] = ( ... args ) => { o ( ... args ) ; ( v [ key ] as any ) ( ... args ) ; } ; } else { visitor [ key ] = v [ key ] ; } } } return visitor ; } param node returns { 'component' | 'mixin' | 'extend' | 'createApp' | 'defineComponent' | null } function getVueComponentDefinitionType ( node ) { const parent = node . parent ; if ( parent && parent . type === 'CallExpression' ) { const callee = parent . callee ; if ( callee . type === 'MemberExpression' ) { const calleeObject = skipTSAsExpression ( callee . object ) ; if ( calleeObject . type === 'Identifier' ) { const propName = ! callee . computed && callee . property . type === 'Identifier' && callee . property . name ; if ( calleeObject . name === 'Vue' ) { const maybeFullVueComponentForVue2 = propName && isObjectArgument ( parent ) ; return maybeFullVueComponentForVue2 && ( propName === 'component' || propName === 'mixin' || propName === 'extend' ) ? propName : null ; } const maybeFullVueComponent = propName && isObjectArgument ( parent ) ; return maybeFullVueComponent && ( propName === 'component' || propName === 'mixin' ) ? propName : null ; } } if ( callee . type === 'Identifier' ) { if ( callee . name === 'component' ) { const isDestructedVueComponent = isObjectArgument ( parent ) ; return isDestructedVueComponent ? 'component' : null ; } if ( callee . name === 'createApp' ) { const isAppVueComponent = isObjectArgument ( parent ) ; return isAppVueComponent ? 'createApp' : null ; } if ( callee . name === 'defineComponent' ) { const isDestructedVueComponent = isObjectArgument ( parent ) ; return isDestructedVueComponent ? 'defineComponent' : null ; } } } return null ; function isObjectArgument ( node ) { return ( node . arguments . length > 0 && skipTSAsExpression ( node . arguments . slice ( - 1 ) [ 0 ] ) . type === 'ObjectExpression' ) ; } } param node function isVueInstance ( node ) { const callee = node . callee ; return Boolean ( node . type === 'NewExpression' && callee . type === 'Identifier' && callee . name === 'Vue' && node . arguments . length && skipTSAsExpression ( node . arguments [ 0 ] ) . type === 'ObjectExpression' ) ; } const componentComments = new WeakMap < RuleContext , VAST . Token [ ] > ( ) ; param context return function getComponentComments ( context ) { let tokens = componentComments . get ( context ) ; if ( tokens ) { return tokens ; } const sourceCode = context . getSourceCode ( ) ; tokens = sourceCode . getAllComments ( ) . filter ( comment => /@vue\/component/g . test ( comment . value ) ) ; componentComments . set ( context , tokens ) ; return tokens ; }