import fs from "fs" ; import findUp from "find-up" ; import memoize from "lodash.memoize" ; import { lintCallExpression , lintMemberExpression , lintNewExpression , lintExpressionStatement , parseBrowsersListVersion , determineTargetsFromConfig , } from "../helpers" ; import { ESLintNode , AstMetadataApiWithTargetsResolver , BrowserListConfig , HandleFailingRule , Context , } from "../types" ; import { nodes } from "../providers" ; type ESLint = { [ astNodeTypeName ] : ( node : ESLintNode ) => void ; } ; function getName ( node ) { switch ( node . type ) { case "NewExpression" : { return node . callee . name ; } case "MemberExpression" : { return node . object . name ; } case "ExpressionStatement" : { return node . expression . name ; } case "CallExpression" : { return node . callee . name ; } default : throw new Error ( "not found" ) ; } } function generateErrorName ( rule ) { if ( rule . name ) return rule . name ; if ( rule . property ) return `${ rule . object }.${ rule . property }()` ; return rule . object ; } const getPolyfillSet = memoize ( ( polyfillArrayJSON ) : Set < String > => new Set ( JSON . parse ( polyfillArrayJSON ) ) ) ; function isPolyfilled ( context , rule ) { if ( ! context . settings ?. polyfills ) return false ; const polyfills = getPolyfillSet ( JSON . stringify ( context . settings . polyfills ) ) ; return ( polyfills . has ( rule . id ) || polyfills . has ( rule . protoChainId ) || polyfills . has ( rule . protoChain [ 0 ] ) ) ; } const items = [ "babel.config.json" , "babel.config.js" , "babel.config.cjs" , ".babelrc" , ".babelrc.json" , ".babelrc.js" , ".babelrc.cjs" , "tsconfig.json" , ] ; TODO babel param dir function isUsingTranspiler ( context ) { if ( context . parserOptions ?. tsconfigRootDir === true ) return true ; const dir = context . getFilename ( ) ; const configPath = findUp . sync ( items , { cwd : dir , } ) ; if ( configPath ) return true ; const pkgPath = findUp . sync ( "package.json" , { cwd : dir , } ) ; if ( pkgPath ) { const pkg = JSON . parse ( fs . readFileSync ( pkgPath ) . toString ( ) ) ; return ! ! pkg . babel ; } return false ; } type RulesFilteredByTargets = { CallExpression : AstMetadataApiWithTargetsResolver [ ] ; NewExpression : AstMetadataApiWithTargetsResolver [ ] ; MemberExpression : AstMetadataApiWithTargetsResolver [ ] ; ExpressionStatement : AstMetadataApiWithTargetsResolver [ ] ; } ; /**
 * A small optimization that only lints APIs that are not supported by targeted browsers.
 * For example, if the user is targeting chrome 50, which supports the fetch API, it is
 * wasteful to lint calls to fetch.
 */ const getRulesForTargets = memoize ( ( targetsJSON , lintAllEsApis ) : RulesFilteredByTargets => { const result = { CallExpression : [ ] , NewExpression : [ ] , MemberExpression : [ ] , ExpressionStatement : [ ] , } ; const targets = JSON . parse ( targetsJSON ) ; nodes . filter ( ( node ) => { return lintAllEsApis ? true : node . kind !== "es" ; } ) . forEach ( ( node ) => { if ( ! node . getUnsupportedTargets ( node , targets ) . length ) return ; result [ node . astNodeType as keyof RulesFilteredByTargets ] . push ( node ) ; } ) ; return result ; } ) ; export default { meta : { docs : { description : "Ensure cross-browser API compatibility" , category : "Compatibility" , url : "https://github.com/amilajack/eslint-plugin-compat/blob/master/docs/rules/compat.md" , recommended : true , } , type : "problem" , schema : [ { type : "string" } ] , } , create ( context ) { const browserslistConfig = context . settings ?. browsers || context . settings ?. targets || context . options [ 0 ] ; const lintAllEsApis = context . settings ?. lintAllEsApis === true || ( ! context . settings ?. polyfills ?. includes ( "es:all" ) && ! isUsingTranspiler ( context ) ) ; const browserslistTargets = parseBrowsersListVersion ( determineTargetsFromConfig ( context . getFilename ( ) , browserslistConfig ) ) ; const targetedRules = getRulesForTargets ( JSON . stringify ( browserslistTargets ) , lintAllEsApis ) ; type Error = { message : string ; node : ESLintNode ; } ; const errors = [ ] ; const handleFailingRule = ( node , eslintNode ) => { if ( isPolyfilled ( context , node ) ) return ; errors . push ( { node : eslintNode , message : [ generateErrorName ( node ) , "is not supported in" , node . getUnsupportedTargets ( node , browserslistTargets ) . join ( ", " ) , ] . join ( " " ) , } ) ; } ; const identifiers = new Set ( ) ; return { CallExpression : lintCallExpression . bind ( null , context , handleFailingRule , targetedRules . CallExpression ) , NewExpression : lintNewExpression . bind ( null , context , handleFailingRule , targetedRules . NewExpression ) , ExpressionStatement : lintExpressionStatement . bind ( null , context , handleFailingRule , [ ... targetedRules . MemberExpression , ... targetedRules . CallExpression ] ) , MemberExpression : lintMemberExpression . bind ( null , context , handleFailingRule , [ ... targetedRules . MemberExpression , ... targetedRules . CallExpression , ... targetedRules . NewExpression , ] ) , Identifier ( node ) { if ( node . parent ) { const { type } = node . parent ; if ( type === "Property" || type === "FunctionDeclaration" || type === "VariableDeclarator" || type === "ClassDeclaration" || type === "ImportDefaultSpecifier" || type === "ImportSpecifier" || type === "ImportDeclaration" ) { identifiers . add ( node . name ) ; } } } , "Program:exit" : ( ) => { errors . filter ( ( error ) => ! identifiers . has ( getName ( error . node ) ) ) . forEach ( ( node ) => context . report ( node ) ) ; } , } ; } , } ;