diff --git a/README.md b/README.md index 93cbc17..3e0159f 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,24 @@ To use rules provided by the plugin, use the following: } ``` +Some rules are fixable with `eslint --fix`. For example the [place-fields](docs/rules/place-fields.md) rule. + +```js +service.getDetails({place_id: 'foo'}) +``` +becomes + +```js +service.getDetails({fields: /** TODO: Add necessary fields to the request */ [], place_id: 'foo'}) +``` + ## Rules | Rule | Description | Configurations | Type | | ------------------------------------------------------------ | ---------------------------------- | ---------------- | ------------ | | [no-api-keys](docs/rules/no-api-keys.md) | Keep API keys out of code. | ![recommended][] | ![suggest][] | | [place-fields](docs/rules/place-fields.md) | Always use place fields. | ![recommended][] | ![suggest][] | -| [require-js-api-loader](docs/rules/require-js-api-loader.md) | Require @googlemaps/js-api-loader. | ![recommended][] | ![suggest][] | +| [require-js-api-loader](docs/rules/require-js-api-loader.md) | Require @googlemaps/js-api-loader. | ![recommended][] | ![fixable][] | [recommended]: https://img.shields.io/badge/-recommended-lightgrey.svg [suggest]: https://img.shields.io/badge/-suggest-yellow.svg diff --git a/docs/rules/place-fields.md b/docs/rules/place-fields.md index dd5d2fd..62a4946 100644 --- a/docs/rules/place-fields.md +++ b/docs/rules/place-fields.md @@ -2,24 +2,32 @@ # place-fields -Use the `fields` option to limit the fields returned by the API and costs. Request to the Places API are billed by the fields that are returned. See [data-skus](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#data-skus) for more details. -> **Note**: This rule is not exhaustive and ignores `Autocomplete.setFields()`. +Use the `fields` option to limit the fields returned by the API and costs. Requests to the Places API are billed by the fields that are returned. See [Places Data SKUs](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#data-skus) for more details. + +More information about fields for specific API calls can be found at the following links: + +- [Place Details fields guidance](https://goo.gle/3H0TxxG) +- [Place Autocomplete fields guidance](https://goo.gle/3sp2XyS) + +> **Note**: This rule is not exhaustive. For example, it ignores `Autocomplete.setFields()`. 📋 This rule is enabled in `plugin:googlemaps/recommended`. +🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + ## Rule details ❌ Examples of **incorrect** code: ```js const service = new google.maps.places.PlacesService(); -const request = {place_id: 'foo'}; -service.getDetails(request) +service.getDetails({place_id: 'foo'}) const service = new google.maps.places.PlacesService(); - service.getDetails({}) +const request = {place_id: 'foo'}; +service.getDetails(request) const service = new google.maps.places.PlacesService(); -service.getDetails({...{bar: 'foo'}}) +service.getDetails({...{place_id: 'foo'}}) const service = new google.maps.places.Autocomplete(null, {}); const service = new google.maps.places.Autocomplete(null); @@ -47,6 +55,15 @@ service.getDetails(buildRequest()) const service = new google.maps.places.Autocomplete(null, {fields: ['place_id']}); ``` +🔧 Examples of code **fixed** by this rule: +```js +const service = new google.maps.places.PlacesService(); /* → */ const service = new google.maps.places.PlacesService(); +service.getDetails({place_id: 'foo'}) /* → */ service.getDetails({fields: /** TODO: Add necessary fields to the request */ [], place_id: 'foo'}) + +const service = new google.maps.places.PlacesService(); /* → */ const service = new google.maps.places.PlacesService(); +service.getDetails({...{place_id: 'foo'}}) /* → */ service.getDetails({fields: /** TODO: Add necessary fields to the request */ [], ...{place_id: 'foo'}}) +``` + ## Resources * [Rule source](/src/rules/place-fields.ts) diff --git a/package-lock.json b/package-lock.json index d7742a3..804ba09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1167,14 +1167,14 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.3.0.tgz", - "integrity": "sha512-NFVxYTjKj69qB0FM+piah1x3G/63WB8vCBMnlnEHUsiLzXSTWb9FmFn36FD9Zb4APKBLY3xRArOGSMQkuzTF1w==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.0.tgz", + "integrity": "sha512-KN5FvNH71bhZ8fKtL+lhW7bjm7cxs1nt+hrDZWIqb6ViCffQcWyLunGrgvISgkRojIDcXIsH+xlFfI4RCDA0xA==", "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.3.0", - "@typescript-eslint/types": "5.3.0", - "@typescript-eslint/typescript-estree": "5.3.0", + "@typescript-eslint/scope-manager": "5.8.0", + "@typescript-eslint/types": "5.8.0", + "@typescript-eslint/typescript-estree": "5.8.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" } @@ -1235,26 +1235,26 @@ } }, "@typescript-eslint/scope-manager": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.3.0.tgz", - "integrity": "sha512-22Uic9oRlTsPppy5Tcwfj+QET5RWEnZ5414Prby465XxQrQFZ6nnm5KnXgnsAJefG4hEgMnaxTB3kNEyjdjj6A==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.8.0.tgz", + "integrity": "sha512-x82CYJsLOjPCDuFFEbS6e7K1QEWj7u5Wk1alw8A+gnJiYwNnDJk0ib6PCegbaPMjrfBvFKa7SxE3EOnnIQz2Gg==", "requires": { - "@typescript-eslint/types": "5.3.0", - "@typescript-eslint/visitor-keys": "5.3.0" + "@typescript-eslint/types": "5.8.0", + "@typescript-eslint/visitor-keys": "5.8.0" } }, "@typescript-eslint/types": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.3.0.tgz", - "integrity": "sha512-fce5pG41/w8O6ahQEhXmMV+xuh4+GayzqEogN24EK+vECA3I6pUwKuLi5QbXO721EMitpQne5VKXofPonYlAQg==" + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.8.0.tgz", + "integrity": "sha512-LdCYOqeqZWqCMOmwFnum6YfW9F3nKuxJiR84CdIRN5nfHJ7gyvGpXWqL/AaW0k3Po0+wm93ARAsOdzlZDPCcXg==" }, "@typescript-eslint/typescript-estree": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.0.tgz", - "integrity": "sha512-FJ0nqcaUOpn/6Z4Jwbtf+o0valjBLkqc3MWkMvrhA2TvzFXtcclIM8F4MBEmYa2kgcI8EZeSAzwoSrIC8JYkug==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.0.tgz", + "integrity": "sha512-srfeZ3URdEcUsSLbkOFqS7WoxOqn8JNil2NSLO9O+I2/Uyc85+UlfpEvQHIpj5dVts7KKOZnftoJD/Fdv0L7nQ==", "requires": { - "@typescript-eslint/types": "5.3.0", - "@typescript-eslint/visitor-keys": "5.3.0", + "@typescript-eslint/types": "5.8.0", + "@typescript-eslint/visitor-keys": "5.8.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -1263,11 +1263,11 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.0.tgz", - "integrity": "sha512-oVIAfIQuq0x2TFDNLVavUn548WL+7hdhxYn+9j3YdJJXB7mH9dAmZNJsPDa7Jc+B9WGqoiex7GUDbyMxV0a/aw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.0.tgz", + "integrity": "sha512-+HDIGOEMnqbxdAHegxvnOqESUH6RWFRR2b8qxP1W9CZnnYh4Usz6MBL+2KMAgPk/P0o9c1HqnYtwzVH6GTIqug==", "requires": { - "@typescript-eslint/types": "5.3.0", + "@typescript-eslint/types": "5.8.0", "eslint-visitor-keys": "^3.0.0" } }, diff --git a/package.json b/package.json index e5db83b..705507f 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "prepare": "tsc", "format": "eslint --ext .js,.ts src --fix", "pretest": "eslint --ext .js,.ts src", - "test": "jest" + "test": "jest --detectOpenHandles" }, "dependencies": { - "@typescript-eslint/experimental-utils": "^5.2.0", - "@typescript-eslint/scope-manager": "^5.2.0" + "@typescript-eslint/experimental-utils": "^5.8.0", + "@typescript-eslint/scope-manager": "^5.8.0" }, "devDependencies": { "@types/eslint": "^8.2.1", diff --git a/src/rules/no-api-keys.test.ts b/src/rules/no-api-keys.test.ts index 51196fe..b5ebd1a 100644 --- a/src/rules/no-api-keys.test.ts +++ b/src/rules/no-api-keys.test.ts @@ -32,7 +32,21 @@ new RuleTester({ invalid: [ { code: 'const apiKey = "AIza00000000000000000000000000000000000";', - errors: [{ messageId }], + errors: [ + { + messageId, + suggestions: [ + { + messageId: "replaceWithEnvVar", + output: "const apiKey = process.env.GOOGLE_MAPS_API_KEY;", + }, + { + messageId: "replaceWithPlaceholder", + output: 'const apiKey = "YOUR_API_KEY";', + }, + ], + }, + ], }, ], }); diff --git a/src/rules/no-api-keys.ts b/src/rules/no-api-keys.ts index 6badb40..12aa9fd 100644 --- a/src/rules/no-api-keys.ts +++ b/src/rules/no-api-keys.ts @@ -40,7 +40,10 @@ export default createRule({ }, messages: { [messageId]: "Avoid placing API keys in source code.", + replaceWithEnvVar: "Use environment variables instead.", + replaceWithPlaceholder: "Use placeholder `YOUR_API_KEY` instead.", }, + hasSuggestions: true, schema: [], type: "suggestion", }, @@ -52,6 +55,23 @@ export default createRule({ context.report({ node, messageId, + suggest: [ + { + messageId: "replaceWithEnvVar", + fix: (fixer) => { + return fixer.replaceText( + node, + `process.env.GOOGLE_MAPS_API_KEY` + ); + }, + }, + { + messageId: "replaceWithPlaceholder", + fix: (fixer) => { + return fixer.replaceText(node, `"YOUR_API_KEY"`); + }, + }, + ], }); } }, diff --git a/src/rules/place-fields.test.ts b/src/rules/place-fields.test.ts index b29cc12..cdd7c6f 100644 --- a/src/rules/place-fields.test.ts +++ b/src/rules/place-fields.test.ts @@ -45,19 +45,23 @@ service.getDetails(buildRequest())`, // getDetails { code: `const service = new google.maps.places.PlacesService(); -const request = {place_id: 'foo'}; -service.getDetails(request)`, +service.getDetails({place_id: 'foo'})`, errors: [{ messageId }], + output: `const service = new google.maps.places.PlacesService(); +service.getDetails({fields: /** TODO: Add necessary fields to the request */ [], place_id: 'foo'})`, }, { code: `const service = new google.maps.places.PlacesService(); - service.getDetails({})`, +const request = {place_id: 'foo'}; +service.getDetails(request)`, errors: [{ messageId }], }, { code: `const service = new google.maps.places.PlacesService(); -service.getDetails({...{bar: 'foo'}})`, +service.getDetails({...{place_id: 'foo'}})`, errors: [{ messageId }], + output: `const service = new google.maps.places.PlacesService(); +service.getDetails({fields: /** TODO: Add necessary fields to the request */ [], ...{place_id: 'foo'}})`, }, // Autocomplete { diff --git a/src/rules/place-fields.ts b/src/rules/place-fields.ts index 81c3008..d440223 100644 --- a/src/rules/place-fields.ts +++ b/src/rules/place-fields.ts @@ -14,14 +14,21 @@ * limitations under the License. */ -import { TSESTree } from "@typescript-eslint/experimental-utils"; +import { TSESLint, TSESTree } from "@typescript-eslint/experimental-utils"; import { Reference } from "@typescript-eslint/scope-manager"; import { createRule, camelCased } from "../utils/rules"; export const messageId = camelCased(__filename); -const description = `Use the \`fields\` option to limit the fields returned by the API and costs. Request to the Places API are billed by the fields that are returned. See [data-skus](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#data-skus) for more details. -> **Note**: This rule is not exhaustive and ignores \`Autocomplete.setFields()\`.`; +const description = `Use the \`fields\` option to limit the fields returned by the API and costs. Requests to the Places API are billed by the fields that are returned. See [Places Data SKUs](https://developers.google.com/maps/documentation/places/web-service/usage-and-billing#data-skus) for more details. + +More information about fields for specific API calls can be found at the following links: + +- [Place Details fields guidance](https://goo.gle/3H0TxxG) +- [Place Autocomplete fields guidance](https://goo.gle/3sp2XyS) + +> **Note**: This rule is not exhaustive. For example, it ignores \`Autocomplete.setFields()\`.`; + export default createRule({ name: __filename, meta: { @@ -35,6 +42,7 @@ export default createRule({ }, schema: [], type: "suggestion", + fixable: "code", }, defaultOptions: [], create: (context) => { @@ -94,6 +102,19 @@ export default createRule({ context.report({ messageId, node: node.property, + fix: + requestArgument.type === "ObjectExpression" + ? (fixer: TSESLint.RuleFixer) => { + return [ + fixer.insertTextBefore( + context + .getSourceCode() + .getTokens(requestArgument)[1], + `fields: /** TODO: Add necessary fields to the request */ [], ` + ), + ]; + } + : null, }); } }