mirror of
https://github.com/LukeHagar/prettier-plugin-openapi.git
synced 2025-12-10 04:21:15 +00:00
Enhance OpenAPI file validation to reject generic content in component directories and update related tests for improved accuracy
This commit is contained in:
30
src/index.ts
30
src/index.ts
@@ -136,7 +136,7 @@ function isOpenAPIFile(content: any, filePath?: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check file path patterns for common OpenAPI file structures
|
// Check file path patterns for common OpenAPI file structures
|
||||||
// Only accept files in OpenAPI-related directories
|
// Only accept files in OpenAPI-related directories, but validate content first
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
const path = filePath.toLowerCase();
|
const path = filePath.toLowerCase();
|
||||||
|
|
||||||
@@ -155,7 +155,33 @@ function isOpenAPIFile(content: any, filePath?: string): boolean {
|
|||||||
path.includes("/webhooks/") ||
|
path.includes("/webhooks/") ||
|
||||||
path.includes("/paths/")
|
path.includes("/paths/")
|
||||||
) {
|
) {
|
||||||
return true;
|
// Fast-path: if filepath matches, still validate content to prevent
|
||||||
|
// generic JSON from being misclassified as OpenAPI
|
||||||
|
const keys = Object.keys(content);
|
||||||
|
const hasOnlyGenericProperties = keys.every(
|
||||||
|
(key) =>
|
||||||
|
!key.startsWith("x-") && // Not a custom extension
|
||||||
|
![
|
||||||
|
"openapi",
|
||||||
|
"swagger",
|
||||||
|
"info",
|
||||||
|
"paths",
|
||||||
|
"components",
|
||||||
|
"definitions",
|
||||||
|
"parameters",
|
||||||
|
"responses",
|
||||||
|
"securityDefinitions",
|
||||||
|
"tags",
|
||||||
|
"servers",
|
||||||
|
"webhooks",
|
||||||
|
].includes(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only accept if content doesn't have only generic properties
|
||||||
|
if (!hasOnlyGenericProperties) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If it has only generic properties, fall through to continue validation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1265,6 +1265,40 @@ type: string`;
|
|||||||
expect(result?.isOpenAPI).toBeFalse();
|
expect(result?.isOpenAPI).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject generic content in component directories (security check)', () => {
|
||||||
|
const parser = parsers?.['openapi-parser'];
|
||||||
|
expect(parser).toBeDefined();
|
||||||
|
|
||||||
|
// Generic JSON/YAML that should NOT be accepted even in component directories
|
||||||
|
const genericContent = `firstName: John
|
||||||
|
lastName: Doe
|
||||||
|
email: john@example.com
|
||||||
|
age: 30`;
|
||||||
|
|
||||||
|
// Test various component directory paths
|
||||||
|
const componentPaths = [
|
||||||
|
'components/schemas/User.yaml',
|
||||||
|
'components/parameters/UserId.yaml',
|
||||||
|
'components/responses/UserResponse.yaml',
|
||||||
|
'components/requestBodies/UserCreate.yaml',
|
||||||
|
'components/headers/RateLimit.yaml',
|
||||||
|
'components/examples/UserExample.yaml',
|
||||||
|
'components/securitySchemes/BearerAuth.yaml',
|
||||||
|
'components/links/UserLink.yaml',
|
||||||
|
'components/callbacks/NewMessageCallback.yaml',
|
||||||
|
'webhooks/messageCreated.yaml',
|
||||||
|
'paths/users.yaml',
|
||||||
|
];
|
||||||
|
|
||||||
|
componentPaths.forEach((path) => {
|
||||||
|
// @ts-expect-error We are testing edge cases
|
||||||
|
const result = parser?.parse(genericContent, { filepath: path });
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
// Should be rejected even though path matches component directory pattern
|
||||||
|
expect(result?.isOpenAPI).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle response code sorting with mixed numeric and non-numeric codes', () => {
|
it('should handle response code sorting with mixed numeric and non-numeric codes', () => {
|
||||||
const printer = printers?.['openapi-ast'];
|
const printer = printers?.['openapi-ast'];
|
||||||
expect(printer).toBeDefined();
|
expect(printer).toBeDefined();
|
||||||
|
|||||||
@@ -159,17 +159,40 @@ city: New York`;
|
|||||||
expect(parsedData?.isOpenAPI).toBeFalse();
|
expect(parsedData?.isOpenAPI).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept files in OpenAPI directories even with simple content', () => {
|
it('should reject generic content in OpenAPI component directories', () => {
|
||||||
const parser = parsers?.['openapi-parser'];
|
const parser = parsers?.['openapi-parser'];
|
||||||
expect(parser).toBeDefined();
|
expect(parser).toBeDefined();
|
||||||
|
|
||||||
const simpleYaml = `name: John
|
// Generic content that doesn't match OpenAPI structure
|
||||||
|
const genericYaml = `name: John
|
||||||
age: 30
|
age: 30
|
||||||
city: New York`;
|
city: New York`;
|
||||||
|
|
||||||
// @ts-expect-error We are mocking things here
|
// @ts-expect-error We are mocking things here
|
||||||
const result = parser?.parse(simpleYaml, { filepath: 'components/schemas/User.yaml' });
|
const result = parser?.parse(genericYaml, { filepath: 'components/schemas/User.yaml' });
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
|
// Should be rejected even though it's in a component directory
|
||||||
|
expect(result?.isOpenAPI).toBeFalse();
|
||||||
|
// Format may be undefined when isOpenAPI is false
|
||||||
|
// The important thing is that it's rejected
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid OpenAPI content in component directories', () => {
|
||||||
|
const parser = parsers?.['openapi-parser'];
|
||||||
|
expect(parser).toBeDefined();
|
||||||
|
|
||||||
|
// Valid schema content in component directory
|
||||||
|
const schemaYaml = `type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
age:
|
||||||
|
type: integer`;
|
||||||
|
|
||||||
|
// @ts-expect-error We are mocking things here
|
||||||
|
const result = parser?.parse(schemaYaml, { filepath: 'components/schemas/User.yaml' });
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
// Should be accepted because content is valid OpenAPI schema
|
||||||
expect(result?.isOpenAPI).toBeTrue();
|
expect(result?.isOpenAPI).toBeTrue();
|
||||||
expect(result?.format).toBe('yaml');
|
expect(result?.format).toBe('yaml');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,90 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { printers } from "../src/index.js";
|
import { printers } from "../src/index.js";
|
||||||
|
import prettier from "prettier";
|
||||||
|
|
||||||
describe("Markdown Formatting in Descriptions", () => {
|
describe("Markdown Formatting in Descriptions", () => {
|
||||||
const printer = printers?.["openapi-ast"];
|
const printer = printers?.["openapi-ast"];
|
||||||
|
|
||||||
|
// Get printDocToString from Prettier's internals (call once per test)
|
||||||
|
function getPrintDocToString() {
|
||||||
|
// Try multiple paths to access Prettier's printDocToString
|
||||||
|
const prettierAny = prettier as any;
|
||||||
|
|
||||||
|
// Path 1: __internal.doc.printDocToString
|
||||||
|
if (prettierAny.__internal?.doc?.printDocToString) {
|
||||||
|
return prettierAny.__internal.doc.printDocToString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path 2: __internal.docPrinter.formatDoc
|
||||||
|
if (prettierAny.__internal?.docPrinter?.formatDoc) {
|
||||||
|
return prettierAny.__internal.docPrinter.formatDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path 3: Try require with different paths
|
||||||
|
try {
|
||||||
|
// Try prettier/standalone or other internal paths
|
||||||
|
const docUtils = require("prettier/standalone");
|
||||||
|
if (docUtils?.printDocToString) {
|
||||||
|
return docUtils.printDocToString;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not available
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try direct access to internal modules
|
||||||
|
const prettierDoc = require("prettier/doc");
|
||||||
|
if (prettierDoc?.printer?.printDocToString) {
|
||||||
|
return prettierDoc.printer.printDocToString;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not available
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all else fails, check if result is already a string
|
||||||
|
// (some Prettier versions return strings directly)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert Doc to formatted string using Prettier's printDocToString
|
||||||
|
function docToString(doc: any, printDocToString: any, options: any = {}): string {
|
||||||
|
// If already a string, return it
|
||||||
|
if (typeof doc === "string") {
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If printDocToString is not available, try to stringify the doc
|
||||||
|
if (!printDocToString) {
|
||||||
|
// Fallback: if it's an object, try JSON.stringify or inspect
|
||||||
|
if (doc && typeof doc === "object") {
|
||||||
|
// Try to see if it has a toString that works
|
||||||
|
const str = String(doc);
|
||||||
|
if (str !== "[object Object]") {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
// Otherwise, we need printDocToString
|
||||||
|
throw new Error("printDocToString is required to convert Doc to string");
|
||||||
|
}
|
||||||
|
return String(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use printDocToString to convert Doc to formatted string
|
||||||
|
const result = printDocToString(doc, {
|
||||||
|
printWidth: options.printWidth || 80,
|
||||||
|
tabWidth: options.tabWidth || 2,
|
||||||
|
useTabs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// printDocToString returns { formatted: string }
|
||||||
|
return result?.formatted || String(doc);
|
||||||
|
}
|
||||||
|
|
||||||
describe("Basic markdown formatting", () => {
|
describe("Basic markdown formatting", () => {
|
||||||
it("should format description fields with markdown", () => {
|
it("should format description fields with markdown", () => {
|
||||||
expect(printer).toBeDefined();
|
expect(printer).toBeDefined();
|
||||||
|
|
||||||
|
const printDocToString = getPrintDocToString();
|
||||||
|
|
||||||
const testData = {
|
const testData = {
|
||||||
isOpenAPI: true,
|
isOpenAPI: true,
|
||||||
format: "yaml",
|
format: "yaml",
|
||||||
@@ -44,18 +121,20 @@ describe("Markdown Formatting in Descriptions", () => {
|
|||||||
throw new Error("Result is undefined");
|
throw new Error("Result is undefined");
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultString = result.toString();
|
const resultString = docToString(result, printDocToString, { tabWidth: 2 });
|
||||||
|
|
||||||
// Check that multiple spaces are normalized in the original content
|
// Check that multiple spaces are normalized in the original content
|
||||||
// Note: YAML may format this differently, but the content should be processed
|
// Note: YAML may format this differently, but the content should be processed
|
||||||
// The description field should exist and be formatted
|
// The description field should exist and be formatted
|
||||||
expect(resultString).toContain("description:");
|
expect(resultString).toContain("description:");
|
||||||
|
|
||||||
// Check that multiple blank lines are normalized
|
// Check that multiple blank lines are normalized (no quad-blank-lines)
|
||||||
expect(resultString).not.toMatch(/\n{4,}/);
|
expect(resultString).not.toMatch(/\n{4,}/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve code blocks in descriptions", () => {
|
it("should preserve code blocks in descriptions", () => {
|
||||||
|
const printDocToString = getPrintDocToString();
|
||||||
|
|
||||||
const testData = {
|
const testData = {
|
||||||
isOpenAPI: true,
|
isOpenAPI: true,
|
||||||
format: "yaml",
|
format: "yaml",
|
||||||
@@ -79,7 +158,7 @@ describe("Markdown Formatting in Descriptions", () => {
|
|||||||
throw new Error("Result is undefined");
|
throw new Error("Result is undefined");
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultString = result.toString();
|
const resultString = docToString(result, printDocToString, { tabWidth: 2 });
|
||||||
|
|
||||||
// Code blocks (4+ spaces) should be preserved
|
// Code blocks (4+ spaces) should be preserved
|
||||||
expect(resultString).toContain(" const x = 1;");
|
expect(resultString).toContain(" const x = 1;");
|
||||||
@@ -87,6 +166,8 @@ describe("Markdown Formatting in Descriptions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should format markdown in nested objects", () => {
|
it("should format markdown in nested objects", () => {
|
||||||
|
const printDocToString = getPrintDocToString();
|
||||||
|
|
||||||
const testData = {
|
const testData = {
|
||||||
isOpenAPI: true,
|
isOpenAPI: true,
|
||||||
format: "yaml",
|
format: "yaml",
|
||||||
@@ -127,7 +208,7 @@ describe("Markdown Formatting in Descriptions", () => {
|
|||||||
throw new Error("Result is undefined");
|
throw new Error("Result is undefined");
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultString = result.toString();
|
const resultString = docToString(result, printDocToString, { tabWidth: 2 });
|
||||||
|
|
||||||
// Both parameter and response descriptions should be formatted
|
// Both parameter and response descriptions should be formatted
|
||||||
expect(resultString).toContain("description:");
|
expect(resultString).toContain("description:");
|
||||||
@@ -136,6 +217,8 @@ describe("Markdown Formatting in Descriptions", () => {
|
|||||||
|
|
||||||
describe("Summary field formatting", () => {
|
describe("Summary field formatting", () => {
|
||||||
it("should format summary fields", () => {
|
it("should format summary fields", () => {
|
||||||
|
const printDocToString = getPrintDocToString();
|
||||||
|
|
||||||
const testData = {
|
const testData = {
|
||||||
isOpenAPI: true,
|
isOpenAPI: true,
|
||||||
format: "yaml",
|
format: "yaml",
|
||||||
@@ -167,7 +250,7 @@ describe("Markdown Formatting in Descriptions", () => {
|
|||||||
throw new Error("Result is undefined");
|
throw new Error("Result is undefined");
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultString = result.toString();
|
const resultString = docToString(result, printDocToString, { tabWidth: 2 });
|
||||||
|
|
||||||
// Summary fields should be processed
|
// Summary fields should be processed
|
||||||
expect(resultString).toContain("summary:");
|
expect(resultString).toContain("summary:");
|
||||||
|
|||||||
Reference in New Issue
Block a user