Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Add line/col information for JSON #1297

Closed
wants to merge 12 commits into from
Closed
9 changes: 6 additions & 3 deletions packages/hint-babel-config/src/is-valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ export default class BabelConfigIsValidHint implements IHint {
};

const invalidSchema = async (fetchEnd: BabelConfigInvalidSchema) => {
const { prettifiedErrors, resource } = fetchEnd;
const { errors, prettifiedErrors, resource } = fetchEnd;

debug(`parse::babel-config::error::schema received`);

for (const error of prettifiedErrors) {
await context.report(resource, null, error);
for (let i = 0; i < errors.length; i++) {
const message = prettifiedErrors[i];
const location = errors[i].location;

await context.report(resource, null, message, null, location);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
{
"name": 'example',
"version": "0.0.1"
}
invalidJson
4 changes: 2 additions & 2 deletions packages/hint-babel-config/tests/is-valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const tests: Array<HintLocalTest> = [
{
name: `Invalid json file should fail`,
path: path.join(__dirname, 'fixtures', 'invalid-json', 'package.json'),
reports: [{ message: `Unexpected token ' in JSON at position 12` }]
reports: [{ message: `Unexpected token i in JSON at position 0` }]
},
{
name: `If package.json doesn't contain "babel" property, it should pass`,
Expand Down Expand Up @@ -55,7 +55,7 @@ const tests: Array<HintLocalTest> = [
name: 'If .babelrc contains an invalid extends, it should fail',
path: path.join(__dirname, 'fixtures', 'invalid-extends'),
reports: [
{ message: `Unexpected token ' in JSON at position 191` }
{ message: `Unexpected token ' in JSON at position 202` }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alrra and @molant, I changed this because the position was off on my machine, but it looks like the original value was expected in CI. Upon further review, it seems this position is different when running on Windows. I suspect this is due to Windows using \r\n instead of \n for newlines as the difference is exactly the same as the number of lines before the token.

I can (1) simply put this back to the old value to pass CI, (2) update the test (and similar tests) to not depend on the exact position, or (3) update how we're loading the test fixture files so we always use \n, making the offsets match cross-OS.

My preference is (3), but I'd like your opinions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you are checking out your files with \r\n in your machine?
What do you think of:

  • Make sure those filed don't have \r\n
  • Change your options to "Checkout as-is, commit Unix-style": git config --global core.autocrlf input if I understand the guide correctly although maybe there's a way to enforce this in the project's configuration in .gitattributes?

I don't think that should cause any issue with VS Code and will not need any code changes on your side. We might

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this by adding eol=lf to .gitattributes in 3a58198. This ensures files are checked out with LF even on Windows (the previous setting only ensured that they were committed to the repo with LF, checkout to the working directory was still controlled by user settings).

Copy link
Contributor

@alrra alrra Sep 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My recommendation is to not use eol=lf as it will bite users in the future. Just use https://nodejs.org/api/os.html#os_os_eol, or something like that to figure out how many characters you need to add.

Copy link
Member Author

@antross antross Sep 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My recommendation is to not use eol=lf as it will bite users in the future.

Ok, I'll remove it.

I'll have to use something slightly different than os.EOL to avoid breaking the test case for @molant who already has user settings to checkout with LF even on Windows...

I can load the actual JSON test file separately and check for \r\n to alter the position if that's fine with you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My recommendation is to not use eol=lf as it will bite users in the future.

I'm curious about this. Why do you think this can bite users in the future? All Windows editors (including notepad) support this line ending and not having this option has already bite us.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All Windows editors (including notepad) support this line ending and not having this option has already bite us.

Ok.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take that as we're keeping eol=lf

Yes.

]
}
];
Expand Down
25 changes: 11 additions & 14 deletions packages/hint-manifest-app-name/src/hint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@
import { ucs2 } from 'punycode';

import { Category } from 'hint/dist/src/lib/enums/category';
import { IHint, HintMetadata } from 'hint/dist/src/lib/types';
import {
Manifest,
ManifestParsed
} from '@hint/parser-manifest/dist/src/types';
import { IHint, HintMetadata, IJSONLocationFunction } from 'hint/dist/src/lib/types';
import { ManifestParsed } from '@hint/parser-manifest/dist/src/types';
import { HintContext } from 'hint/dist/src/lib/hint-context';
import { HintScope } from 'hint/dist/src/lib/enums/hintscope';

Expand Down Expand Up @@ -46,15 +43,15 @@ export default class ManifestAppNameHint implements IHint {
}
};

const checkIfPropertyValueIsNotEmpty = async (resource: string, content: string, propertyName: string) => {
const checkIfPropertyValueIsNotEmpty = async (resource: string, content: string, propertyName: string, getLocation: IJSONLocationFunction) => {
if (content && (content.trim() === '')) {
await context.report(resource, null, `Web app manifest should have non-empty '${propertyName}' property value.`);
await context.report(resource, null, `Web app manifest should have non-empty '${propertyName}' property value.`, null, getLocation(propertyName));
}
};

const checkIfPropertyValueIsUnderLimit = async (resource: string, content: string, propertyName: string, shortNameLengthLimit: number) => {
const checkIfPropertyValueIsUnderLimit = async (resource: string, content: string, propertyName: string, shortNameLengthLimit: number, getLocation: IJSONLocationFunction) => {
if (content && (ucs2.decode(content).length > shortNameLengthLimit)) {
await context.report(resource, null, `Web app manifest should have '${propertyName}' property value under ${shortNameLengthLimit} characters.`);
await context.report(resource, null, `Web app manifest should have '${propertyName}' property value under ${shortNameLengthLimit} characters.`, null, getLocation(propertyName));

return false;
}
Expand All @@ -63,7 +60,7 @@ export default class ManifestAppNameHint implements IHint {
};

const validate = async (manifestParsed: ManifestParsed) => {
const { parsedContent: manifest, resource }: { parsedContent: Manifest, resource: string } = manifestParsed;
const { getLocation, parsedContent: manifest, resource } = manifestParsed;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add the types back? We are trying to go full strict (#576) and I think this is needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? TypeScript fully infers those types correctly so I'm surprised that they'd be required...

I might try a local test case first just to make sure this is necessary in full strict mode - I hate unnecessarily redundant type definitions. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've had some problems in the past believing we were getting one type when it was actually another, or when we changed the return type and nothing complained and then hell happen...


const name = manifest.name;

Expand Down Expand Up @@ -100,8 +97,8 @@ export default class ManifestAppNameHint implements IHint {
const shortNameLengthLimit: number = 12;

await checkIfPropertyExists(resource, name, 'name');
await checkIfPropertyValueIsNotEmpty(resource, name, 'name');
await checkIfPropertyValueIsUnderLimit(resource, name, 'name', nameLengthLimit);
await checkIfPropertyValueIsNotEmpty(resource, name, 'name', getLocation);
await checkIfPropertyValueIsUnderLimit(resource, name, 'name', nameLengthLimit, getLocation);

const shortName: string = manifest.short_name;
const shortNameIsRequired: boolean = name && (name.trim() !== '') && (ucs2.decode(name).length > shortNameLengthLimit);
Expand All @@ -118,8 +115,8 @@ export default class ManifestAppNameHint implements IHint {
}

await checkIfPropertyExists(resource, shortName, 'short_name');
await checkIfPropertyValueIsNotEmpty(resource, shortName, 'short_name');
await checkIfPropertyValueIsUnderLimit(resource, shortName, 'short_name', shortNameLengthLimit);
await checkIfPropertyValueIsNotEmpty(resource, shortName, 'short_name', getLocation);
await checkIfPropertyValueIsUnderLimit(resource, shortName, 'short_name', shortNameLengthLimit, getLocation);
};

context.on('parse::manifest::end', validate);
Expand Down
25 changes: 15 additions & 10 deletions packages/hint-manifest-is-valid/src/hint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Category } from 'hint/dist/src/lib/enums/category';
import {
IAsyncHTMLElement,
IHint,
IJSONLocationFunction,
HintMetadata
} from 'hint/dist/src/lib/types';
import { isSupported } from 'hint/dist/src/lib/utils/caniuse';
Expand Down Expand Up @@ -79,7 +80,7 @@ export default class ManifestIsValidHint implements IHint {
color.model === 'hwb';
};

const checkColors = async (resource: string, element: IAsyncHTMLElement, manifest: Manifest) => {
const checkColors = async (resource: string, element: IAsyncHTMLElement, manifest: Manifest, getLocation: IJSONLocationFunction) => {
const colorProperties = [
'background_color',
'theme_color'
Expand All @@ -96,22 +97,22 @@ export default class ManifestIsValidHint implements IHint {
const color = parseColor(normalizedColorValue);

if (color === null) {
await context.report(resource, element, `Web app manifest should not have invalid value '${colorValue}' for property '${property}'.`);
await context.report(resource, element, `Web app manifest should not have invalid value '${colorValue}' for property '${property}'.`, null, getLocation(property));

continue;
}

if (isNotSupportedColorValue(color, normalizedColorValue)) {
await context.report(resource, element, `Web app manifest should not have unsupported value '${colorValue}' for property '${property}'.`);
await context.report(resource, element, `Web app manifest should not have unsupported value '${colorValue}' for property '${property}'.`, null, getLocation(property));
}
}
};

const checkLang = async (resource: string, element: IAsyncHTMLElement, manifest: Manifest) => {
const checkLang = async (resource: string, element: IAsyncHTMLElement, manifest: Manifest, getLocation: IJSONLocationFunction) => {
const lang = manifest.lang;

if (lang && !bcp47(lang)) {
await context.report(resource, element, `Web app manifest should not have invalid value '${manifest.lang}' for property 'lang'.`);
await context.report(resource, element, `Web app manifest should not have invalid value '${manifest.lang}' for property 'lang'.`, null, getLocation('lang'));
}
};

Expand All @@ -122,21 +123,25 @@ export default class ManifestIsValidHint implements IHint {
};

const handleInvalidSchema = async (manifestInvalidSchemaEvent: ManifestInvalidSchema) => {
for (const error of manifestInvalidSchemaEvent.prettifiedErrors) {
await context.report(manifestInvalidSchemaEvent.resource, manifestInvalidSchemaEvent.element, error);
for (let i = 0; i < manifestInvalidSchemaEvent.prettifiedErrors.length; i++) {
const error = manifestInvalidSchemaEvent.prettifiedErrors[i];
const location = manifestInvalidSchemaEvent.errors[i].location;

await context.report(manifestInvalidSchemaEvent.resource, manifestInvalidSchemaEvent.element, error, null, location);
}
};

const validateOtherProperties = async (manifestParsed: ManifestParsed) => {
const {
element,
getLocation,
parsedContent: manifest,
resource
}: { element: IAsyncHTMLElement, parsedContent: Manifest, resource: string } = manifestParsed;
} = manifestParsed;

// Additional checks not covered by the schema.
await checkLang(resource, element, manifest);
await checkColors(resource, element, manifest);
await checkLang(resource, element, manifest, getLocation);
await checkColors(resource, element, manifest, getLocation);
};

context.on('parse::manifest::end', validateOtherProperties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HintContext } from 'hint/dist/src/lib/hint-context';
const configChecker = (property: string, desiredValue: boolean, message: string, context: HintContext) => {

return async (evt: TypeScriptConfigParse): Promise<void> => {
const { config, resource } = evt;
const { config, getLocation, resource } = evt;
const properties = property.split('.');

let current = config[properties.shift()];
Expand All @@ -15,7 +15,7 @@ const configChecker = (property: string, desiredValue: boolean, message: string,
}

if (current !== desiredValue) {
await context.report(resource, null, message);
await context.report(resource, null, message, null, getLocation(property));
}
};
};
Expand Down
9 changes: 6 additions & 3 deletions packages/hint-typescript-config/src/is-valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,15 @@ export default class TypeScriptConfigIsValid implements IHint {
};

const invalidSchema = async (fetchEnd: TypeScriptConfigInvalidSchema) => {
const { prettifiedErrors, resource } = fetchEnd;
const { errors, prettifiedErrors, resource } = fetchEnd;

debug(`parse::typescript-config::error::schema received`);

for (const error of prettifiedErrors) {
await context.report(resource, null, error);
for (let i = 0; i < errors.length; i++) {
sarvaje marked this conversation as resolved.
Show resolved Hide resolved
const message = prettifiedErrors[i];
const location = errors[i].location;

await context.report(resource, null, message, null, location);
}
};

Expand Down
4 changes: 2 additions & 2 deletions packages/hint-typescript-config/src/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,15 @@ export default class TypeScriptConfigTarget implements IHint {
};

const validate = async (evt: TypeScriptConfigParse) => {
const { config, resource } = evt;
const { config, getLocation, resource } = evt;
const { targetedBrowsers } = context;
const target = getConfiguredTarget(config);
const minimumBrowsers = toMiniumBrowser(targetedBrowsers);

const maxESVersion = getMaxVersion(minimumBrowsers);

if (maxESVersion !== target) {
await context.report(resource, null, `Based on your browser configuration your "compilerOptions.target" should be "${maxESVersion}". Current one is "${target}"`);
await context.report(resource, null, `Based on your browser configuration your "compilerOptions.target" should be "${maxESVersion}". Current one is "${target}"`, null, getLocation('compilerOptions.target'));
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1 @@
{
"compilerOptions": {
"alwaysStrict": true,
"declaration": true,
"inlineSourceMap": true,
"lib": [
'dom',
"dom.iterable",
"es2017",
"esnext",
"esnext.asynciterable"
],
"module": "commonjs",
"newLine": "lf",
"removeComments": false,
"target": "esnext"
},
"exclude": [
"dist",
"node_modules",
"packages"
]
}
invalidJson
14 changes: 10 additions & 4 deletions packages/hint-typescript-config/tests/is-valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ const tests: Array<HintLocalTest> = [
{
name: 'Invalid configuration should fail',
path: path.join(__dirname, 'fixtures', 'invalidjson'),
reports: [{ message: `Unexpected token ' in JSON at position 148` }]
reports: [{ message: `Unexpected token i in JSON at position 0` }]
},
{
name: 'Invalid configuration should fail',
path: path.join(__dirname, 'fixtures', 'invalidschemaenum'),
reports: [{ message: `'compilerOptions.lib[3]' should be equal to one of the allowed values 'es5, es6, es2015, es7, es2016, es2017, es2018, esnext, dom, dom.iterable, webworker, scripthost, es2015.core, es2015.collection, es2015.generator, es2015.iterable, es2015.promise, es2015.proxy, es2015.reflect, es2015.symbol, es2015.symbol.wellknown, es2016.array.include, es2017.object, es2017.sharedmemory, es2017.typedarrays, esnext.array, esnext.asynciterable, esnext.promise'. Value found 'invalidlib'` }]
reports: [{
message: `'compilerOptions.lib[3]' should be equal to one of the allowed values 'es5, es6, es2015, es7, es2016, es2017, es2018, esnext, dom, dom.iterable, webworker, scripthost, es2015.core, es2015.collection, es2015.generator, es2015.iterable, es2015.promise, es2015.proxy, es2015.reflect, es2015.symbol, es2015.symbol.wellknown, es2016.array.include, es2017.object, es2017.sharedmemory, es2017.typedarrays, esnext.array, esnext.asynciterable, esnext.promise'. Value found 'invalidlib'`,
position: { column: 12, line: 9 }
}]
},
{
name: 'If schema has additional properties, it should fail',
path: path.join(__dirname, 'fixtures', 'invalidschemaadditional'),
reports: [{ message: `'compilerOptions' should NOT have additional properties. Additional property found 'invalidProperty'.` }]
reports: [{
message: `'compilerOptions' should NOT have additional properties. Additional property found 'invalidProperty'.`,
position: { column: 9, line: 15 }
}]
},
{
name: 'If schema has an invalid pattern, it should fail',
Expand All @@ -50,7 +56,7 @@ const tests: Array<HintLocalTest> = [
name: 'If the configuration has an invalid extends, it should fail',
path: path.join(__dirname, 'fixtures', 'invalid-extends'),
reports: [
{ message: `Unexpected token ' in JSON at position 148` }
{ message: `Unexpected token i in JSON at position 0` }
]
}
];
Expand Down
1 change: 1 addition & 0 deletions packages/hint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"handlebars": "^4.0.11",
"is-ci": "^1.2.0",
"is-svg": "^3.0.0",
"jsonc-parser": "^2.0.2",
"lodash": "^4.17.10",
"mime-db": "1.35.0",
"npm-registry-fetch": "^3.8.0",
Expand Down
1 change: 1 addition & 0 deletions packages/hint/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './types/async-html';
export * from './types/connector';
export * from './types/events';
export * from './types/formatters';
export * from './types/json-parser';
export * from './types/network';
export * from './types/problems';
export * from './types/hints';
Expand Down
36 changes: 36 additions & 0 deletions packages/hint/src/lib/types/json-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ProblemLocation } from './problems';

export interface IJSONLocationOptions {
at?: 'name' | 'value';
}

/**
* Resolve the location of a JSON object path (defaults to property name).
* Pass `true` for `atValue` to get the location of the property value instead.
*/
export interface IJSONLocationFunction {
(path: string, options?: IJSONLocationOptions): ProblemLocation;
}

/**
* Access parsed JSON with location information and scoping options.
*/
export interface IJSONResult {

/**
* The raw parsed data (as would be returned by `JSON.parse`).
*/
data: any;

/**
* Resolve the location of a JSON object path (defaults to property name).
* Pass `true` for `atValue` to get the location of the property value instead.
*/
getLocation: IJSONLocationFunction;

/**
* Get a `JSONResult` scoped to the specified path as its root.
* @param path The path to the new root (e.g. `foo.bar`)
*/
scope(path: string): IJSONResult;
}
7 changes: 6 additions & 1 deletion packages/hint/src/lib/types/schema-validation-result.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import * as ajv from 'ajv';
import { ProblemLocation } from './problems';

export interface ISchemaValidationError extends ajv.ErrorObject {
location?: ProblemLocation;
}

export type SchemaValidationResult = {
data: any;
errors: Array<ajv.ErrorObject>;
errors: Array<ISchemaValidationError>;
prettifiedErrors: Array<string>;
valid: boolean;
};
Loading