New: Add rule to validate JSLL usage

This commit is contained in:
Cătălin Mariș 2018-01-16 17:20:23 -08:00 коммит произвёл Anton Molleda
Родитель fbeaf8dce2
Коммит 9a19620035
18 изменённых файлов: 11602 добавлений и 2 удалений

3
package-lock.json сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

17
rules/jsll/.sonarwhalrc Normal file
Просмотреть файл

@ -0,0 +1,17 @@
{
"connector": {
"name": "jsdom",
"options": {
"waitFor": 1000
}
},
"formatters": "stylish",
"parsers": ["javascript"],
"rulesTimeout": 120000,
"rules": {
"jsll-script-included": "error",
"jsll-awa-init": "error",
"jsll-optional-config": "warning",
"jsll-required-config": "error"
}
}

116
rules/jsll/README.md Normal file
Просмотреть файл

@ -0,0 +1,116 @@
# jsll (`jsll`)
Validate the inclusion and initialization of the JSLL script via
multiple related rules.
## What does the rule check?
JSLL is the analytics library used by Microsoft. This package
contains the following rules:
* `jsll-script-included`
* `jsll-awa-init`
* `jsll-required-config`
* `jsll-optional-config`
These rules test the following:
* Whether or not the JSLL script has been included.
* If the script is inlcuded in `<head>`.
* If the JSLL script is included before any other scripts.
* If the script version is valid.
* Whether or not the JSLL init script has been included.
* If the init script is placed immediately after the JSLL script.
* If `awa.init` was used to initialize JSLL.
* If `config` was defined.
* Validate required properties.
* Validate Optional properties.
### Examples that **trigger** the rule
The JSLL script was not included
```html
<head>
...
</head>
```
The JSLL init script was not included.
```html
<head>
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
</head>
```
The JSLL init script doesn't follow the JSLL script immediately.
```html
<head>
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
<script>var a = 1;</script>
<script>
config = {...};
awa.init(config);
</script>
</head>
```
Required/Optional Properties are missing in `config`.
```html
<head>
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
<script>
awa.init({});
</script>
</head>
```
Invalid required/optional properties.
```html
<head>
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
<script>
var config = {
autoCapture: true // invalid type
coreData: {
appId: 'YourAppId',
env: 'env',
market: 'oo-oo', // invalid code
pageName: 'name',
pageType: 'type'
},
useShortNameForContentBlob: true
};
awa.init(config);
</script>
</head>
```
### Examples that **pass** the rule
```html
<head>
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
<script>
var config = {
autoCapture: {
lineage: true,
scroll: true
},
coreData: {
appId: 'YourAppId',
env: 'env',
market: 'en-us',
pageName: 'name',
pageType: 'type'
},
useShortNameForContentBlob: true
};
awa.init(config);
</script>
</head>
```

10317
rules/jsll/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

68
rules/jsll/package.json Normal file
Просмотреть файл

@ -0,0 +1,68 @@
{
"author": "",
"ava": {
"babel": {
"presets": []
},
"concurrency": 5,
"failFast": false,
"files": [
"dist/tests/**/*.js"
],
"timeout": "1m"
},
"description": "Validate the initialization of the JSLL script.",
"devDependencies": {
"@types/debug": "0.0.30",
"@types/node": "8.0.14",
"async-retry": "^1.1.4",
"ava": "^0.24.0",
"babel-cli": "^6.26.0",
"babel-plugin-istanbul": "^4.1.5",
"babel-preset-es2017": "^6.22.0",
"babel-register": "^6.26.0",
"cpx": "^1.5.0",
"eslint": "^4.15.0",
"eslint-plugin-markdown": "^1.0.0-beta.6",
"eslint-plugin-typescript": "^0.8.1",
"express": "^4.16.2",
"markdownlint-cli": "^0.6.0",
"npm-run-all": "^4.1.2",
"on-headers": "^1.0.1",
"rimraf": "^2.6.2",
"sonarwhal": "^0.22.1",
"typescript": "^2.6.2",
"typescript-eslint-parser": "^12.0.0"
},
"engines": {
"node": ">=8.0.0"
},
"keywords": [
"rule",
"sonarwhal"
],
"main": "dist/src/index.js",
"name": "sonarwhal-rule-jsll",
"scripts": {
"build": "npm run clean && npm-run-all build:*",
"build:assets": "cpx \"./{src,tests}/**/{!(*.ts),.!(ts)}\" dist",
"build:ts": "tsc --outDir \"./dist\" --rootDir .",
"clean": "rimraf dist",
"test": "npm run lint && npm run build && ava",
"lint": "npm-run-all lint:*",
"lint:js": "eslint --ext md --ext ts --ignore-pattern dist .",
"lint:md": "markdownlint README.md",
"watch:ts": "npm run build:ts -- --watch",
"init": "npm install && npm run build",
"sonarwhal": "node node_modules/sonarwhal/dist/src/bin/sonarwhal.js"
},
"peerDependencies": {
"sonarwhal": "^0.22.1"
},
"repository": "https://github.com/Microsoft/sonarwhal-microsoft-rules",
"version": "0.1.0",
"dependencies": {
"locale-code": "^1.1.1",
"lodash.clonedeep": "^4.5.0"
}
}

10
rules/jsll/src/index.ts Normal file
Просмотреть файл

@ -0,0 +1,10 @@
/**
* @fileoverview jsll the initialization of the JSLL script.
*/
module.exports = {
'jsll-awa-init': require('./rules/jsll-awa-init/jsll-awa-init'),
'jsll-optional-config': require('./rules/jsll-optional-config/jsll-optional-config'),
'jsll-required-config': require('./rules/jsll-required-config/jsll-required-config'),
'jsll-script-included': require('./rules/jsll-script-included/jsll-script-included')
};

Просмотреть файл

@ -0,0 +1,122 @@
/**
* @fileoverview Validate the use of `awa.init` to initialize the JSLL script.
*/
import { Category } from 'sonarwhal/dist/src/lib/enums/category';
import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context';
import { IRule, IAsyncHTMLElement, IRuleBuilder, IElementFound, IScriptParse, ITraverseUp } from 'sonarwhal/dist/src/lib/types';
import { debug as d } from 'sonarwhal/dist/src/lib/utils/debug';
import { validateAwaInit } from '../validator';
import { isJsllDir, isHeadElement } from '../utils';
import { Linter } from 'eslint';
const debug: debug.IDebugger = d(__filename);
/*
* ------------------------------------------------------------------------------
* Public
* ------------------------------------------------------------------------------
*/
const rule: IRuleBuilder = {
create(context: RuleContext): IRule {
const linter = new Linter();
let validated: boolean = false; // If `validateAwaInit` has run.
let hasJSLLScript: boolean = false; // If link to JSLL scripts has been included.
linter.defineRule('jsll-awa-init', {
create(eslintContext) {
let isFirstExpressionStatement: boolean = true;
return {
ExpressionStatement(node) {
if (!isFirstExpressionStatement) { // Only the first expression statement should be checked.
return;
}
isFirstExpressionStatement = false;
validateAwaInit(node, eslintContext, true);
validated = true;
},
'Program:exit'(programNode) {
if (hasJSLLScript && (!validated)) {
// Should verify init but no ExpressionStatement was encountered.
// e.g.:
// ...
// <script src="../jsll-4.js"></script>
// <script>var a = 2;</script>
eslintContext.report(programNode, `JSLL is not initialized with "awa.init(config)" function. Initialization script should be placed immediately after JSLL script.`);
}
}
};
}
});
const validateScript = async (scriptParse: IScriptParse) => {
if (!hasJSLLScript) {
return;
}
const results = linter.verify(scriptParse.sourceCode, { rules: { 'jsll-awa-init': 'error' } });
hasJSLLScript = false;
// Only validates the script included immediately after the JSLL link.
// So flip this flag to avoid duplicates.
for (const result of results) {
await context.report(scriptParse.resource, null, result.message);
}
};
const enableInitValidate = (data: IElementFound) => {
const { element }: { element: IAsyncHTMLElement, resource: string } = data;
if (hasJSLLScript) {
return;
}
// JSLL script has been included at this point.
// Now JSLL init needs to be verified.
hasJSLLScript = isJsllDir(element);
};
const validateInit = async (event: ITraverseUp) => {
const { resource }: { resource: string } = event;
if (!isHeadElement(event.element)) {
return;
}
if (hasJSLLScript && (!validated)) {
// Should verify init but no script tag was encountered after the JSLL link.
// e.g.:
// <head>
// <script src="../jsll-4.js"></script>
// </head>
await context.report(resource, null, `JSLL is not initialized with "awa.init(config)" function. Initialization script should be placed immediately after JSLL script.`);
}
};
return {
'element::script': enableInitValidate,
'parse::javascript': validateScript,
'traverse::up': validateInit
};
},
meta: {
docs: {
category: Category.other,
description: `Validate the use of 'awa.init' to initialize the JSLL script.`
},
recommended: false,
schema: [],
worksWithLocalFiles: true
}
};
module.exports = rule;

Просмотреть файл

@ -0,0 +1,98 @@
/**
* @fileoverview Validate the required optional properties.
*/
import { Category } from 'sonarwhal/dist/src/lib/enums/category';
import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context';
import { IRule, IAsyncHTMLElement, IRuleBuilder, IElementFound, IScriptParse } from 'sonarwhal/dist/src/lib/types';
import { debug as d } from 'sonarwhal/dist/src/lib/utils/debug';
import { validateNodeProps, validateAwaInit, configProps } from '../validator';
import { isJsllDir } from '../utils';
import { Linter } from 'eslint';
const debug: debug.IDebugger = d(__filename);
/*
* ------------------------------------------------------------------------------
* Public
* ------------------------------------------------------------------------------
*/
const rule: IRuleBuilder = {
create(context: RuleContext): IRule {
const linter = new Linter();
let hasJSLLScript: boolean = false; // If link to JSLL scripts has been included.
linter.defineRule('jsll-required-config', {
create(eslintContext) {
let isFirstExpressionStatement = true;
return {
ExpressionStatement(node) {
if (!isFirstExpressionStatement) { // Only the first expression statement should be checked.
return;
}
isFirstExpressionStatement = false;
const configNode = validateAwaInit(node, eslintContext, false);
if (!configNode) {
return;
}
const category = `optional`;
validateNodeProps(configProps.config[category], configNode, category, eslintContext);
}
};
}
});
const validateScript = async (scriptParse: IScriptParse) => {
if (!hasJSLLScript) {
return;
}
const results = linter.verify(scriptParse.sourceCode, { rules: { 'jsll-required-config': 'warn' } });
hasJSLLScript = false;
// Only the script included immediately after the JSLL link needs to be validated.
// So flip the flag.
for (const result of results) {
await context.report(scriptParse.resource, null, result.message);
}
};
const enableInitValidate = (data: IElementFound) => {
const { element }: { element: IAsyncHTMLElement, resource: string } = data;
if (hasJSLLScript) {
return;
}
// JSLL init needs to be verified if JSLL script was included.
hasJSLLScript = isJsllDir(element);
};
return {
'element::script': enableInitValidate,
'parse::javascript': validateScript
};
},
meta: {
docs: {
category: Category.other,
description: `Validate the optional config properties.`
},
recommended: false,
schema: [],
worksWithLocalFiles: true
}
};
module.exports = rule;

Просмотреть файл

@ -0,0 +1,98 @@
/**
* @fileoverview Validate the required config properties.
*/
import { Category } from 'sonarwhal/dist/src/lib/enums/category';
import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context';
import { IRule, IAsyncHTMLElement, IRuleBuilder, IElementFound, IScriptParse } from 'sonarwhal/dist/src/lib/types';
import { debug as d } from 'sonarwhal/dist/src/lib/utils/debug';
import { validateNodeProps, validateAwaInit, configProps } from '../validator';
import { isJsllDir } from '../utils';
import { Linter } from 'eslint';
const debug: debug.IDebugger = d(__filename);
/*
* ------------------------------------------------------------------------------
* Public
* ------------------------------------------------------------------------------
*/
const rule: IRuleBuilder = {
create(context: RuleContext): IRule {
const linter = new Linter();
let initValidate = false; // If JSLL initialization should be verified, only the script immediately after the JSLL should be verified.
linter.defineRule('jsll-required-config', {
create(eslintContext) {
let isFirstExpressionStatement = true;
return {
ExpressionStatement(node) {
if (!isFirstExpressionStatement) {
// Only the first expression statement should be checked.
return;
}
isFirstExpressionStatement = false;
const configNode = validateAwaInit(node, eslintContext, false);
if (!configNode) {
return;
}
const category = `required`;
validateNodeProps(configProps.config[category], configNode, category, eslintContext);
}
};
}
});
const validateScript = async (scriptParse: IScriptParse) => {
if (!initValidate) {
return;
}
const results = linter.verify(scriptParse.sourceCode, { rules: { 'jsll-required-config': 'error' } });
initValidate = false;
// Only the script included immediately after the JSLL link needs to be validated.
// So flip the flag.
for (const result of results) {
await context.report(scriptParse.resource, null, result.message);
}
};
const enableInitValidate = (data: IElementFound) => {
const { element }: { element: IAsyncHTMLElement, resource: string } = data;
if (initValidate) {
return;
}
// JSLL init needs to be verified if JSLL script was included.
initValidate = isJsllDir(element);
};
return {
'element::script': enableInitValidate,
'parse::javascript': validateScript
};
},
meta: {
docs: {
category: Category.other,
description: `Validate the required config properties.`
},
recommended: false,
schema: [],
worksWithLocalFiles: true
}
};
module.exports = rule;

Просмотреть файл

@ -0,0 +1,106 @@
/**
* @fileoverview This rule confirms that JSLL script is included in the page
*/
import { Category } from 'sonarwhal/dist/src/lib/enums/category';
import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context';
import { IAsyncHTMLElement, IRule, IRuleBuilder, IElementFound, ITraverseUp, Severity } from 'sonarwhal/dist/src/lib/types';
import { normalizeString } from 'sonarwhal/dist/src/lib/utils/misc';
import { isJsllDir } from '../utils';
/*
* ------------------------------------------------------------------------------
* Public
* ------------------------------------------------------------------------------
*/
const rule: IRuleBuilder = {
create(context: RuleContext): IRule {
// Messages.
const noScriptInHeadMsg: string = `No JSLL script was included in the <head> tag.`;
const redundantScriptInHeadMsg: string = `More than one JSLL scripts were included in the <head> tag.`;
const warningScriptVersionMsg: string = `Use the latest release of JSLL with 'jsll-4.js'. It is not recommended to specify the version number unless you wish to lock to a specific release.`;
const invalidScriptVersionMsg: string = `The jsll script versioning is not valid.`;
const wrongScriptOrderMsg: string = `The JSLL script isn't placed prior to other scripts.`;
const jsllDir: string = `https://az725175.vo.msecnd.net/scripts/jsll-`;
let isHead: boolean = true; // Flag to indicate if script is in head.
let totalHeadScriptCount: number = 0; // Total number of script tags in head.
let jsllScriptCount: number = 0; // Total number of JSLL script tag in head.
const validateScript = async (data: IElementFound) => {
const { element, resource }: { element: IAsyncHTMLElement, resource: string } = data;
const passRegex = new RegExp(`^(\\d+\\.)js`); // 4.js
const warningRegex = new RegExp(`^(\\d+\\.){2,}js`); // 4.2.1.js
if (!isHead) {
return;
}
totalHeadScriptCount += 1;
if (!isJsllDir(element)) {
return;
}
jsllScriptCount += 1;
if (jsllScriptCount > 1) {
await context.report(resource, element, redundantScriptInHeadMsg);
return;
}
if (totalHeadScriptCount > 1) {
// There are other scripts in <head> prior to this JSLL script.
await context.report(resource, element, wrongScriptOrderMsg);
return;
}
const fileName: string = normalizeString(element.getAttribute('src').replace(jsllDir, ''));
if (passRegex.test(fileName)) {
return;
}
if (warningRegex.test(fileName)) {
await context.report(resource, element, warningScriptVersionMsg, null, null, Severity.warning);
return;
}
await context.report(resource, element, invalidScriptVersionMsg);
return;
};
const enterBody = async (event: ITraverseUp) => {
const { resource }: { resource: string } = event;
if (jsllScriptCount === 0) {
await context.report(resource, null, noScriptInHeadMsg);
return;
}
isHead = false;
};
return {
'element::body': enterBody,
'element::script': validateScript
};
},
meta: {
docs: {
category: Category.other,
description: `This rule confirms that JSLL script is included in the head of the page`
},
recommended: false,
schema: [],
worksWithLocalFiles: false
}
};
module.exports = rule;

Просмотреть файл

@ -0,0 +1,46 @@
import { IAsyncHTMLElement } from 'sonarwhal/dist/src/lib/types';
import { normalizeString } from 'sonarwhal/dist/src/lib/utils/misc';
const jsllDir = `https://az725175.vo.msecnd.net/scripts/jsll-`;
const severityMatch = (parentObj: object, prop: string, severity: string) => {
return parentObj[severity].includes(prop);
};
const pluralizedVerb = (props: Array<string>) => {
return props.length > 1 ? 'are' : 'is';
};
const reportMissingProps = (props: Array<string>, existingProps: Array<string>, severity: string, target, eslintContext) => {
if (!props || !props.length) {
return;
}
const missingProps = props && props.filter((prop) => {
return !existingProps.includes(prop);
});
if (missingProps && missingProps.length) {
eslintContext.report(target, `${missingProps.join(', ')} ${pluralizedVerb(missingProps)} ${severity} but missing.`);
}
};
const isJsllDir = (element: IAsyncHTMLElement) => {
const src = element.getAttribute('src');
if (!src) {
return false;
}
return normalizeString(src).startsWith(jsllDir);
};
const isHeadElement = (element: IAsyncHTMLElement): boolean => {
return normalizeString(element.nodeName) === 'head';
};
export {
isHeadElement,
isJsllDir,
severityMatch,
reportMissingProps
};

Просмотреть файл

@ -0,0 +1,234 @@
import * as validateCode from 'locale-code';
import { severityMatch, reportMissingProps } from './utils';
/** List of types in the parsed Javascript. */
const types = {
identifier: 'Identifier',
object: 'ObjectExpression',
string: 'Literal'
};
/** Optional and required properties of 'config'. */
const config = {
optional: ['useShortNameForContentBlob'],
required: ['autoCapture', 'coreData']
};
/** Optional and required properties of 'coreData'. */
const coreData = {
optional: ['pageName', 'pageType', 'env', 'market'],
required: ['appId']
};
/** Optional properties of 'autoCapture'. */
const autoCapture = { optional: ['scroll', 'lineage'] };
/** Validate the 'coreData' property. */
const validateCoreData = (property, eslintContext, severity: string) => {
const coreDataValue = property.value;
const report: boolean = config[severity].includes('coreData');
if (report && coreDataValue.type !== types.object) {
return eslintContext.report(coreDataValue, `The "coreData" property must be a valid object.`);
}
return validateNodeProps(coreData[severity], coreDataValue, severity, eslintContext); // eslint-disable-line typescript/no-use-before-define,no-use-before-define
};
/** Validate the 'autoCapture' property. */
const validateAutoCapture = (property, eslintContext, severity: string) => {
const report: boolean = severityMatch(config, 'autoCapture', severity);
const autoCaptureValue = property.value;
if (report && autoCaptureValue.type !== types.object) {
return eslintContext.report(autoCaptureValue, `The "autoCapture" property is not a valid object.`);
}
return validateNodeProps(autoCapture[severity], autoCaptureValue, severity, eslintContext); // eslint-disable-line typescript/no-use-before-define,no-use-before-define
};
/** Validate the 'useShortNameForContentBlob' property. */
const validateUseShortName = (property, eslintContext, severity: string) => {
if (!severityMatch(config, 'useShortNameForContentBlob', severity)) {
return;
}
const useShortNameValue = property.value;
if (!useShortNameValue || useShortNameValue.value !== true) {
eslintContext.report(property.value, `"useShortNameForContentBlob" parameter is not set to true.`);
}
};
/** Validate the 'appId' property. */
const validateAppId = (property, eslintContext, severity: string) => {
if (!severityMatch(coreData, 'appId', severity)) {
return;
}
const id = property.value;
if (id.type !== types.string || !id.value || !id.value.length) {
eslintContext.report(property.value, `The "appId" must be a non-empty string.`);
}
};
/** Validate the 'lineage' property. */
const validateLineage = (property, eslintContext, severity: string) => {
if (!severityMatch(autoCapture, 'lineage', severity)) {
return;
}
const lineageValue = property.value;
if (!lineageValue || lineageValue.value !== true) {
eslintContext.report(property.value, `"lineage" parameter is not set to true.`);
}
};
/** Validate the 'market' property. */
const validateMarket = (property, eslintContext, severity: string) => {
if (!severityMatch(coreData, 'market', severity)) {
return;
}
const marketValue = property.value;
if (!marketValue) {
eslintContext.report(marketValue, `"market" parameter needs to be defined.`);
return;
}
const regex: RegExp = /[a-z]*-[a-z]*/;
if (!regex.test(marketValue.value)) {
eslintContext.report(marketValue, `The format of "market" parameter is not valid.`);
return;
}
const [languageCode, countryCode] = marketValue.value.split('-');
const denormalizedCode: string = `${languageCode}-${countryCode.toUpperCase()}`;
// The validator doesn't recognize lowercase country codes.
if (!validateCode.validateLanguageCode(denormalizedCode)) {
eslintContext.report(property.value, `The "market" parameter contains invalid language code "${languageCode}".`);
}
if (!validateCode.validateLanguageCode(denormalizedCode)) {
eslintContext.report(property.value, `The "market" parameter contains invalid country code "${countryCode}".`);
}
};
/** Validate the initialization of JSLL using `awa.init(config)`. */
export const validateAwaInit = (node, eslintContext, report: boolean) => {
const expression = node.expression;
const scope = eslintContext.getScope();
const variables = scope.variables;
const { callee } = expression;
let configNode;
if (!callee || !callee.object || callee.object.name !== 'awa' || callee.property.name !== 'init') {
if (report) {
eslintContext.report(node, 'JSLL is not initialized with "awa.init(config)" function. Initialization script should be placed immediately after JSLL script.');
}
return null;
}
const initArgs = expression.arguments;
if (initArgs.length < 1) {
if (report) {
eslintContext.report(node, `JSLL initialization function "awa.init(config)" missing required parameter "config".`);
}
return null;
}
if (initArgs.length > 1) {
if (report) {
eslintContext.report(node, `"init" arguments can't take more than one arguments.`);
}
return null;
}
const configVal = initArgs[0];
if (!['Identifier', 'ObjectExpression'].includes(configVal.type)) {
if (report) {
eslintContext.report(configVal, `The argument of "awa.init" is not of type Object.`);
}
return null;
}
if (configVal.type === types.identifier) { // e.g., awa.init(config);
const configDefinition = variables.find((variable) => {
return variable.name === configVal.name;
});
if (!configDefinition) {
if (report) {
eslintContext.report(node, `${configVal.name} is not defined.`);
}
return null;
}
configNode = configDefinition.defs[0].node.init;
if (configNode.type !== types.object) {
if (report) {
eslintContext.report(configNode, `${configDefinition.name} is not of type Object.`);
}
return null;
}
} else {
configNode = configVal; // awa.init({...});
}
return configNode;
};
/** List of validators. */
const validators = {
appId: validateAppId,
autoCapture: validateAutoCapture,
coreData: validateCoreData,
lineage: validateLineage,
market: validateMarket,
useShortNameForContentBlob: validateUseShortName
};
/** Validate properties of the current node based on severity. */
export const validateNodeProps = (expectedProps: Array<string>, target, severity: string, eslintContext) => { // eslint-disable-line consistent-return
if (!expectedProps || !expectedProps.length) {
return;
}
const properties = target.properties;
const existingProps = [];
properties.forEach((property) => {
const key = property.key.name || property.key.value;
const validator = validators[key];
if (validator) {
validator(property, eslintContext, severity);
}
existingProps.push(key);
});
reportMissingProps(expectedProps, existingProps, severity, target, eslintContext);
};
export const configProps = {
autoCapture,
config,
coreData,
types
};

Просмотреть файл

@ -0,0 +1,79 @@
import * as cloneDeep from 'lodash.cloneDeep';
/** Look for the target property recursively and modify/delete the value */
export const modifyValue = (obj, targetProp: string, targetValue) => {
if (!(obj instanceof Object) || Array.isArray(obj)) {
return;
}
const isDelete: boolean = (typeof targetValue === 'undefined') || (targetValue === null);
if (obj.hasOwnProperty(targetProp)) {
if (isDelete) {
delete obj[targetProp];
} else {
obj[targetProp] = targetValue;
}
} else {
const props: Array<string> = Object.keys(obj);
props.forEach((childProp) => {
modifyValue(obj[childProp], targetProp, targetValue);
});
}
};
export const perfectConfig = {
autoCapture: {
lineage: true,
scroll: true
},
coreData: {
appId: 'YourAppId',
env: 'env',
market: 'en-us',
pageName: 'name',
pageType: 'type'
},
useShortNameForContentBlob: true
};
export const code = {
emptyObjconfig: `awa.init({});`,
initConfig: `awa.init(config);`,
jsllScript: `<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>`,
noConfigArgs: `awa.init();`,
notImmediateInitNoFn: `var a = 1;</script><script>awa.init({})`,
notImmediateInithasFn: `console.log('a');</script><script>awa.init({})`
};
/** Delete one or more properties. */
export const deleteProp = (prop: string | Array<string>): string => {
const missiongPropConfig = cloneDeep(perfectConfig);
const props: Array<string> = Array.isArray(prop) ? prop : [prop];
props.forEach((property) => {
modifyValue(missiongPropConfig, property, null);
});
return `var config=${JSON.stringify(missiongPropConfig)};`;
};
/** Modify the value of a (nested property). */
export const modifyConfigVal = (targetProp: string, targetValue: any): string => {
const modifiedConfig = cloneDeep(perfectConfig);
modifyValue(modifiedConfig, targetProp, targetValue);
return `var config=${JSON.stringify(modifiedConfig)};`;
};
export const scriptWrapper = (config: string, initCode: string, includeJSLLScript: boolean = true): string => {
let res = `${includeJSLLScript ? code.jsllScript : ''}`;
if (config || initCode) {
res += `<script>${config || ''}${initCode || ''}</script>`;
}
return res;
};

Просмотреть файл

@ -0,0 +1,49 @@
import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc';
import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers';
import { IRuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type';
import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner';
import { code, scriptWrapper } from '../helpers/common';
const ruleName = getRuleName(__dirname);
const messages = {
noConfigArgs: `JSLL initialization function "awa.init(config)" missing required parameter "config".`,
noInit: `JSLL is not initialized with "awa.init(config)" function. Initialization script should be placed immediately after JSLL script.`
};
const tests: Array<IRuleTest> = [
{
// Validate init shouldn't run if the JSLL script link is not included.
name: `The JSLL script itself was not included`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn, false)}`)
},
{
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has no function calls`,
reports: [{ message: messages.noInit }],
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInitNoFn)}`)
},
{
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has function calls`,
reports: [{ message: messages.noInit }],
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn)}`)
},
{
name: `"awa.init" doesn't have the required parameter "config"`,
reports: [{ message: messages.noConfigArgs }],
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.noConfigArgs)}`)
},
{
name: `"awa.init" has an empty object as config parameter`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.emptyObjconfig)}`)
},
{
// <head>
// <script src="../jsll-4.js"></script>
// </head>
name: `No script tages are encountered after the JSLL script link`,
reports: [{ message: messages.noInit }],
serverConfig: generateHTMLPage(`${scriptWrapper(null, null, true)}`)
}
];
ruleRunner.testRule(ruleName, tests, { parsers: ['javascript'] });

Просмотреть файл

@ -0,0 +1,84 @@
import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc';
import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers';
import { IRuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type';
import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner';
import { code, deleteProp, modifyConfigVal, scriptWrapper } from '../helpers/common';
const ruleName = getRuleName(__dirname);
const messages = {
invalidCountryCode: `The "market" parameter contains invalid country code "oo".`,
invalidLangCode: `The "market" parameter contains invalid language code "oo".`,
invalidLineage: `"lineage" parameter is not set to true.`,
invalidMarketValue: `The format of "market" parameter is not valid.`,
invalidUseShortNameForContentBlob: `"useShortNameForContentBlob" parameter is not set to true.`,
missingLineage: `lineage" parameter is not set to true.`,
missingOptionalConfigProp: `useShortNameForContentBlob is optional but missing.`,
missingPageName: `pageName is optional but missing.`,
missingPageNameAndEnv: `pageName, env are optional but missing.`,
missingUseShortNameForContentBlob: `useShortNameForContentBlob is optional but missing.`
};
const tests: Array<IRuleTest> = [
{
// Validate init shouldn't run if the JSLL script link is not included.
name: `The JSLL script itself was not included`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn, false)}`)
},
{
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has no function calls`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInitNoFn)}`)
},
{
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has function calls`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn)}`)
},
{
name: `"awa.init" doesn't have any arguments`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.noConfigArgs)}`)
},
// All the tests beyond this point should pass, because they will be reported in rule `jsll-awa-init`.
{
name: `"config" misses optional properties`,
reports: [{ message: messages.missingOptionalConfigProp }],
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp('useShortNameForContentBlob'), code.initConfig)}`)
},
{
name: `"market" has an invalid value`,
reports: [{ message: messages.invalidMarketValue }],
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('market', 'market'), code.initConfig)}`)
},
{
name: `"market" has an invalid lauguage/country code`,
reports: [{ message: messages.invalidLangCode }, { message: messages.invalidCountryCode }],
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('market', 'oo-oo'), code.initConfig)}`)
},
{
name: `"pageName" is missing`, // optional child property (pageName) of a required parent property (coreData)
reports: [{ message: messages.missingPageName }],
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp('pageName'), code.initConfig)}`)
},
{
name: `"lineage" is not set as true`,
reports: [{ message: messages.invalidLineage }],
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('lineage', 1), code.initConfig)}`)
},
{
name: `"useShortNameForContentBlob" is not set as true`,
reports: [{ message: messages.invalidUseShortNameForContentBlob }],
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('useShortNameForContentBlob', false), code.initConfig)}`)
},
{
name: `"config" is an empty object`,
reports: [{ message: messages.missingUseShortNameForContentBlob }],
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.emptyObjconfig)}`)
},
{
name: `Both required and optional properties are missing in "config"`,
reports: [{ message: messages.missingPageNameAndEnv }],
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp(['appId', 'pageName', 'env']), code.initConfig)}`)
}
];
ruleRunner.testRule(ruleName, tests, { parsers: ['javascript'] });

Просмотреть файл

@ -0,0 +1,86 @@
import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc';
import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers';
import { IRuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type';
import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner';
import { code, deleteProp, modifyConfigVal, scriptWrapper } from '../helpers/common';
const ruleName = getRuleName(__dirname);
const messages = {
invalidAppId: `The "appId" must be a non-empty string.`,
invalidAutoCapture: `The "autoCapture" property is not a valid object.`,
missingAppId: `appId is required but missing.`,
missingAutoCapture: `autoCapture is required but missing.`,
missingRequiredConfigProps: `autoCapture, coreData are required but missing.`
};
const stringPropertyConfig = {
'autoCapture': { // eslint-disable-line quote-props
lineage: true,
scroll: true
},
coreData: {
'appId': '1', // eslint-disable-line quote-props
env: 'env',
market: 'en-us',
pageName: 'name',
pageType: 'type'
},
useShortNameForContentBlob: true
};
const tests: Array<IRuleTest> = [
{
// Validate init shouldn't run if the JSLL script link is not included.
name: `The JSLL script itself was not included`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn, false)}`)
},
{
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has no function calls`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInitNoFn)}`)
},
{
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has function calls`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn)}`)
},
{
name: `"awa.init" doesn't have the required parameter "config"`,
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.noConfigArgs)}`)
},
// All the tests beyond this point should pass, because they will be reported in rule `jsll-awa-init`.
{
name: `"config" misses required properties "autoCapture"`,
reports: [{ message: messages.missingAutoCapture }],
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp('autoCapture'), code.initConfig)}`)
},
{
name: `"appId" is missing`,
reports: [{ message: messages.missingAppId }],
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp('appId'), code.initConfig)}`)
},
{
name: `"appId" is not a string`,
reports: [{ message: messages.invalidAppId }],
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('appId', 1), code.initConfig)}`)
},
{
name: `"autoCapture" is not a valid object`,
reports: [{ message: messages.invalidAutoCapture }],
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('autoCapture', true), code.initConfig)}`)
},
{
name: `"config" is an empty object`,
reports: [{ message: messages.missingRequiredConfigProps }],
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.emptyObjconfig)}`)
},
{
name: `"config" properties are strings instead of identifiers`,
serverConfig: generateHTMLPage(`${scriptWrapper(`var config=${JSON.stringify(stringPropertyConfig)};`, code.initConfig)}`)
},
{
name: `Both required and optional properties are missing in "config"`,
reports: [{ message: messages.missingAppId }],
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp(['appId', 'pageName', 'env']), code.initConfig)}`)
}
];
ruleRunner.testRule(ruleName, tests, { parsers: ['javascript'] });

Просмотреть файл

@ -0,0 +1,68 @@
import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc';
import { IRuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type';
import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers';
import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner';
const ruleName: string = getRuleName(__dirname);
const baseUrl: string = `https://az725175.vo.msecnd.net/scripts/jsll`;
const simpleVersionLink: string = `${baseUrl}-4.js`;
const specifiedVersionLink: string = `${baseUrl}-4.2.1.js`;
const invalidVersionLink: string = `${baseUrl}-4-2-1.js`;
const redundantScriptLinks: Array<string> = [`${baseUrl}-4.js`, `${baseUrl}-4.2.1.js`];
const wrongScriptOrderLinks: Array<string> = [`https://uhf.microsoft.com/mscc/statics/mscc-0.3.6.min.js`, `${baseUrl}-4.js`];
const generateScript = (links: string | Array<string>) => {
let generatedScript = '';
const scriptLinks = Array.isArray(links) ? links : [links];
scriptLinks.forEach((link) => {
generatedScript += `<script src="${link}"></script>`;
});
return generatedScript;
};
// Messages.
const noScriptInHeadMsg = `No JSLL script was included in the <head> tag.`;
const redundantScriptInHeadMsg = `More than one JSLL scripts were included in the <head> tag.`;
const warningScriptVersionMsg = `Use the latest release of JSLL with 'jsll-4.js'. It is not recommended to specify the version number unless you wish to lock to a specific release.`;
const invalidScriptVersionMsg = `The jsll script versioning is not valid.`;
const wrongScriptOrderMsg = `The JSLL script isn't placed prior to other scripts.`;
const tests: Array<IRuleTest> = [
{
name: 'JSLL script locates in the <head> tag and has the recommended version format',
serverConfig: generateHTMLPage(generateScript(simpleVersionLink))
},
{
name: 'Multiple JSLL scripts are included in the page',
reports: [{ message: redundantScriptInHeadMsg }],
serverConfig: generateHTMLPage(generateScript(redundantScriptLinks))
},
{
name: 'JSLL script locates in the <body> tag instead of the <head> tag',
reports: [{ message: noScriptInHeadMsg }],
serverConfig: generateHTMLPage(null, generateScript(simpleVersionLink))
},
{
name: 'JSLL script name has the specified version format',
reports: [{ message: warningScriptVersionMsg }],
serverConfig: generateHTMLPage(generateScript(specifiedVersionLink))
},
{
name: 'JSLL script name has an invalid version in name',
reports: [{ message: invalidScriptVersionMsg }],
serverConfig: generateHTMLPage(generateScript(invalidVersionLink))
},
{
name: 'JSLL script is not placed prior to other scripts',
reports: [{ message: wrongScriptOrderMsg }],
serverConfig: generateHTMLPage(generateScript(wrongScriptOrderLinks))
},
{
name: 'JSLL script placed prior to other scripts',
serverConfig: generateHTMLPage(generateScript(wrongScriptOrderLinks.reverse()))
}
];
ruleRunner.testRule(ruleName, tests);

Просмотреть файл

@ -17,7 +17,6 @@
"node_modules"
],
"include": [
"src/**/*.ts",
"tests/**/*.ts"
"**/*.ts"
]
}