mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 21:25:08 +00:00
Compile for WASM
- wasm-pack build --target web - rm -rf demo/src/pkg && cp -r pkg demo/src/pkg - npm run dev
This commit is contained in:
parent
32e62d9209
commit
a716cd1989
30 changed files with 2978 additions and 3697 deletions
1652
Cargo.lock
generated
1652
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
@ -5,41 +5,28 @@ edition = "2021"
|
|||
|
||||
[lib]
|
||||
name = "ruff"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.83"
|
||||
anyhow = { version = "1.0.60" }
|
||||
bincode = { version = "1.3.3" }
|
||||
cacache = { version = "10.0.1" }
|
||||
chrono = { version = "0.4.21" }
|
||||
clap = { version = "3.2.16", features = ["derive"] }
|
||||
clearscreen = { version = "1.0.10" }
|
||||
colored = { version = "2.0.0" }
|
||||
common-path = { version = "1.0.0" }
|
||||
dirs = { version = "4.0.0" }
|
||||
fern = { version = "0.6.1" }
|
||||
filetime = { version = "0.2.17" }
|
||||
glob = { version = "0.3.0" }
|
||||
itertools = { version = "0.10.3" }
|
||||
log = { version = "0.4.17" }
|
||||
notify = { version = "4.0.17" }
|
||||
once_cell = { version = "1.13.1" }
|
||||
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
|
||||
rayon = { version = "1.5.3" }
|
||||
regex = { version = "1.6.0" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" }
|
||||
serde = { version = "1.0.143", features = ["derive"] }
|
||||
serde_json = { version = "1.0.83" }
|
||||
toml = { version = "0.5.9" }
|
||||
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
|
||||
walkdir = { version = "2.3.2" }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { version = "1.19.1", features = ["yaml"] }
|
||||
|
||||
[features]
|
||||
default = ["update-informer"]
|
||||
update-informer = ["dep:update-informer"]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = "thin"
|
||||
|
|
24
demo/.gitignore
vendored
Normal file
24
demo/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
2
demo/.prettierignore
Normal file
2
demo/.prettierignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
node_modules
|
7
demo/index.css
Normal file
7
demo/index.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
body,
|
||||
html,
|
||||
#root {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
29
demo/index.html
Normal file
29
demo/index.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="An in-browser playground for Ruff, an extremely fast Python linter written in Rust."
|
||||
/>
|
||||
<meta name="keywords" content="ruff, python, rust, webassembly, wasm" />
|
||||
<title>Ruff Playground</title>
|
||||
<link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛠️</text></svg>"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div style="display: flex; position: fixed; right: 16px; top: 16px">
|
||||
<a href="https://GitHub.com/charliermarsh/ruff"
|
||||
><img
|
||||
src="https://img.shields.io/github/stars/charliermarsh/ruff.svg?style=social&label=GitHub&maxAge=2592000&?logoWidth=100"
|
||||
alt="GitHub stars"
|
||||
style="width: 120px"
|
||||
/></a>
|
||||
</div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
2542
demo/package-lock.json
generated
Normal file
2542
demo/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
demo/package.json
Normal file
26
demo/package.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "demo",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"push": "wrangler pages publish dist --project-name=ruff"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.4.5",
|
||||
"monaco-editor": "^0.34.0",
|
||||
"prettier": "^2.7.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
}
|
1
demo/public/vite.svg
Normal file
1
demo/public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
69
demo/src/App.tsx
Normal file
69
demo/src/App.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import Editor, { Monaco } from "@monaco-editor/react";
|
||||
import { editor, MarkerSeverity } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { DEFAULT_SOURCE } from "./constants";
|
||||
import init, { check } from "./pkg/ruff.js";
|
||||
import { Check } from "./types";
|
||||
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
|
||||
|
||||
function App() {
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [_, setCounter] = useState(0);
|
||||
const [monaco, setMonaco] = useState<Monaco | null>(null);
|
||||
|
||||
// Load the WASM module.
|
||||
useEffect(() => {
|
||||
init().then(() => setInitialized(true));
|
||||
}, []);
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
() => setCounter((counter) => counter + 1),
|
||||
[]
|
||||
);
|
||||
const handleEditorDidMount = useCallback(
|
||||
(editor: IStandaloneCodeEditor, monaco: Monaco) => {
|
||||
setMonaco(monaco);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (initialized) {
|
||||
if (monaco) {
|
||||
const editor = monaco.editor;
|
||||
if (editor) {
|
||||
const model = editor.getModels()[0];
|
||||
if (model) {
|
||||
const checks: Check[] = JSON.parse(check(model.getValue()));
|
||||
editor.setModelMarkers(
|
||||
model,
|
||||
"owner",
|
||||
checks.map((check) => ({
|
||||
startLineNumber: check.location.row,
|
||||
startColumn: check.location.column,
|
||||
endLineNumber: check.location.row,
|
||||
endColumn: check.location.column,
|
||||
message: `${check.code}: ${check.message}`,
|
||||
severity: MarkerSeverity.Error,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Editor
|
||||
height={"100%"}
|
||||
width={"100%"}
|
||||
path={"ruff"}
|
||||
options={{ readOnly: false, minimap: { enabled: false } }}
|
||||
defaultLanguage="python"
|
||||
defaultValue={DEFAULT_SOURCE}
|
||||
theme={"light"}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={handleEditorChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
2
demo/src/assets/react.svg
Normal file
2
demo/src/assets/react.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" class="iconify iconify--logos"
|
||||
width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4 KiB |
55
demo/src/constants.ts
Normal file
55
demo/src/constants.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
export const DEFAULT_SOURCE = `#: E721
|
||||
if type(res) == type(42):
|
||||
pass
|
||||
#: E721
|
||||
if type(res) != type(""):
|
||||
pass
|
||||
#: E721
|
||||
import types
|
||||
|
||||
if res == types.IntType:
|
||||
pass
|
||||
#: E721
|
||||
import types
|
||||
|
||||
if type(res) is not types.ListType:
|
||||
pass
|
||||
#: E721
|
||||
assert type(res) == type(False) or type(res) == type(None)
|
||||
#: E721
|
||||
assert type(res) == type([])
|
||||
#: E721
|
||||
assert type(res) == type(())
|
||||
#: E721
|
||||
assert type(res) == type((0,))
|
||||
#: E721
|
||||
assert type(res) == type((0))
|
||||
#: E721
|
||||
assert type(res) != type((1,))
|
||||
#: E721
|
||||
assert type(res) is type((1,))
|
||||
#: E721
|
||||
assert type(res) is not type((1,))
|
||||
#: E721
|
||||
assert type(res) == type(
|
||||
[
|
||||
2,
|
||||
]
|
||||
)
|
||||
#: E721
|
||||
assert type(res) == type(())
|
||||
#: E721
|
||||
assert type(res) == type((0,))
|
||||
|
||||
#: Okay
|
||||
import types
|
||||
|
||||
if isinstance(res, int):
|
||||
pass
|
||||
if isinstance(res, str):
|
||||
pass
|
||||
if isinstance(res, types.MethodType):
|
||||
pass
|
||||
if type(a) != type(b) or type(a) == type(ccc):
|
||||
pass
|
||||
`;
|
10
demo/src/main.tsx
Normal file
10
demo/src/main.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "../index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
5
demo/src/types.ts
Normal file
5
demo/src/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type Check = {
|
||||
code: string;
|
||||
message: string;
|
||||
location: { row: number; column: number };
|
||||
};
|
1
demo/src/vite-env.d.ts
vendored
Normal file
1
demo/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
21
demo/tsconfig.json
Normal file
21
demo/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
9
demo/tsconfig.node.json
Normal file
9
demo/tsconfig.node.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
7
demo/vite.config.ts
Normal file
7
demo/vite.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
16
index.html
Normal file
16
index.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>hello-wasm example</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
import init, {check} from "./pkg/ruff.js";
|
||||
|
||||
init().then(() => {
|
||||
console.log(check("if (1, 2): pass"));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,11 +1,3 @@
|
|||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{Check, Fix};
|
||||
|
||||
#[derive(Hash)]
|
||||
pub enum Mode {
|
||||
Generate,
|
||||
|
@ -21,196 +13,3 @@ impl From<bool> for Mode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file(checks: &mut [Check], contents: &str, path: &Path) -> Result<()> {
|
||||
if checks.iter().all(|check| check.fix.is_none()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = apply_fixes(
|
||||
checks.iter_mut().filter_map(|check| check.fix.as_mut()),
|
||||
contents,
|
||||
);
|
||||
|
||||
fs::write(path, output).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Apply a series of fixes.
|
||||
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
|
||||
let mut output = "".to_string();
|
||||
let mut last_pos: Location = Location::new(0, 0);
|
||||
|
||||
for fix in fixes {
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos > fix.start {
|
||||
continue;
|
||||
}
|
||||
|
||||
if fix.start.row() > last_pos.row() {
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..fix.start.row() - 1] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&lines[fix.start.row() - 1][..fix.start.column() - 1]);
|
||||
output.push_str(&fix.content);
|
||||
} else {
|
||||
output.push_str(
|
||||
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.start.column() - 1],
|
||||
);
|
||||
output.push_str(&fix.content);
|
||||
}
|
||||
|
||||
last_pos = fix.end;
|
||||
fix.applied = true;
|
||||
}
|
||||
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::fixer::apply_fixes;
|
||||
use crate::checks::Fix;
|
||||
|
||||
#[test]
|
||||
fn empty_file() -> Result<()> {
|
||||
let mut fixes = vec![];
|
||||
let actual = apply_fixes(fixes.iter_mut(), "");
|
||||
let expected = "";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_replacement() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
start: Location::new(1, 9),
|
||||
end: Location::new(1, 15),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A(Bar):
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_removal() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 16),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_double_removal() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 17),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 17),
|
||||
end: Location::new(1, 24),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object, object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_overlapping_fixes() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 16),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
start: Location::new(1, 10),
|
||||
end: Location::new(1, 12),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
153
src/cache.rs
153
src/cache.rs
|
@ -1,153 +0,0 @@
|
|||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fs::{create_dir_all, File, Metadata};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use cacache::Error::EntryNotFound;
|
||||
use filetime::FileTime;
|
||||
use log::error;
|
||||
use path_absolutize::Absolutize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::message::Message;
|
||||
use crate::settings::Settings;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CacheMetadata {
|
||||
mtime: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CheckResultRef<'a> {
|
||||
metadata: &'a CacheMetadata,
|
||||
messages: &'a [Message],
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CheckResult {
|
||||
metadata: CacheMetadata,
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
pub enum Mode {
|
||||
ReadWrite,
|
||||
ReadOnly,
|
||||
WriteOnly,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
fn allow_read(&self) -> bool {
|
||||
match self {
|
||||
Mode::ReadWrite => true,
|
||||
Mode::ReadOnly => true,
|
||||
Mode::WriteOnly => false,
|
||||
Mode::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn allow_write(&self) -> bool {
|
||||
match self {
|
||||
Mode::ReadWrite => true,
|
||||
Mode::ReadOnly => false,
|
||||
Mode::WriteOnly => true,
|
||||
Mode::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => Mode::ReadWrite,
|
||||
false => Mode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_dir() -> &'static str {
|
||||
"./.ruff_cache"
|
||||
}
|
||||
|
||||
fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
settings.hash(&mut hasher);
|
||||
autofix.hash(&mut hasher);
|
||||
format!(
|
||||
"{}@{}@{}",
|
||||
path.absolutize().unwrap().to_string_lossy(),
|
||||
VERSION,
|
||||
hasher.finish()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn init() -> Result<()> {
|
||||
let gitignore_path = Path::new(cache_dir()).join(".gitignore");
|
||||
if gitignore_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
create_dir_all(cache_dir())?;
|
||||
let mut file = File::create(gitignore_path)?;
|
||||
file.write_all(b"*").map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
path: &Path,
|
||||
metadata: &Metadata,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
mode: &Mode,
|
||||
) -> Option<Vec<Message>> {
|
||||
if !mode.allow_read() {
|
||||
return None;
|
||||
};
|
||||
|
||||
match cacache::read_sync(cache_dir(), cache_key(path, settings, autofix)) {
|
||||
Ok(encoded) => match bincode::deserialize::<CheckResult>(&encoded[..]) {
|
||||
Ok(CheckResult {
|
||||
metadata: CacheMetadata { mtime },
|
||||
messages,
|
||||
}) => {
|
||||
if FileTime::from_last_modification_time(metadata).unix_seconds() == mtime {
|
||||
return Some(messages);
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to deserialize encoded cache entry: {e:?}"),
|
||||
},
|
||||
Err(EntryNotFound(_, _)) => {}
|
||||
Err(e) => error!("Failed to read from cache: {e:?}"),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
path: &Path,
|
||||
metadata: &Metadata,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
messages: &[Message],
|
||||
mode: &Mode,
|
||||
) {
|
||||
if !mode.allow_write() {
|
||||
return;
|
||||
};
|
||||
|
||||
let check_result = CheckResultRef {
|
||||
metadata: &CacheMetadata {
|
||||
mtime: FileTime::from_last_modification_time(metadata).unix_seconds(),
|
||||
},
|
||||
messages,
|
||||
};
|
||||
if let Err(e) = cacache::write_sync(
|
||||
cache_dir(),
|
||||
cache_key(path, settings, autofix),
|
||||
bincode::serialize(&check_result).unwrap(),
|
||||
) {
|
||||
error!("Failed to write to cache: {e:?}")
|
||||
}
|
||||
}
|
243
src/fs.rs
243
src/fs.rs
|
@ -1,243 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use log::debug;
|
||||
use path_absolutize::path_dedot;
|
||||
use path_absolutize::Absolutize;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::settings::FilePattern;
|
||||
|
||||
/// Extract the absolute path and basename (as strings) from a Path.
|
||||
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
|
||||
let file_path = path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
|
||||
let file_basename = path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
|
||||
Ok((file_path, file_basename))
|
||||
}
|
||||
|
||||
fn is_excluded(file_path: &str, file_basename: &str, exclude: &[FilePattern]) -> bool {
|
||||
for pattern in exclude {
|
||||
match &pattern {
|
||||
FilePattern::Simple(basename) => {
|
||||
if *basename == file_basename {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
FilePattern::Complex(absolute, basename) => {
|
||||
if absolute.matches(file_path) {
|
||||
return true;
|
||||
}
|
||||
if basename
|
||||
.as_ref()
|
||||
.map(|pattern| pattern.matches(file_basename))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_included(path: &Path) -> bool {
|
||||
let file_name = path.to_string_lossy();
|
||||
file_name.ends_with(".py") || file_name.ends_with(".pyi")
|
||||
}
|
||||
|
||||
pub fn iter_python_files<'a>(
|
||||
path: &'a Path,
|
||||
exclude: &'a [FilePattern],
|
||||
extend_exclude: &'a [FilePattern],
|
||||
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
|
||||
// Run some checks over the provided patterns, to enable optimizations below.
|
||||
let has_exclude = !exclude.is_empty();
|
||||
let has_extend_exclude = !extend_exclude.is_empty();
|
||||
let exclude_simple = exclude
|
||||
.iter()
|
||||
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
|
||||
let extend_exclude_simple = extend_exclude
|
||||
.iter()
|
||||
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
|
||||
|
||||
WalkDir::new(normalize_path(path))
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
.filter_entry(move |entry| {
|
||||
if !has_exclude && !has_extend_exclude {
|
||||
return true;
|
||||
}
|
||||
|
||||
let path = entry.path();
|
||||
match extract_path_names(path) {
|
||||
Ok((file_path, file_basename)) => {
|
||||
let file_type = entry.file_type();
|
||||
|
||||
if has_exclude
|
||||
&& (!exclude_simple || file_type.is_dir())
|
||||
&& is_excluded(file_path, file_basename, exclude)
|
||||
{
|
||||
debug!("Ignored path via `exclude`: {:?}", path);
|
||||
false
|
||||
} else if has_extend_exclude
|
||||
&& (!extend_exclude_simple || file_type.is_dir())
|
||||
&& is_excluded(file_path, file_basename, extend_exclude)
|
||||
{
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("Ignored path due to error in parsing: {:?}", path);
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(|entry| {
|
||||
entry.as_ref().map_or(true, |entry| {
|
||||
(entry.depth() == 0 && !entry.file_type().is_dir()) || is_included(entry.path())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert any path to an absolute path (based on the current working directory).
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
if let Ok(path) = path.absolutize() {
|
||||
return path.to_path_buf();
|
||||
}
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Convert any path to an absolute path (based on the specified project root).
|
||||
pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
|
||||
if let Ok(path) = path.absolutize_from(project_root) {
|
||||
return path.to_path_buf();
|
||||
}
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Convert an absolute path to be relative to the current working directory.
|
||||
pub fn relativize_path(path: &Path) -> Cow<str> {
|
||||
if let Ok(path) = path.strip_prefix(path_dedot::CWD.deref()) {
|
||||
return path.to_string_lossy();
|
||||
}
|
||||
path.to_string_lossy()
|
||||
}
|
||||
|
||||
/// Read a file's contents from disk.
|
||||
pub fn read_file(path: &Path) -> Result<String> {
|
||||
let file = File::open(path)?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
buf_reader.read_to_string(&mut contents)?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use path_absolutize::Absolutize;
|
||||
|
||||
use crate::fs::{extract_path_names, is_excluded, is_included};
|
||||
use crate::settings::FilePattern;
|
||||
|
||||
#[test]
|
||||
fn inclusions() {
|
||||
let path = Path::new("foo/bar/baz.py").absolutize().unwrap();
|
||||
assert!(is_included(&path));
|
||||
|
||||
let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap();
|
||||
assert!(is_included(&path));
|
||||
|
||||
let path = Path::new("foo/bar/baz.js").absolutize().unwrap();
|
||||
assert!(!is_included(&path));
|
||||
|
||||
let path = Path::new("foo/bar/baz").absolutize().unwrap();
|
||||
assert!(!is_included(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclusions() -> Result<()> {
|
||||
let project_root = Path::new("/tmp/");
|
||||
|
||||
let path = Path::new("foo").absolutize_from(project_root).unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"foo",
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
|
||||
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"bar",
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"baz.py",
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
|
||||
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"foo/bar",
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"foo/bar/baz.py",
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"foo/bar/*.py",
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"baz",
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(!is_excluded(file_path, file_basename, &exclude));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
73
src/lib.rs
73
src/lib.rs
|
@ -1,16 +1,73 @@
|
|||
extern crate core;
|
||||
use rustpython_parser::ast::Location;
|
||||
use std::path::Path;
|
||||
|
||||
use rustpython_parser::parser;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::check_ast::check_ast;
|
||||
use crate::check_lines::check_lines;
|
||||
use crate::checks::{Check, CheckCode, CheckKind, ALL_CHECK_CODES};
|
||||
use crate::settings::Settings;
|
||||
|
||||
mod ast;
|
||||
mod autofix;
|
||||
pub mod cache;
|
||||
pub mod check_ast;
|
||||
mod check_lines;
|
||||
pub mod checks;
|
||||
pub mod fs;
|
||||
pub mod linter;
|
||||
pub mod logging;
|
||||
pub mod message;
|
||||
pub mod printer;
|
||||
pub mod pyproject;
|
||||
mod python;
|
||||
pub mod settings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct Message {
|
||||
code: CheckCode,
|
||||
message: String,
|
||||
location: Location,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
pub fn alert(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn check(contents: &str) -> String {
|
||||
let settings = Settings::for_rules(ALL_CHECK_CODES.to_vec());
|
||||
let autofix = autofix::fixer::Mode::None;
|
||||
|
||||
// Aggregate all checks.
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
// Run the AST-based checks.
|
||||
match parser::parse_program(contents, "<filename>") {
|
||||
Ok(python_ast) => checks.extend(check_ast(
|
||||
&python_ast,
|
||||
contents,
|
||||
&settings,
|
||||
&autofix,
|
||||
Path::new("<filename>"),
|
||||
)),
|
||||
Err(parse_error) => {
|
||||
if settings.select.contains(&CheckCode::E999) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::SyntaxError(parse_error.error.to_string()),
|
||||
parse_error.location,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the lines-based checks.
|
||||
check_lines(&mut checks, contents, &settings);
|
||||
|
||||
let messages: Vec<Message> = checks
|
||||
.into_iter()
|
||||
.map(|check| Message {
|
||||
code: check.kind.code().clone(),
|
||||
message: check.kind.body(),
|
||||
location: check.location,
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string(&messages).unwrap()
|
||||
}
|
||||
|
|
628
src/linter.rs
628
src/linter.rs
|
@ -1,628 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::debug;
|
||||
use rustpython_parser::parser;
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::fixer::fix_file;
|
||||
use crate::check_ast::check_ast;
|
||||
use crate::check_lines::check_lines;
|
||||
use crate::checks::{Check, CheckCode, CheckKind, LintSource};
|
||||
use crate::message::Message;
|
||||
use crate::settings::Settings;
|
||||
use crate::{cache, fs};
|
||||
|
||||
fn check_path(
|
||||
path: &Path,
|
||||
contents: &str,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Vec<Check> {
|
||||
// Aggregate all checks.
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
// Run the AST-based checks.
|
||||
if settings
|
||||
.select
|
||||
.iter()
|
||||
.any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
|
||||
{
|
||||
match parser::parse_program(contents, "<filename>") {
|
||||
Ok(python_ast) => {
|
||||
checks.extend(check_ast(&python_ast, contents, settings, autofix, path))
|
||||
}
|
||||
Err(parse_error) => {
|
||||
if settings.select.contains(&CheckCode::E999) {
|
||||
checks.push(Check::new(
|
||||
CheckKind::SyntaxError(parse_error.error.to_string()),
|
||||
parse_error.location,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the lines-based checks.
|
||||
check_lines(&mut checks, contents, settings);
|
||||
|
||||
checks
|
||||
}
|
||||
|
||||
pub fn lint_path(
|
||||
path: &Path,
|
||||
settings: &Settings,
|
||||
mode: &cache::Mode,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Vec<Message>> {
|
||||
let metadata = path.metadata()?;
|
||||
|
||||
// Check the cache.
|
||||
if let Some(messages) = cache::get(path, &metadata, settings, autofix, mode) {
|
||||
debug!("Cache hit for: {}", path.to_string_lossy());
|
||||
return Ok(messages);
|
||||
}
|
||||
|
||||
// Read the file from disk.
|
||||
let contents = fs::read_file(path)?;
|
||||
|
||||
// Generate checks.
|
||||
let mut checks = check_path(path, &contents, settings, autofix);
|
||||
|
||||
// Apply autofix.
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
fix_file(&mut checks, &contents, path)?;
|
||||
};
|
||||
|
||||
// Convert to messages.
|
||||
let messages: Vec<Message> = checks
|
||||
.into_iter()
|
||||
.map(|check| Message {
|
||||
kind: check.kind,
|
||||
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
|
||||
location: check.location,
|
||||
filename: path.to_string_lossy().to_string(),
|
||||
})
|
||||
.collect();
|
||||
cache::set(path, &metadata, settings, autofix, &messages, mode);
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::fs;
|
||||
use crate::linter;
|
||||
use crate::settings;
|
||||
|
||||
fn check_path(
|
||||
path: &Path,
|
||||
settings: &settings::Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Result<Vec<Check>> {
|
||||
let contents = fs::read_file(path)?;
|
||||
Ok(linter::check_path(path, &contents, settings, autofix))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e402() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E402.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E402),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e501() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E501.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E501),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e711() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E711.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E711),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e712() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E712.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E712),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e713() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E713.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E713),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e721() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E721.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E721),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e722() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E722.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E722),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e714() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E714.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E714),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e731() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E731.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E731),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e741() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E741.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E741),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e742() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E742.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E742),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e743() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E743.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E743),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f401() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F401.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F401),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f402() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F402.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F402),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f403() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F403.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F403),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f404() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F404.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F404),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f405() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F405.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F405),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f406() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F406.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F406),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f407() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F407.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F407),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f541() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F541.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F541),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f601() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F601.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F601),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f602() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F602.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F602),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f622() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F622.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F622),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f631() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F631.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F631),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f632() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F632.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F632),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f633() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F633.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F633),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f634() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F634.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F634),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f701() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F701.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F701),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f702() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F702.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F702),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f704() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F704.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F704),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f706() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F706.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F706),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f707() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F707.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F707),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f722() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F722.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F722),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f821() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F821.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F821),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f822() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F822.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F822),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f823() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F823.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F823),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f831() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F831.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F831),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f841() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F841.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F841),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f901() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F901.py"),
|
||||
&settings::Settings::for_rule(CheckCode::F901),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn r001() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/R001.py"),
|
||||
&settings::Settings::for_rule(CheckCode::R001),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn r002() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/R002.py"),
|
||||
&settings::Settings::for_rule(CheckCode::R002),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/__init__.py"),
|
||||
&settings::Settings::for_rules(vec![CheckCode::F821, CheckCode::F822]),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_annotations() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/future_annotations.py"),
|
||||
&settings::Settings::for_rules(vec![CheckCode::F401, CheckCode::F821]),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use fern;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! tell_user {
|
||||
($($arg:tt)*) => {
|
||||
println!(
|
||||
"[{}] {}",
|
||||
chrono::Local::now()
|
||||
.format("%H:%M:%S %p")
|
||||
.to_string()
|
||||
.dimmed(),
|
||||
format_args!($($arg)*)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_up_logging(verbose: bool) -> Result<()> {
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
record.target(),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(if verbose {
|
||||
log::LevelFilter::Debug
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
})
|
||||
.chain(std::io::stdout())
|
||||
.apply()
|
||||
.map_err(|e| e.into())
|
||||
}
|
304
src/main.rs
304
src/main.rs
|
@ -1,304 +0,0 @@
|
|||
extern crate core;
|
||||
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueHint};
|
||||
use colored::Colorize;
|
||||
use log::{debug, error};
|
||||
use notify::{raw_watcher, RecursiveMode, Watcher};
|
||||
use rayon::prelude::*;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use ::ruff::cache;
|
||||
use ::ruff::checks::CheckCode;
|
||||
use ::ruff::checks::CheckKind;
|
||||
use ::ruff::fs::iter_python_files;
|
||||
use ::ruff::linter::lint_path;
|
||||
use ::ruff::logging::set_up_logging;
|
||||
use ::ruff::message::Message;
|
||||
use ::ruff::printer::{Printer, SerializationFormat};
|
||||
use ::ruff::pyproject;
|
||||
use ::ruff::settings::{FilePattern, Settings};
|
||||
use ::ruff::tell_user;
|
||||
|
||||
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(name = format!("{CARGO_PKG_NAME} (v{CARGO_PKG_VERSION})"))]
|
||||
#[clap(about = "An extremely fast Python linter.", long_about = None)]
|
||||
#[clap(version)]
|
||||
struct Cli {
|
||||
#[clap(parse(from_os_str), value_hint = ValueHint::AnyPath, required = true)]
|
||||
files: Vec<PathBuf>,
|
||||
/// Enable verbose logging.
|
||||
#[clap(short, long, action)]
|
||||
verbose: bool,
|
||||
/// Disable all logging (but still exit with status code "1" upon detecting errors).
|
||||
#[clap(short, long, action)]
|
||||
quiet: bool,
|
||||
/// Exit with status code "0", even upon detecting errors.
|
||||
#[clap(short, long, action)]
|
||||
exit_zero: bool,
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
#[clap(short, long, action)]
|
||||
watch: bool,
|
||||
/// Attempt to automatically fix lint errors.
|
||||
#[clap(short, long, action)]
|
||||
fix: bool,
|
||||
/// Disable cache reads.
|
||||
#[clap(short, long, action)]
|
||||
no_cache: bool,
|
||||
/// List of error codes to enable.
|
||||
#[clap(long, multiple = true)]
|
||||
select: Vec<CheckCode>,
|
||||
/// List of error codes to ignore.
|
||||
#[clap(long, multiple = true)]
|
||||
ignore: Vec<CheckCode>,
|
||||
/// List of paths, used to exclude files and/or directories from checks.
|
||||
#[clap(long, multiple = true)]
|
||||
exclude: Vec<String>,
|
||||
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
|
||||
#[clap(long, multiple = true)]
|
||||
extend_exclude: Vec<String>,
|
||||
/// Output serialization format for error messages.
|
||||
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
|
||||
format: SerializationFormat,
|
||||
/// See the files ruff will be run against with the current settings.
|
||||
#[clap(long, action)]
|
||||
show_files: bool,
|
||||
/// See ruff's settings.
|
||||
#[clap(long, action)]
|
||||
show_settings: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "update-informer")]
|
||||
fn check_for_updates() {
|
||||
use update_informer::{registry, Check};
|
||||
|
||||
let informer = update_informer::new(registry::PyPI, CARGO_PKG_NAME, CARGO_PKG_VERSION);
|
||||
|
||||
if let Some(new_version) = informer.check_version().ok().flatten() {
|
||||
let msg = format!(
|
||||
"A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
|
||||
pkg_name = CARGO_PKG_NAME.italic().cyan(),
|
||||
pkg_version = CARGO_PKG_VERSION,
|
||||
new_version = new_version.to_string().green()
|
||||
);
|
||||
|
||||
let cmd = format!(
|
||||
"Run to update: {cmd} {pkg_name}",
|
||||
cmd = "pip3 install --upgrade".green(),
|
||||
pkg_name = CARGO_PKG_NAME.green()
|
||||
);
|
||||
|
||||
println!("\n{msg}\n{cmd}");
|
||||
}
|
||||
}
|
||||
|
||||
fn show_settings(settings: &Settings) {
|
||||
println!("{:#?}", settings);
|
||||
}
|
||||
|
||||
fn show_files(files: &[PathBuf], settings: &Settings) {
|
||||
let mut entries: Vec<DirEntry> = files
|
||||
.iter()
|
||||
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
|
||||
.flatten()
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.path().cmp(b.path()));
|
||||
for entry in entries {
|
||||
println!("{}", entry.path().to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
fn run_once(
|
||||
files: &[PathBuf],
|
||||
settings: &Settings,
|
||||
cache: bool,
|
||||
autofix: bool,
|
||||
) -> Result<Vec<Message>> {
|
||||
// Collect all the files to check.
|
||||
let start = Instant::now();
|
||||
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
|
||||
.iter()
|
||||
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
|
||||
.collect();
|
||||
let duration = start.elapsed();
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
|
||||
let start = Instant::now();
|
||||
let mut messages: Vec<Message> = paths
|
||||
.par_iter()
|
||||
.map(|entry| {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
let path = entry.path();
|
||||
lint_path(path, settings, &cache.into(), &autofix.into())
|
||||
.map_err(|e| (Some(path.to_owned()), e.to_string()))
|
||||
}
|
||||
Err(e) => Err((
|
||||
e.path().map(Path::to_owned),
|
||||
e.io_error()
|
||||
.map_or_else(|| e.to_string(), io::Error::to_string),
|
||||
)),
|
||||
}
|
||||
.unwrap_or_else(|(path, message)| {
|
||||
if let Some(path) = path {
|
||||
if settings.select.contains(&CheckCode::E902) {
|
||||
vec![Message {
|
||||
kind: CheckKind::IOError(message),
|
||||
fixed: false,
|
||||
location: Default::default(),
|
||||
filename: path.to_string_lossy().to_string(),
|
||||
}]
|
||||
} else {
|
||||
error!("Failed to check {}: {message}", path.to_string_lossy());
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
error!("{message}");
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
messages.sort_unstable();
|
||||
let duration = start.elapsed();
|
||||
debug!("Checked files in: {:?}", duration);
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
fn inner_main() -> Result<ExitCode> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
set_up_logging(cli.verbose)?;
|
||||
|
||||
// Find the project root and pyproject.toml.
|
||||
let project_root = pyproject::find_project_root(&cli.files);
|
||||
match &project_root {
|
||||
Some(path) => debug!("Found project root at: {:?}", path),
|
||||
None => debug!("Unable to identify project root; assuming current directory..."),
|
||||
};
|
||||
let pyproject = pyproject::find_pyproject_toml(&project_root);
|
||||
match &pyproject {
|
||||
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
|
||||
None => debug!("Unable to find pyproject.toml; using default settings..."),
|
||||
};
|
||||
|
||||
// Parse the settings from the pyproject.toml and command-line arguments.
|
||||
let exclude: Vec<FilePattern> = cli
|
||||
.exclude
|
||||
.iter()
|
||||
.map(|path| FilePattern::from_user(path, &project_root))
|
||||
.collect();
|
||||
let extend_exclude: Vec<FilePattern> = cli
|
||||
.extend_exclude
|
||||
.iter()
|
||||
.map(|path| FilePattern::from_user(path, &project_root))
|
||||
.collect();
|
||||
|
||||
let mut settings = Settings::from_pyproject(pyproject, project_root);
|
||||
if !exclude.is_empty() {
|
||||
settings.exclude = exclude;
|
||||
}
|
||||
if !extend_exclude.is_empty() {
|
||||
settings.extend_exclude = extend_exclude;
|
||||
}
|
||||
if !cli.select.is_empty() {
|
||||
settings.select(cli.select);
|
||||
}
|
||||
if !cli.ignore.is_empty() {
|
||||
settings.ignore(&cli.ignore);
|
||||
}
|
||||
|
||||
if cli.show_settings && cli.show_files {
|
||||
println!("Error: specify --show-settings or show-files (not both).");
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
if cli.show_settings {
|
||||
show_settings(&settings);
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
if cli.show_files {
|
||||
show_files(&cli.files, &settings);
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
cache::init()?;
|
||||
|
||||
let mut printer = Printer::new(cli.format, cli.verbose);
|
||||
if cli.watch {
|
||||
if cli.fix {
|
||||
println!("Warning: --fix is not enabled in watch mode.");
|
||||
}
|
||||
|
||||
if cli.format != SerializationFormat::Text {
|
||||
println!("Warning: --format 'text' is used in watch mode.");
|
||||
}
|
||||
|
||||
// Perform an initial run instantly.
|
||||
printer.clear_screen()?;
|
||||
tell_user!("Starting linter in watch mode...\n");
|
||||
|
||||
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
|
||||
if !cli.quiet {
|
||||
printer.write_continuously(&messages)?;
|
||||
}
|
||||
|
||||
// Configure the file watcher.
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = raw_watcher(tx)?;
|
||||
for file in &cli.files {
|
||||
watcher.watch(file, RecursiveMode::Recursive)?;
|
||||
}
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(e) => {
|
||||
if let Some(path) = e.path {
|
||||
if path.to_string_lossy().ends_with(".py") {
|
||||
printer.clear_screen()?;
|
||||
tell_user!("File change detected...\n");
|
||||
|
||||
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
|
||||
if !cli.quiet {
|
||||
printer.write_continuously(&messages)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
|
||||
if !cli.quiet {
|
||||
printer.write_once(&messages)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "update-informer")]
|
||||
check_for_updates();
|
||||
|
||||
if !messages.is_empty() && !cli.exit_zero {
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match inner_main() {
|
||||
Ok(code) => code,
|
||||
Err(_) => ExitCode::FAILURE,
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use colored::Colorize;
|
||||
use rustpython_parser::ast::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::checks::CheckKind;
|
||||
use crate::fs::relativize_path;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub kind: CheckKind,
|
||||
pub fixed: bool,
|
||||
pub location: Location,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
impl Ord for Message {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
(&self.filename, self.location.row(), self.location.column()).cmp(&(
|
||||
&other.filename,
|
||||
other.location.row(),
|
||||
other.location.column(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Message {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Message {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}{}{}{}{}{} {} {}",
|
||||
relativize_path(Path::new(&self.filename)).white().bold(),
|
||||
// self.filename.white(),
|
||||
":".cyan(),
|
||||
self.location.row(),
|
||||
":".cyan(),
|
||||
self.location.column(),
|
||||
":".cyan(),
|
||||
self.kind.code().as_str().red().bold(),
|
||||
self.kind.body()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use clap::ValueEnum;
|
||||
use colored::Colorize;
|
||||
|
||||
use crate::message::Message;
|
||||
use crate::tell_user;
|
||||
|
||||
#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)]
|
||||
pub enum SerializationFormat {
|
||||
Text,
|
||||
Json,
|
||||
}
|
||||
|
||||
pub struct Printer {
|
||||
format: SerializationFormat,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
impl Printer {
|
||||
pub fn new(format: SerializationFormat, verbose: bool) -> Self {
|
||||
Self { format, verbose }
|
||||
}
|
||||
|
||||
pub fn write_once(&mut self, messages: &[Message]) -> Result<()> {
|
||||
let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) =
|
||||
messages.iter().partition(|message| message.fixed);
|
||||
let num_fixable = outstanding
|
||||
.iter()
|
||||
.filter(|message| message.kind.fixable())
|
||||
.count();
|
||||
|
||||
match self.format {
|
||||
SerializationFormat::Json => {
|
||||
println!("{}", serde_json::to_string_pretty(&messages)?)
|
||||
}
|
||||
SerializationFormat::Text => {
|
||||
if !fixed.is_empty() {
|
||||
println!(
|
||||
"Found {} error(s) ({} fixed).",
|
||||
outstanding.len(),
|
||||
fixed.len()
|
||||
)
|
||||
} else if !outstanding.is_empty() || self.verbose {
|
||||
println!("Found {} error(s).", outstanding.len())
|
||||
}
|
||||
|
||||
for message in outstanding {
|
||||
println!("{}", message)
|
||||
}
|
||||
|
||||
if num_fixable > 0 {
|
||||
println!("{num_fixable} potentially fixable with the --fix option.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_continuously(&mut self, messages: &[Message]) -> Result<()> {
|
||||
tell_user!(
|
||||
"Found {} error(s). Watching for file changes.",
|
||||
messages.len(),
|
||||
);
|
||||
|
||||
if !messages.is_empty() {
|
||||
println!();
|
||||
for message in messages {
|
||||
println!("{}", message)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_screen(&mut self) -> Result<()> {
|
||||
clearscreen::clear()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
289
src/pyproject.rs
289
src/pyproject.rs
|
@ -1,289 +0,0 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use common_path::common_path_all;
|
||||
use path_absolutize::Absolutize;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::fs;
|
||||
|
||||
pub fn load_config(pyproject: &Option<PathBuf>) -> Config {
|
||||
match pyproject {
|
||||
Some(pyproject) => match parse_pyproject_toml(pyproject) {
|
||||
Ok(pyproject) => pyproject
|
||||
.tool
|
||||
.and_then(|tool| tool.ruff)
|
||||
.unwrap_or_default(),
|
||||
Err(e) => {
|
||||
println!("Failed to load pyproject.toml: {:?}", e);
|
||||
println!("Falling back to default configuration...");
|
||||
Default::default()
|
||||
}
|
||||
},
|
||||
None => {
|
||||
println!("No pyproject.toml found.");
|
||||
println!("Falling back to default configuration...");
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
pub struct Config {
|
||||
pub line_length: Option<usize>,
|
||||
pub exclude: Option<Vec<String>>,
|
||||
pub extend_exclude: Option<Vec<String>>,
|
||||
pub select: Option<Vec<CheckCode>>,
|
||||
pub ignore: Option<Vec<CheckCode>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
struct Tools {
|
||||
ruff: Option<Config>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
struct PyProject {
|
||||
tool: Option<Tools>,
|
||||
}
|
||||
|
||||
fn parse_pyproject_toml(path: &Path) -> Result<PyProject> {
|
||||
let contents = fs::read_file(path)?;
|
||||
toml::from_str(&contents).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn find_pyproject_toml(path: &Option<PathBuf>) -> Option<PathBuf> {
|
||||
if let Some(path) = path {
|
||||
let path_pyproject_toml = path.join("pyproject.toml");
|
||||
if path_pyproject_toml.is_file() {
|
||||
return Some(path_pyproject_toml);
|
||||
}
|
||||
}
|
||||
|
||||
find_user_pyproject_toml()
|
||||
}
|
||||
|
||||
fn find_user_pyproject_toml() -> Option<PathBuf> {
|
||||
let mut path = dirs::config_dir()?;
|
||||
path.push("ruff");
|
||||
path.push("pyproject.toml");
|
||||
if path.is_file() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
|
||||
let absolute_sources: Vec<PathBuf> = sources
|
||||
.iter()
|
||||
.flat_map(|source| source.absolutize().map(|path| path.to_path_buf()))
|
||||
.collect();
|
||||
if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
|
||||
for directory in prefix.ancestors() {
|
||||
if directory.join(".git").is_dir() {
|
||||
return Some(directory.to_path_buf());
|
||||
}
|
||||
if directory.join(".hg").is_dir() {
|
||||
return Some(directory.to_path_buf());
|
||||
}
|
||||
if directory.join("pyproject.toml").is_file() {
|
||||
return Some(directory.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env::current_dir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::pyproject::{
|
||||
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn deserialize() -> Result<()> {
|
||||
let pyproject: PyProject = toml::from_str(r#""#)?;
|
||||
assert_eq!(pyproject.tool, None);
|
||||
|
||||
let pyproject: PyProject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(pyproject.tool, Some(Tools { ruff: None }));
|
||||
|
||||
let pyproject: PyProject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Config {
|
||||
line_length: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
select: None,
|
||||
ignore: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
let pyproject: PyProject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
line-length = 79
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Config {
|
||||
line_length: Some(79),
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
select: None,
|
||||
ignore: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
let pyproject: PyProject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
exclude = ["foo.py"]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Config {
|
||||
line_length: None,
|
||||
exclude: Some(vec!["foo.py".to_string()]),
|
||||
extend_exclude: None,
|
||||
select: None,
|
||||
ignore: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
let pyproject: PyProject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
select = ["E501"]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Config {
|
||||
line_length: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
select: Some(vec![CheckCode::E501]),
|
||||
ignore: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
let pyproject: PyProject = toml::from_str(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
ignore = ["E501"]
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
pyproject.tool,
|
||||
Some(Tools {
|
||||
ruff: Some(Config {
|
||||
line_length: None,
|
||||
exclude: None,
|
||||
extend_exclude: None,
|
||||
select: None,
|
||||
ignore: Some(vec![CheckCode::E501]),
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
assert!(toml::from_str::<PyProject>(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
line_length = 79
|
||||
"#,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
assert!(toml::from_str::<PyProject>(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
select = ["E123"]
|
||||
"#,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
assert!(toml::from_str::<PyProject>(
|
||||
r#"
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
line-length = 79
|
||||
other-attribute = 1
|
||||
"#,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_and_parse_pyproject_toml() -> Result<()> {
|
||||
let cwd = current_dir()?;
|
||||
let project_root =
|
||||
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")])
|
||||
.expect("Unable to find project root.");
|
||||
assert_eq!(project_root, cwd.join("resources/test/fixtures"));
|
||||
|
||||
let path =
|
||||
find_pyproject_toml(&Some(project_root)).expect("Unable to find pyproject.toml.");
|
||||
assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
|
||||
|
||||
let pyproject = parse_pyproject_toml(&path)?;
|
||||
let config = pyproject
|
||||
.tool
|
||||
.and_then(|tool| tool.ruff)
|
||||
.expect("Unable to find tool.ruff.");
|
||||
assert_eq!(
|
||||
config,
|
||||
Config {
|
||||
line_length: Some(88),
|
||||
exclude: None,
|
||||
extend_exclude: Some(vec![
|
||||
"excluded.py".to_string(),
|
||||
"migrations".to_string(),
|
||||
"directory/also_excluded.py".to_string(),
|
||||
]),
|
||||
select: None,
|
||||
ignore: None,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
120
src/settings.rs
120
src/settings.rs
|
@ -1,68 +1,25 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use glob::Pattern;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::checks::{CheckCode, ALL_CHECK_CODES};
|
||||
use crate::fs;
|
||||
use crate::pyproject::load_config;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FilePattern {
|
||||
Simple(&'static str),
|
||||
Complex(Pattern, Option<Pattern>),
|
||||
}
|
||||
|
||||
impl FilePattern {
|
||||
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
|
||||
let path = Path::new(pattern);
|
||||
let absolute_path = match project_root {
|
||||
Some(project_root) => fs::normalize_path_to(path, project_root),
|
||||
None => fs::normalize_path(path),
|
||||
};
|
||||
|
||||
let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern.");
|
||||
let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
|
||||
Some(Pattern::new(pattern).expect("Invalid pattern."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
FilePattern::Complex(absolute, basename)
|
||||
}
|
||||
}
|
||||
use crate::checks::CheckCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Settings {
|
||||
pub pyproject: Option<PathBuf>,
|
||||
pub project_root: Option<PathBuf>,
|
||||
pub line_length: usize,
|
||||
pub exclude: Vec<FilePattern>,
|
||||
pub extend_exclude: Vec<FilePattern>,
|
||||
pub select: BTreeSet<CheckCode>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn for_rule(check_code: CheckCode) -> Self {
|
||||
Self {
|
||||
pyproject: None,
|
||||
project_root: None,
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
select: BTreeSet::from([check_code]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
|
||||
Self {
|
||||
pyproject: None,
|
||||
project_root: None,
|
||||
line_length: 88,
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
select: BTreeSet::from_iter(check_codes),
|
||||
}
|
||||
}
|
||||
|
@ -76,78 +33,3 @@ impl Hash for Settings {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
|
||||
vec![
|
||||
FilePattern::Simple(".bzr"),
|
||||
FilePattern::Simple(".direnv"),
|
||||
FilePattern::Simple(".eggs"),
|
||||
FilePattern::Simple(".git"),
|
||||
FilePattern::Simple(".hg"),
|
||||
FilePattern::Simple(".mypy_cache"),
|
||||
FilePattern::Simple(".nox"),
|
||||
FilePattern::Simple(".pants.d"),
|
||||
FilePattern::Simple(".ruff_cache"),
|
||||
FilePattern::Simple(".svn"),
|
||||
FilePattern::Simple(".tox"),
|
||||
FilePattern::Simple(".venv"),
|
||||
FilePattern::Simple("__pypackages__"),
|
||||
FilePattern::Simple("_build"),
|
||||
FilePattern::Simple("buck-out"),
|
||||
FilePattern::Simple("build"),
|
||||
FilePattern::Simple("dist"),
|
||||
FilePattern::Simple("node_modules"),
|
||||
FilePattern::Simple("venv"),
|
||||
]
|
||||
});
|
||||
|
||||
impl Settings {
|
||||
pub fn from_pyproject(pyproject: Option<PathBuf>, project_root: Option<PathBuf>) -> Self {
|
||||
let config = load_config(&pyproject);
|
||||
let mut settings = Settings {
|
||||
line_length: config.line_length.unwrap_or(88),
|
||||
exclude: config
|
||||
.exclude
|
||||
.map(|paths| {
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| FilePattern::from_user(path, &project_root))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
|
||||
extend_exclude: config
|
||||
.extend_exclude
|
||||
.map(|paths| {
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| FilePattern::from_user(path, &project_root))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
select: if let Some(select) = config.select {
|
||||
BTreeSet::from_iter(select)
|
||||
} else {
|
||||
BTreeSet::from_iter(ALL_CHECK_CODES)
|
||||
},
|
||||
pyproject,
|
||||
project_root,
|
||||
};
|
||||
if let Some(ignore) = &config.ignore {
|
||||
settings.ignore(ignore);
|
||||
}
|
||||
settings
|
||||
}
|
||||
|
||||
pub fn select(&mut self, codes: Vec<CheckCode>) {
|
||||
self.select.clear();
|
||||
for code in codes {
|
||||
self.select.insert(code);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ignore(&mut self, codes: &[CheckCode]) {
|
||||
for code in codes {
|
||||
self.select.remove(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue